接着前兩天繼續看《VC深入詳解》的網絡編程部分,這次我快速看了遍書上的函數以及套接字C-S模型,然后自己從0開始寫了個簡單的服務端,結果發現一直在輸出
而明明我還沒有寫客戶端程序,由於打印的代碼只有一處,在如下的while循環里
while (true) { /* 5. 接收客戶端發送的連接請求 */ SOCKET sockConnect = accept(sockServer, (SOCKADDR*)&addrClient, &len); /* 6. [發送/接收]數據 */ char sendBuf[BUFSIZ]; char ipBuf[INET_ADDRSTRLEN]; sprintf(sendBuf, "Welcome [IP: %s] to Server", inet_ntop(AF_INET, &addrClient.sin_addr, ipBuf, sizeof(ipBuf))); send(sockConnect, sendBuf, sizeof(sendBuf), 0); char recvBuf[BUFSIZ]; recv(sockConnect, recvBuf, BUFSIZ, 0); printf("Receive Data: %s\n", recvBuf); /* 7. 斷開連接,關閉套接字 */ closesocket(sockConnect); }
引用《UNIX網絡編程》:accept函數由TCP服務器調用,用於從完成連接隊列頭返回下一個已完成連接。如果已完成連接隊列為空,那么進程被投入睡眠(假定套接字為默認的阻塞方式)。
調試發現,每一次accept函數都成功完成並執行后續代碼,所以才會有無限循環打印的現象。仔細對比書上代碼和說明,我的accept函數也沒有用錯,於是頭疼了很久。
How often have I said to you that when you have eliminated the impossible, whatever remains, however improbable, must be the truth?
既然我沒有得到程序錯誤的真相,那么說明我沒有排除掉所有的錯誤。回顧之前代碼,我跟示例代碼的區別就在於我把綁定IP和端口的代碼封裝起來了。
/* 3. 綁定套接字到本地的地址和端口 */ SOCKADDR_IN addrServer = SockAddr(6000, "127.0.0.1"); /* 4. 將套接字設為監聽模式, 准備接收連接請求 */ listen(sockServer, SOMAXCONN);
inline SOCKADDR_IN SockAddr(u_short port, PCSTR ip, int af = AF_INET) { SOCKADDR_IN sockAddr_In; sockAddr_In.sin_family = af; sockAddr_In.sin_port = htons(port); auto& addr = sockAddr_In.sin_addr.S_un.S_addr; int err = inet_pton(af, ip, &addr); if (err == 0) { throw std::runtime_error("IP地址字節序從網絡轉換到主機出錯!"); } else if (err == -1) { throw std::runtime_error("IP地址輸入格式無效!"); } return sockAddr_In; }
但是錯誤並沒有出在我的SockAddr函數,而是在於我沒有綁定!漏掉了關鍵的一行代碼
bind(sockServer, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));
我僅僅只是創建了一個包含協議家族、IP地址、端口號的結構體SOCKADDR_IN,雖然也輸入了IP和端口信息,但是卻沒有把SOCKADDR_IN當做SOCKADDR類型(套接字的地址信息)和SOCKET(套接字本身)進行綁定,而沒有綁定的后果呢?
繼續引用《UNIX網絡編程》:如果一個TCP客戶或服務器未曾調用bind捆綁一個端口,當調用connect或listen時,內核就要為套接字選擇一個臨時端口。讓內核選擇臨時端口對於TCP服務器來說非常罕見,因為服務器是通過它們的眾所周知端口被大家認識。
調試發現sockConnect(也就是accept的返回值)是4294967295。稍微敏感的就會發現,這是32位無符號整型的上限值,也就是2^32-1,而SOCKET類型實際上就是UINT_PTR(unsigned int表示的指針,也就是32位地址)
typedef UINT_PTR SOCKET;
再仔細看看《UNIX網絡編程》上關於accept函數的解釋
若出錯則為-1,UNIX上的accept是返回int,而windows上則是相當於返回unsigned int,-1的二進制表示就是每一位都為1,對應unsigned int的上限值(UINT_MAX)。所以剛才並不是每次都連接成功,而是每次都連接出錯(因為試圖用一個未綁定地址的服務器socket來接收客戶的連接請求),返回錯誤碼-1。如果accept正常,則是有客戶連接則返回一個表示連接的socket,沒有客戶連接則使進程睡眠直到有客戶連接(即阻塞)。
總結下來,根本原因還是socket沒有配置好就調用accept函數了,同樣,如果注釋掉listen那行,也會出現同樣的現象。
----------------------------------------------------------粗心的分割線----------------------------------------------------------
最后附上一點感悟,急於求成反而會浪費更多的時間,但也有個好處,如果照着書上代碼敲一遍,運行正確,然后再看看每個函數代表什么,也許當時就清楚了,但是后來出錯時可能就不知道到底掉到那個坑了。有出錯經驗也不錯。《UNIX網絡編程》確實是本不錯的書,對套接字API講得很詳細,像htnol還有inet_addr等轉換函數還是看這本書講得更清楚。