linux下的socket通信小程序分享——第三聖子


第三聖子

最近學習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 ,所謂無圖無真相:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM