最近閱讀UNIX網絡編程第四章時,書本末尾介紹了兩個函數getsockname()和getpeername(),可以用於獲取服務器端和客戶端的IP地址與端口,原本很簡單的兩個函數,過一眼即明白函數的用法,但在實際編程測試中,卻出現了一個讓人意外的結果,如下圖所示:
這兩個函數在第一個客戶連接時解析出的IP地址和端口全部為0,出乎我的期望。而在后面的客戶連接時,打印出的IP地址和端口卻是正確的。
下面先給出客戶端和服務端的代碼:
客戶端:

#include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> #include <pton32> #include <iostream> int main(int argc, char const *argv[]) { int sockfd; sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; if (argc < 2) { std::cout << "usage:" << argv[0] << " <IP address>" << std::endl; return -1; } srvaddr.sin_addr = pton32(argv[1]); srvaddr.sin_port = htons(7777); sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (connect(sockfd, (sockaddr *)&srvaddr, sizeof(srvaddr)) != 0) { std::cout << "connect error,retry please."; throw; } char str[128] = {'\0'}; while ((read(sockfd, str, 127)) > 0) { std::cout << str << ' '; } std::cout << std::endl; close(sockfd); return 0; }
服務端:
1 #include <netinet/in.h> 2 #include <sys/socket.h> 3 #include <unistd.h> 4 #include <pton32> 5 #include <iostream> 6 7 8 int main(int argc, char const *argv[]) 9 { 10 int listenfd, connfd; 11 sockaddr_in cliaddr, srvaddr; 12 13 bzero(&srvaddr, sizeof(srvaddr)); 14 srvaddr.sin_family = AF_INET; 15 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); 16 srvaddr.sin_port = htons(7777); 17 18 listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 19 if (bind(listenfd, (sockaddr *)&srvaddr, sizeof(srvaddr))) 20 { 21 std::cout << "bind failed." << std::endl; 22 return -1; 23 } 24 25 if (listen(listenfd, 32)) 26 { 27 std::cout << "listen failed." << std::endl; 28 return -1; 29 } 30 31 char str[128] = {'\0'}; 32 33 bzero(&cliaddr, sizeof(cliaddr)); 34 35 while (true) 36 { 37 socklen_t length = sizeof(cliaddr); 38 connfd = accept(listenfd, (sockaddr *)&cliaddr, &length); 39 40 sockaddr_in local, peer; 41 socklen_t local_len, peer_len; 42 bzero(&local, sizeof(local)); 43 bzero(&peer, sizeof(srvaddr)); 44 45 getsockname(connfd, (sockaddr *)&local, &local_len); 46 getpeername(connfd, (sockaddr *)&peer, &peer_len); 47 48 char buff_local[64] = {'\0'}, buff_peer[64] = {'\0'}; 49 50 if (inet_ntop(AF_INET, (void *)&local.sin_addr, buff_local, 63)) 51 { 52 std::cout << "local ip: " << buff_local 53 << "\t\tlocal port: " << ntohs(local.sin_port); 54 } 55 56 if (inet_ntop(AF_INET, (void *)&peer.sin_addr, buff_peer, 63)) 57 { 58 std::cout << "\npeer ip: " << buff_peer 59 << "\t\tpeer port: " << ntohs(peer.sin_port); 60 } 61 62 std::cout << std::endl; 63 64 write(connfd, str, 127); 65 close(connfd); 66 } 67 return 0; 68 }
其中的pton32頭文件是我個人自定義的頭文件,定義如下:

#include <netinet/in.h> #include <arpa/inet.h> #include <strings.h> #include <iostream> in_addr pton32(const char *str) { in_addr s_addr; try { if (1 == inet_pton(AF_INET, str, &s_addr)) return s_addr; else throw false; } catch (bool b) { std::cout << "convert failed." << std::endl; } bzero(&s_addr, sizeof(s_addr)); return s_addr; }
定義這個函數的目的在於一步到位快速轉換IP地址。
在給出的服務端代碼中,程序的流程邏輯依然是:socket →bind → listen → accept → read/write → close。
我個人的代碼規范是:盡量在需要用到變量時才定義該變量,保持變量的定義和使用聯系緊密。40~41行中的變量在即將被使用前進行定義。
這里需要關注的是第45~46行,getsockname()和getpeername()函數使用一個已連接的socket描述符分別獲取服務端和客戶端的IP地址和端口。並在50~58行中進行打印輸出。運行服務端和客戶端得到的結果在文章開頭的貼圖里給出。這是個相當奇怪的結果,getsockname()和getpeername()函數是對一個已連接socket描述符進行讀取,卻沒有正確獲得結果,利用搜索引擎搜索良久未得到結果,最終通過查看getsockname()函數的man手冊說明,看到一句說明才找到問題所在,說明如下:
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
getsockname() returns the current address to which the socket sockfd is bound,in the buffer pointed to by addr. The addrlen argument should be initialized to indicate the amount of space (in bytes) pointed to by addr. On return it contains the actual size of the socket address.
The returned address is truncated if the buffer provided is too small; in this case, addrlen will return a value greater than was supplied to the call.
通過手冊說明,可以看到addrlen參數所指的對象必須初始化,另外,如果初始提供的值太小,getsockname()函數在返回時,新寫入addrlen指向的對象的值將會大於所調用時提供的值。
根據此特性,我的服務端程序出現的問題可以如此進行推斷了:
由於我是在linux環境下使用的gcc編譯器,在編譯時,編譯器會將main函數中未初始化的局部變量全部初始化未0,因此在第一次調用getsockname()和getpeername()函數時,第三個參數所指的對象的值為0,getsockname()和getpeername()沒有對套接字地址結構進行寫入操作,另外,初始提供的第三個參數所指的對象的值太小,getsockname()和getpeername()函數在返回時,用一個更大的值覆蓋了原來的值。
接下來,在while循環中不斷建立銷毀40~41行代碼中定義的變量,但是程序所使用的棧內存位置不變,由於沒有初始化操作,因此內存殘留的值(第一次函數調用返回的更大值)在下一次循環中被讀取使用,造成了后續的結果正確輸出。
接下來驗證推斷,稍稍改動一下服務端程序,在41行后增加兩行可以輸出定義的變量的值和地址的語句
std::cout << "local_len: " << local_len << "\t\t\t" << "peer_len: " << peer_len << std::endl; std::cout << "local_len: " << &local_len << '\t' << "peer_len: " << &peer_len << std::endl;
重新編譯后運行,測試結果如下:
在結果中證明了上面的推測,每次循環都重復使用之前的內存,也就讀取了之前內存的值。
以上,再一次說明了,C++定義變量時一定要初始化。