• 关于getsockname()/getpeername()函数第一次被调用得到0.0.0.0结果的说明


    最近阅读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] = {''};
        while ((read(sockfd, str, 127)) > 0)
        {
            std::cout << str << ' ';
        }
    
        std::cout << std::endl;
    
        close(sockfd);
    
        return 0;
    }
    View Code

    服务端:

     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] = {''};
    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] = {''}, buff_peer[64]  = {''};
    49 
    50         if (inet_ntop(AF_INET, (void *)&local.sin_addr, buff_local, 63))
    51         {
    52             std::cout << "local ip: "        << buff_local
    53                       << "		local port: "  << ntohs(local.sin_port);
    54         }
    55 
    56         if (inet_ntop(AF_INET, (void *)&peer.sin_addr, buff_peer, 63))
    57         {
    58             std::cout << "
    peer  ip: "      << buff_peer
    59                       << "		peer  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;
    }
    View Code

    定义这个函数的目的在于一步到位快速转换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  << "			" << "peer_len: "  << peer_len  << std::endl;
    std::cout << "local_len: "  << &local_len << '	'    << "peer_len: "   << &peer_len << std::endl;

    重新编译后运行,测试结果如下:

    在结果中证明了上面的推测,每次循环都重复使用之前的内存,也就读取了之前内存的值。

    以上,再一次说明了,C++定义变量时一定要初始化。

  • 相关阅读:
    关于两次fork
    阻塞非阻塞与同步异步的区别
    函数调用堆栈
    数组
    64位操作系统与32位操作系统数据类型的字节数
    Redis 键(key)
    Redis 命令
    Redis 安装
    Redis 配置
    MongoDB 安装
  • 原文地址:https://www.cnblogs.com/pluse/p/6545514.html
Copyright © 2020-2023  润新知