第三聖子
最近學習unix網絡編程,感覺東西零零碎碎,比較混亂。因此決定整理以下,發一個小博客。一來可以與大家分享以下,二來可以總結提高一下所學的東西。話說:竹子為什么長的高,因為它喜歡總結阿~~^_^
廢話不多說了,上代碼。小弟半路出家,入行不深,過路大神不喜勿噴阿,嘿嘿~~^_^
程序是一個基於tcp的 C/S .簡單回顯功能( 聲明以下,不要以為注釋是英語就說我是在哪里下載的,原因是我運行程序 漢字老顯示亂碼,就改成蹩腳英語了 )。
首先是一個自己的庫
1 #ifndef MYLIB_H 2 #define MYLIB_H 3 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <netinet/in.h> 7 #include <sys/socket.h> 8 #include <arpa/inet.h> 9 #include <unistd.h> 10 #include <string.h> 11 #include <errno.h> 12 #include <signal.h> 13 #include <sys/wait.h> 14 15 #define LISTENQ 1024 16 #define MAXLINE 1460 17 #define SERV_PORT 9877 18 19 typedef void (*SignalFunc)(int); 20 21 SignalFunc signal(int sigNo,SignalFunc fun); 22 void sig_chld(int sigNo); 23 void sys_err(char *pa); 24 25 #endif // MYLIB_H
這些是需要的頭文件和一些宏定義,服務端和客戶端都需要,我都把他們搞一塊兒了,這樣方便,叫mylib.h 。
哦,先大致解釋一下:
1、signal 這個函數是用來捕獲信號的。后邊服務端會用到,在服務端在細說
2、sig_chld 是signal捕獲到信號后的處理函數
3、sys_err 用來輸出提示,並退出進程
下邊是頭文件里函數的實現,里邊的函數如果沒看太懂可以先不用理解,后邊會細說,哈哈
1 #include <mylib.h> 2 3 void sys_err(char *pa) 4 { 5 printf("%s",pa); 6 exit(1); 7 } 8 9 SignalFunc signal(int sigNo, SignalFunc fun){ 10 struct sigaction act , oact; 11 act.sa_handler=fun; 12 sigemptyset(&act.sa_mask); //Additional set of signals to be blocked. 13 act.sa_flags=0; 14 if(sigaction(sigNo,&act,&oact)<0) 15 return SIG_ERR; 16 return oact.sa_handler; 17 } 18 19 void sig_chld(int sigNo) 20 { 21 pid_t pid; 22 int state; 23 while ((pid=waitpid(-1,&state,WNOHANG))>0) { 24 printf("process %d terminated \n",pid); 25 } 26 return; 27 }
下邊是客戶端代碼:
1 #include <mylib.h> 2 int main(void) 3 { 4 int sock_fd; 5 sock_fd=socket(AF_INET,SOCK_STREAM,0); 6 7 struct sockaddr_in serv_add; 8 bzero(&serv_add,sizeof(serv_add)); 9 serv_add.sin_family=AF_INET; 10 serv_add.sin_port=htons(SERV_PORT); 11 struct in_addr add; 12 inet_aton("192.168.1.105",&add); 13 serv_add.sin_addr=add; 14 15 if(connect(sock_fd,(struct sockaddr *)&serv_add,sizeof(serv_add))<0) 16 sys_err("connect error!\n"); 17 char sendbuff[MAXLINE],recvbuff[MAXLINE]; 18 char *temp; 19 ssize_t n; 20 while ((temp=fgets(sendbuff,sizeof(sendbuff),stdin)) !=NULL) 21 { 22 int k; 23 if((k=write(sock_fd,sendbuff,sizeof(sendbuff)))<0) 24 { 25 26 } 27 if((n=read(sock_fd,recvbuff,sizeof(recvbuff)))>0) 28 printf("SVR:%s",recvbuff); 29 if(n<0) 30 printf("fail to get data from server %s\n",inet_ntoa(serv_add.sin_addr)); 31 if(n==0) 32 { 33 //broken pipe ,haha sigpipe 34 printf("%s","server defunct\nclosing the socket..."); 35 close(sock_fd); 36 } 37 } 38 return 0; 39 }
解釋一下客戶端代碼:
第5行:sock_fd=socket(AF_INET,SOCK_STREAM,0); 用socket函數創建一個sock,第一個參數是協議族,我們用AF_INET代表tcp/ip協議族。第二個參數代表流方式,也就是TCP 字節流方式。第三個參數,額...貌似有點高深,說實話,不懂,注釋說 If it is zero, is chosen automatically. 就是自動選擇,我擦,這一自動,我感覺整個人都不舒服了...
行7:struct sockaddr_in serv_add; 定義一個sock 地址結構,用來存放服務器斷ip,端口,協議族之類的。
行10,12:這兩行里都有一個來處理端口和ip,為啥,這里牽扯一個“字節序”的問題,處理器對字節的排列順序不是相同的,這個可以百度以下,呵呵
行15:用本地初始化的sock和服務端地質結構建立連接。等等,為啥客戶端sock沒有地址和端口呢,怎么直接就連接了,這不科學。額,就這在這一不,tcp默認將本地sock地址設為本地ip,端口在允許范圍內隨機取值,一般不會是vip端口(<1024)啦。而且每次鏈接都會隨機端口。好,地址設好就可以連接服務器了,進行關鍵的三次握手。
行20,23:從輸入設備讀取 輸入值。寫入打開的 socket 文件符。將輸入值 寫入建好的 pipe里。這里的write為什么回有小於0的情況呢,原因是,當服務起進程斷開連接,或者不小心關閉時,服務端乎發給客戶端一個FIN,表示終止鏈接,但是由於TCP是半關閉的,客戶端可能正在輸入,不知道服務端已經斷開了,服務端TCP會返回一個RST,告訴客戶端管道不通。這時如果繼續將值寫入pipe,系統就會立馬提示你 管道斷裂,發一個SIGPIPE信號給你。這個信號很要命啊,你不捕獲處理,系統就默認關掉你的進程。處理了,write就返回小於0
行27:讀取服務器的返回值,讀取失敗就返回小於0
行31:同樣,服務斷斷開后,客戶端已收到FIN的通知,read后直接返回0. 這里為了不讓出現23行的問題,干脆把socket關閉了
下邊是服務端代碼:
1 #include <mylib.h> 2 3 int main(void) 4 { 5 int listen_fd , connected_fd; 6 struct sockaddr_in serv_add; 7 //----------------------------------------------------------------------------- 8 listen_fd=socket(AF_INET,SOCK_STREAM,0); 9 serv_add.sin_family=AF_INET; 10 serv_add.sin_port=htons(SERV_PORT); 11 serv_add.sin_addr.s_addr=htonl(INADDR_ANY); 12 //------------------------------------------------------------------------------ 13 if(bind(listen_fd,(struct sockaddr *)&serv_add,sizeof(serv_add))<0) 14 sys_err("bind error\n"); 15 if(listen(listen_fd,LISTENQ)<0) 16 sys_err("listen error\n"); 17 18 signal(SIGCHLD,sig_chld); 19 20 __pid_t pid; 21 while (1) { 22 connected_fd = accept(listen_fd,0,0); 23 if(connected_fd<0){ 24 if(errno==EINTR) 25 { 26 printf("interrupt\n"); 27 continue; 28 } 29 else 30 sys_err("accept eero!\n"); 31 } 32 if((pid=fork())==0) 33 { 34 char recevBuff[MAXLINE]; 35 int n; 36 close(listen_fd); 37 while ((n=read(connected_fd,recevBuff,MAXLINE))>0) { 38 printf("Client:%s",recevBuff); 39 write(connected_fd,recevBuff,MAXLINE); 40 } 41 if(n<0) 42 sys_err("read error\n"); 43 close(connected_fd); 44 exit(0); 45 } 46 close(connected_fd); 47 } 48 return 0; 49 }
服務端和客戶端代碼有一些相似的地方。說一下不一樣的地方把。打字打的手要抽了都。
行11:意思是通配本主機上所有的網絡接口(如果有多個的話).就是不管哪個接口受到請求都去處理連接
行13:把sock文件符綁定到指定的地址和端口上,形成一個完整sock。
行15:服務端sock打開偵聽文件符,不同的sock過來建立連接是要排隊的,第二個參數控制最大排隊的數量,畢竟緩沖區是有限的
行22:服務端進入偵聽后回阻塞在accept,如果一個tcp三次握手成功了,就打開一個accepted_fd,並建立一個通道。繼續往下走
行23:鏈接錯誤或者鏈接被內核中斷,就會返回小於0,因為這時進程處在一個可被中斷的睡眠狀態。如果進程接到要去處理進程的通知,這個睡眠會被喚醒,而且 內核不一定就回重啟這個等待,不重啟的時候就回返回一個errno=EINTR(error interrupt),這時,我們就重新啟動這個等待 , continue
行32:這里有個比較重要的函數 fork,它是系統唯一能創造分支進程的方法,就是子進程。
QA-01:為什么這里要開進程。 A:因為如果有多個鏈接連入服務器的話,一個進程肯定忙不過來阿,這樣就會導致很對在那排隊等待,有的甚至連不上,因為服務起很忙。
QA-02:if((pid=fork())==0) ,為什么這里這么寫呢?因為 這個fork函數很特別,調用一次會返回兩個值,一個是子進程的pid,一個是0 。類似於一個鏈表格式 ppid | pid |chldpid ,子進程pid在父進程里返回,子進程就返回0 。進入子進程后,父進程的所有文件符都會復制到子進程的上下文,是的,是復制。子進程對文件符的操作不會影響父進程,父進程也不會影響子進程。子進程執行完畢后必須 退出,否則的話 可能會繼續fork子進程,死循環。
這樣以來,每次成功建立連接都會有一個獨立的進程去處理他們的數據交流,不會阻塞在父進程,就完成了 並發處理。
行43:這里為什么要關閉 connected fd呢,因為如果不關閉的話,每來一個鏈接都會新建一個fd(file describe),少年,內核里進程表表項里存儲文件符的數組大小可是有限的。這里子進程也會關掉從父進程復制來的文件符,這個文件符是有計數的,稱謂共享,當計數恢復0時,文件符就關了。
最后說行18:捕獲信號。 當這些個子進程都完成自己任務后 ( 也就是客戶端斷了之后 ),不會自動退出內核。而是變成了 defunct 狀態,掛掉了。木錯,是掛掉了!
為什么兒子們都掛掉了,老爹不來收屍呢? 這個原因貌似是比較復雜,因為子進程結束了,要通知父進程一些關於自己執行情況的數據 。父進程默認是忽略的,等父進程結束的時候,這些 僵死進程就會 被只給 進程 1, init,他恢復則處理這些 掛掉的進程。
但是我們的服務起 肯定不想讓這些 掛掉的進程 擠滿內存,占據資源,於是就在 行18 捕獲子進程發來的信號 SIGCHLD ,然后,進程如果接到信號就會從睡眠中蘇醒,去 wait 它,這個函數很特別,他會負責處理掉這些掛掉的進程。
好吧,服務端是比較復雜,這里代碼肯定是有很多缺陷的。一個服務要想跑起來 要考慮非常多的突發情況,攻擊神馬的,這個小程序只是打通通信過程,呵!呵!
寫到這里,我又凌亂了.....睡覺
機智的少年 估計去開發局域網聊天程序了 o(∩_∩)o...
哦,忘了上截圖,sorry ,所謂無圖無真相: