1. 程式人生 > >socket之send和recv原理剖析

socket之send和recv原理剖析

談到網路socket程式設計, 我們不得不提兩個基本也很重要的函式:send和recv.  對socket程式設計理解不深的童鞋容易產生這樣一個錯誤的認識: send函式是用來發送資料, 而recv函式是用來接收資料的, 其實, 這種觀點是稍微有點偏頗的, 掩蓋了本質。

       下面, 我們看一幅圖, 瞭解一下send和recv函式所處的位置(這幅圖是我在網上找的, 不太清晰, 請湊合看一下):


         為了簡便起見, 我們僅考慮單向的資料流, 即A(客戶端)向B(服務端)傳送資料。 在應用程式Program A中, 我們定義一個數組char szBuf[100] = "tcp"; 那麼這個szBuf就是應用程式緩衝區(對應上圖的Program A中的某塊記憶體), send函式對應上面藍色的Socket API, 核心緩衝區對應上面的黃色部分。 我們看到, send函式的作用是把應用程式緩衝區中的資料拷貝到核心緩衝區, 僅此而已。 核心緩衝區中的資料經過網絡卡, 經歷網路傳到B端的網絡卡(TCP協議), 然後進入B的核心緩衝區, 然後由recv函式剪下/複製到Program B的應用程式緩衝區。前面我們用過wireshark抓包, wireshark抓的正是流經網絡卡的資料。

        強調一下:

        1. 對於客戶端A, 其傳送的核心緩衝區和接收的核心緩衝區是不一樣的, 互不干擾. 服務端B也同理。

        2. recv函式是剪下還是複製, 由最後一個引數決定, 我們在之前的博文已經講述過了。

        下面, 我們不考慮recv函式, 僅僅玩轉一下send(雙向send), 並用wireshark抓包實驗一下, 加深理解。 注意, Wireshark抓不了環回包, 所以, 需要在兩臺電腦上測試。

         服務端B的程式為:

  1. #include <stdio.h>
  2. #include <winsock2.h> // winsock介面
  3. #pragma comment(lib, "ws2_32.lib") // winsock實現
  4. int main()  
  5. {  
  6.     WORD wVersionRequested;  // 雙位元組,winsock庫的版本
  7.     WSADATA wsaData;         // winsock庫版本的相關資訊
  8.     wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
  9.     // 載入winsock庫並確定winsock版本,系統會把資料填入wsaData中
  10.     WSAStartup( wVersionRequested, &wsaData );  
  11.     // AF_INET 表示採用TCP/IP協議族
  12.     // SOCK_STREAM 表示採用TCP協議
  13.     // 0是通常的預設情況
  14.     unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);  
  15.     SOCKADDR_IN addrSrv;  
  16.     addrSrv.sin_family = AF_INET; // TCP/IP協議族
  17.     addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket對應的IP地址
  18.     addrSrv.sin_port = htons(8888); // socket對應的埠
  19.     // 將socket繫結到某個IP和埠(IP標識主機,埠標識通訊程序)
  20.     bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));  
  21.     // 將socket設定為監聽模式,5表示等待連線佇列的最大長度
  22.     listen(sockSrv, 5);  
  23.     // sockSrv為監聽狀態下的socket
  24.     // &addrClient是緩衝區地址,儲存了客戶端的IP和埠等資訊
  25.     // len是包含地址資訊的長度
  26.     // 如果客戶端沒有啟動,那麼程式一直停留在該函式處
  27.     SOCKADDR_IN addrClient;  
  28.     int len = sizeof(SOCKADDR);  
  29.     unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);  
  30.     while(1)  
  31.     {  
  32.         getchar(); // 阻塞一下
  33.         send(sockConn, "tcp", strlen("tcp") + 1, 0); // send來啦      
  34.     }  
  35.     closesocket(sockConn);    
  36.     closesocket(sockSrv);  
  37.     WSACleanup();  
  38.     return 0;  
  39. }  
      先啟動服務端B.

      下面我們來看客戶端A:

  1. #include <winsock2.h>
  2. #include <stdio.h>
  3. #pragma comment(lib, "ws2_32.lib")
  4. int main()  
  5. {  
  6.     WORD wVersionRequested;  
  7.     WSADATA wsaData;  
  8.     wVersionRequested = MAKEWORD(1, 1);  
  9.     WSAStartup( wVersionRequested, &wsaData );  
  10.     SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);  
  11.     SOCKADDR_IN addrSrv;  
  12.     addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.101");  
  13.     addrSrv.sin_family = AF_INET;  
  14.     addrSrv.sin_port = htons(8888);  
  15.     connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));  
  16.     while(1)  
  17.     {  
  18.         getchar();  
  19.         send(sockClient, "cpp", strlen("cpp") + 1, 0);  
  20.     }  
  21.     closesocket(sockClient);  
  22.     WSACleanup();  
  23.     return 0;  
  24. }  
      好, 再啟動客戶端A.

      我們在客戶端A上安裝wireshark並啟動抓包, 實驗發現: 

     1. 當A向B傳送資料時, A上的wireshark可以抓到對應的包, 因為資料經過了A的網絡卡。(不管B是否有去recv)

     2. 當B向A傳送資料時, A上的wireshark也可以抓到對應的包,    因為資料到達了A的核心緩衝區, 也經歷了A的網絡卡。(不管A是否有去recv)

      相信通過本文的討論, 又加深了對send和recv的認識。 未來, 路漫漫, 我們慢慢來。