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的程式為:
- #include <stdio.h>
-
#include <winsock2.h> // winsock介面
- #pragma comment(lib, "ws2_32.lib") // winsock實現
- int main()
- {
- WORD wVersionRequested; // 雙位元組,winsock庫的版本
- WSADATA wsaData; // winsock庫版本的相關資訊
- wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
- // 載入winsock庫並確定winsock版本,系統會把資料填入wsaData中
-
WSAStartup( wVersionRequested, &wsaData );
- // AF_INET 表示採用TCP/IP協議族
- // SOCK_STREAM 表示採用TCP協議
- // 0是通常的預設情況
- unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
- SOCKADDR_IN addrSrv;
- addrSrv.sin_family = AF_INET; // TCP/IP協議族
- addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket對應的IP地址
- addrSrv.sin_port = htons(8888); // socket對應的埠
- // 將socket繫結到某個IP和埠(IP標識主機,埠標識通訊程序)
- bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
- // 將socket設定為監聽模式,5表示等待連線佇列的最大長度
- listen(sockSrv, 5);
- // sockSrv為監聽狀態下的socket
- // &addrClient是緩衝區地址,儲存了客戶端的IP和埠等資訊
- // len是包含地址資訊的長度
- // 如果客戶端沒有啟動,那麼程式一直停留在該函式處
- SOCKADDR_IN addrClient;
- int len = sizeof(SOCKADDR);
- unsigned int sockConn = accept(sockSrv,(SOCKADDR*)&addrClient, &len);
- while(1)
- {
- getchar(); // 阻塞一下
- send(sockConn, "tcp", strlen("tcp") + 1, 0); // send來啦
- }
- closesocket(sockConn);
- closesocket(sockSrv);
- WSACleanup();
- return 0;
- }
下面我們來看客戶端A:
- #include <winsock2.h>
- #include <stdio.h>
- #pragma comment(lib, "ws2_32.lib")
- int main()
- {
- WORD wVersionRequested;
- WSADATA wsaData;
- wVersionRequested = MAKEWORD(1, 1);
- WSAStartup( wVersionRequested, &wsaData );
- SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
- SOCKADDR_IN addrSrv;
- addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.101");
- addrSrv.sin_family = AF_INET;
- addrSrv.sin_port = htons(8888);
- connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
- while(1)
- {
- getchar();
- send(sockClient, "cpp", strlen("cpp") + 1, 0);
- }
- closesocket(sockClient);
- WSACleanup();
- return 0;
- }
我們在客戶端A上安裝wireshark並啟動抓包, 實驗發現:
1. 當A向B傳送資料時, A上的wireshark可以抓到對應的包, 因為資料經過了A的網絡卡。(不管B是否有去recv)
2. 當B向A傳送資料時, A上的wireshark也可以抓到對應的包, 因為資料到達了A的核心緩衝區, 也經歷了A的網絡卡。(不管A是否有去recv)
相信通過本文的討論, 又加深了對send和recv的認識。 未來, 路漫漫, 我們慢慢來。