管道的性質
- 讀緩沖區為空,read阻塞
- 寫緩沖區為空,write阻塞
- 一端先close,另一端繼續read,read不阻塞,立刻返回0
- 一端先close,另一端繼續write,write會觸發SIGPIPE信號,進程異常終止
socket和管道性質基本一致
socket的性質幾乎和管道是一樣的,唯一的區別是socket是全雙工的。
寫端先close,讀端繼續recv:recv不阻塞,返回0。
讀端先close,寫端后send:讀端會接接收到第一次的send,隨后關閉。
使用gdb
使用命令行:gdb --args? <./xxxx>? <ip>? ?<port>,
會開啟gdb并且執行命令。
close
關閉文件描述符。具體類似于關閉管道。
基于tcp socket的即時聊天
實現的大致方式為:
?若使用:
while(1){read(stdin);……recv(通信socket); }
會導致串聯式問題
需要改成并聯式解決問題
使用select IO 多路復用。(也可以使用多線程解決,但是IO多路復用比多線程好的多)
select IO多路復用
同時等待多個資源就緒
- 設計一個數據結構 fd_set 用來接收監聽集合
- FD_ZERO(&set) 初始化數據結構,同時將關注的資源加入 fd_set ——FD_SET()
- 操作系統內核做輪詢,進程在等待 select()
- 有任何一個資源就緒了,輪詢結束,返回一個就緒集合
- 進程根據就緒集合去做IO操作 FD_ISSET()
代碼實現:
使用bzero()初始化buf,需要加入頭文件#include <strings.h>
sever.c
#include <my_header.h>/* Usage: */ int main(int argc, char *argv[]){ ARGS_CHECK(argc,3);int sockfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));ERROR_CHECK(ret,-1,"bind");listen(sockfd,50);int netfd = accept(sockfd,NULL,NULL);char buf[4096];fd_set readset;//讀集合while(1){FD_ZERO(&readset);//初始化FD_SET(STDIN_FILENO,&readset);//把stdin加入監聽FD_SET(netfd,&readset);//把通信socket加入監聽select(netfd+1,&readset,NULL,NULL,NULL);//內核輪詢,進程等待//有至少一個就緒//此時readset變成就緒隊列if(FD_ISSET(STDIN_FILENO,&readset)){bzero(buf,sizeof(buf));ssize_t sret = read(STDIN_FILENO,buf,sizeof(buf));if(sret == 0){send(sockfd,"serve closed\n",12,0);printf("I will close connect\n");break;}send(netfd,buf,strlen(buf),0);} if(FD_ISSET(netfd,&readset)){//對面來消息了或者對面斷開了bzero(buf,sizeof(buf));ssize_t sret = recv(netfd,buf,sizeof(buf),0);if(sret == 0){printf("close connect\n");break;}printf("buf = %s\n",buf);}}close(sockfd);close(netfd);return 0; }
client.c
#include <my_header.h>/* Usage: */ int main(int argc, char *argv[]){ ARGS_CHECK(argc,3);int sockfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));ERROR_CHECK(ret,-1,"connect");char buf[4096];fd_set readset;//讀集合while(1){FD_ZERO(&readset);//初始化FD_SET(STDIN_FILENO,&readset);//把stdin加入監聽FD_SET(sockfd,&readset);//把通信socket加入監聽select(sockfd+1,&readset,NULL,NULL,NULL);//內核輪詢,進程等待//有至少一個就緒//此時readset變成就緒隊列if(FD_ISSET(STDIN_FILENO,&readset)){bzero(buf,sizeof(buf));ssize_t sret = read(STDIN_FILENO,buf,sizeof(buf));if(sret == 0){send(sockfd,"client closed\n",13,0);printf("I will close connect\n");break;}send(sockfd,buf,strlen(buf),0);}if(FD_ISSET(sockfd,&readset)){bzero(buf,sizeof(buf));ssize_t sret = recv(sockfd,buf,sizeof(buf),0);if(sret ==0){printf("close connect\n");break;}printf("buf = %s\n",buf);}}close(sockfd);return 0; }
服務端與客戶端的大致代碼一樣,只需要做少量修改。可以復制一份在做更改即可。
vim文本更改技巧 :<n>,<m>s/<src>/<dec> 將第n行到第m行的src更改為dec
退出時,我們可以使用更加溫和的方式,用ctrl + D實現退出。
Linux上 ctrl + D 是讓recv就緒,并且返回值為0。
服務端主動退出
當退出后,短時間再次連接,主動退出的以訪會進入TIME_WAIT狀態,此狀態下,bind函數會報錯。socket庫這樣設計的原因是為了避免相同的四元組連接在短時間內斷開又重連。實際上該設計過分嚴格,僅針對于bind,而我們在實現時,客戶端并不使用bind,每次都是不同的ip與port,只有服務端使用了bind。
修改socket的屬性
setsockopt(sockfd,level,optname,*optval,optlen)
這是一個函數,可以修改很多種類屬性,不同種類的屬性是不一樣的
使用<man 7 socket>查詢手冊
在bind時無視TIME_WITE參數選項:(flag = 0/1,flag = 1允許重復使用本地址)?
optval 是參數的地址,optlen是參數的長度,用來支持多種類型。?
TIME_WITE狀態依舊存在,只是在bind時無視掉了。
setsockopt()使用在服務端socket之后,bind之前
int flag = 1;setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
?
斷線重連
應用層的斷線重連,A退出了,B不要立刻終止,而是等待A的重新連接。
accept的本質是一個讀全連接隊列的操作,accept的阻塞就緒可以由select監聽。select除了監聽數據,還可以用來監聽發有沒有新的連接到來。
幾種情況
A的狀態
- 無連接狀態(單身狀態):等待B連接。此時,select監聽sockfd。
- 通信狀態(戀愛狀態):等待stdin、B發送數據。此時select監聽stdin 和 netfd。
解除監聽集合和就緒集合之間的耦合
把兩個無關的東西扯在一起稱為耦合,現在需要解除耦合
while(1){memcpy(&readset,&monitorset,sizeof(fd_set));select(nfds,&readset,NULL,NULL,NULL);}
若服務器A與B通信時,此時C與服務器A,請求建立連接,是可以成功的,服務器A將C放入全連接隊列但是不進行通信。這是因為建立連接是由操作系統內核完成的。等到客戶端B終止了,就可以從全連接隊列取出C進行通信。
當客戶端斷開連接
客戶端B斷開連接,netfd就緒,recv的返回值是0。
此時需要:
- 重新監聽sockfd
- 取消監聽netfd和stdin
- 修改netfd == -1
代碼實現
select()第一個參數不知道寫什么可以寫1024。
serve.c
#include <my_header.h>/* Usage: */ int main(int argc, char *argv[]){ ARGS_CHECK(argc,3);int sockfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int flag = 1;setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));ERROR_CHECK(ret,-1,"bind");listen(sockfd,50);char buf[4096];fd_set readyset;//就緒集合fd_set monitorset;//監聽集合FD_ZERO(&monitorset);FD_SET(sockfd,&monitorset);int netfd = -1;while(1){//用監聽集合覆蓋就緒集合memcpy(&readyset,&monitorset,sizeof(fd_set));select(1024,&readyset,NULL,NULL,NULL);//sockfd就緒,此時accept不會zuseif(FD_ISSET(sockfd,&readyset)){netfd = accept(sockfd,NULL,NULL);printf("connect\n");//移除對sockfd的監聽FD_CLR(sockfd,&monitorset);//增加對stdin以及netfd的監聽FD_SET(STDIN_FILENO,&monitorset);FD_SET(netfd,&monitorset);}//stdin狀態if(netfd != -1 && FD_ISSET(STDIN_FILENO,&readyset)){bzero(buf,sizeof(buf));read(STDIN_FILENO,buf,sizeof(buf));send(netfd,buf,strlen(buf),0);}//netfd就緒狀態if(netfd != -1 && FD_ISSET(netfd,&readyset)){bzero(buf,sizeof(buf));ssize_t sret = recv(netfd,buf,sizeof(buf),0);if(sret == 0){FD_CLR(netfd,&monitorset);FD_CLR(STDIN_FILENO,&monitorset);FD_SET(sockfd,&monitorset);close(netfd);netfd = -1;continue;}printf("%s\n",buf);}}close(sockfd);return 0; }
client.c
#include <my_header.h>/* Usage: */ int main(int argc, char *argv[]){ ARGS_CHECK(argc,3);int sockfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int ret = connect(sockfd,(struct sockaddr *)&addr,sizeof(addr));ERROR_CHECK(ret,-1,"connect");char buf[4096];fd_set readset;//讀集合while(1){FD_ZERO(&readset);//初始化FD_SET(STDIN_FILENO,&readset);//把stdin加入監聽FD_SET(sockfd,&readset);//把通信socket加入監聽select(sockfd+1,&readset,NULL,NULL,NULL);//內核輪詢,進程等待//有至少一個就緒//此時readset變成就緒隊列if(FD_ISSET(STDIN_FILENO,&readset)){bzero(buf,sizeof(buf));ssize_t sret = read(STDIN_FILENO,buf,sizeof(buf));if(sret == 0){send(sockfd,"client closed\n",13,0);printf("I will close connect\n");break;}send(sockfd,buf,strlen(buf),0);}if(FD_ISSET(sockfd,&readset)){bzero(buf,sizeof(buf));ssize_t sret = recv(sockfd,buf,sizeof(buf),0);if(sret ==0){printf("close connect\n");break;}printf("buf = %s\n",buf);}}close(sockfd);return 0; }
?
群聊
模型
客戶端實現同上的斷線重連
服務端的幾種監聽狀態:
- 沒有客戶端,監聽sockfd
- 每連入一個客戶端,監聽對應的netfd,原來的sockfd依然保持監聽
- 客戶端斷開連接時,要取消監聽相關的netfd
由于netfd將會由很多個,我們需要設計數據結構去管理它。
設計一個數組管理netfd
int netfd[1024];//用netfd數組中的元素來存放所有的netfdint curidx = 0;//下一次進入netfd數組的元素的下標
寫代碼時,先考慮數據結構怎么處理、存儲,接著就知道如何去進行增刪改查了。?
if(FD_ISSET(sockfd,&readyset)){netfd[curidx] = accept(sockfd,NULL,NULL);printf("curidx = %d, netfd = %d\n",curidx,netfd[curidx]);FD_SET(netfd[curidx],&monitorset);++curidx;}
在監聽到sockfd連接時,使用下一個數組元素accept。
代碼實現
sever.c
#include <my_header.h>/* Usage: */ int main(int argc, char *argv[]){ ARGS_CHECK(argc,3);int sockfd = socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int flag = 1;setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));int ret = bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));ERROR_CHECK(ret,-1,"bind");listen(sockfd,50);char buf[4096];fd_set readyset;//就緒集合fd_set monitorset;//監聽集合//一直監聽sockfdFD_ZERO(&monitorset);FD_SET(sockfd,&monitorset);int netfd[1024];//用netfd數組中的元素來存放所有的netfdint curidx = 0;//下一次進入netfd數組的元素的下標for(int i=0;i<1024;i++){netfd[i]=-1;}while(1){memcpy(&readyset,&monitorset,sizeof(fd_set));select(1024,&readyset,NULL,NULL,NULL);if(FD_ISSET(sockfd,&readyset)){netfd[curidx] = accept(sockfd,NULL,NULL);printf("curidx = %d, netfd = %d\n",curidx,netfd[curidx]);FD_SET(netfd[curidx],&monitorset);++curidx;}//已連接的客戶端發消息for(int i=0;i<curidx;i++){//i號客戶端來消息if(netfd[i]!=-1&&FD_ISSET(netfd[i],&readyset)){bzero(buf,sizeof(buf));ssize_t sret = recv(netfd[i],buf,sizeof(buf),0);if(sret == 0){printf("client disconnected i = %d, netfd = %d\n",i,netfd[i]);FD_CLR(netfd[i],&monitorset);close(netfd[i]);netfd[i] = -1;continue;}for(int j =0 ;j<curidx;j++){if(j!=i&&netfd[i] != -1){send(netfd[j],buf,strlen(buf),0);}}}}}close(sockfd);return 0; }
?當然也可以使用刪除,將退出netfd元素后所有元素前移。
產生錯誤處理方式
- 使用gdb復現bug? ?<gdb? --args? ?./xxxxx>
- 查看錯誤提示
- bt 從上往下找到自己寫的代碼,分析修改
擴展
服務端超時踢人
如果客戶端十秒沒有通信,服務器將他踢掉。
timeout是一個傳入傳出參數,如果是固定的超時時間,把設置timeout放在循環里面。