如何实现异步 Connect

写过网络程序的同学,应该都知道 connect 函数,在 socket 开始读写操作之前,先要进行连接,也即 TCP 的三次握手 , 这个过程就是在 connect 函数中完成的, connect 函数本身是阻塞的,通过设置 socket 的选项及调用 select/poll 函数可以实现异步 connect 的功能。
首页 新闻资讯 行业资讯 如何实现异步 Connect

[[402493]]

本文转载自微信公众号「 Linux开发那些事儿」,作者LinuxThings。转载本文请联系 Linux开发那些事儿公众号。

写过网络程序的同学,应该都知道 connect 函数,在 socket 开始读写操作之前,先要进行连接,也即 TCP 的三次握手 , 这个过程就是在  connect 函数中完成的, connect 函数本身是阻塞的,通过设置 socket 的选项及调用 select/poll 函数可以实现异步 connect  的功能

socket 默认是阻塞模式,处于阻塞模式时,调用 connect 函数之后, 会一直等待连接结果返回为止,要么成功,要么失败,connect 函数返回  0 时成功,返回 -1 失败

在局域网中,调用 connect 函数,基本上会立即返回结果,当服务器在国外时,connect  函数时会阻塞一段时间,大概几秒钟吧,具体的还要看当时的网络状况

为什么要用异步 connect

Linux 下 connect 默认的超时时间大概在一分钟左右(不同的Linux版本略有差别),在实际的开发中,这个时间显得有点儿长了

对于服务器来说,需要为很多的客户端服务,要尽量减少阻塞,所以,一般都是采用 异步 connect 的技术

对于每一个编写网络程序的同学来说,异步connect 应该是必须掌握的基本功

异步connect 步骤

(1) 创建socket,调用 fcntl 函数将其设置为非阻塞

(2) 调用 connect 函数,返回 0 表示连接成功,返回 -1,需要检查错误码

如果错误码为 EINPROGRESS,表示正在建立连接中

如果错误码是 EINTR 表示,表示发生了系统中断,这时继续执行连接即可

如果是其他错误码,调用 close(fd) 函数关闭 socket, 连接失败

(3) 将 socket 加入 select/poll 的可写文件描述符集合中,并设置超时时间

(4) 判断 select/poll 函数的返回值

小于等于 0 表示失败

其他,表示 socket 可写,调用 getsockopt 函数 捕获 socket 的错误信息

具体的代码如下:

复制

/*     异步 connect 测试代码, test_connect.cpp */ #include <stdint.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/select.h> #include <poll.h> #include <sys/un.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <netdb.h> #include <errno.h> #include <stdarg.h> #include <poll.h> #include <limits.h> #include <iostream> using namespace std;  int32_t main(int32_t argc, char *argv[]) {     if(argc < 3)     {         std::cout << "argc < 3..." << std::endl;         return -1;     }     std::string strip = argv[1];     uint32_t port = atoi(argv[2]);     //创建 socket     int32_t fd = socket(AF_INET, SOCK_STREAM, 0);     if(-1 == fd)     {         std::cout << "create socket error:" << errno << std::endl;         return -1;     }     //将 socket 设置成非阻塞     int32_t flag = fcntl(fd, F_GETFL, 0);     flag |= O_NONBLOCK;     if(-1 == fcntl(fd, F_SETFL, flag))     {         std::cout << " set socket nonblock error:" << errno << std::endl;         close(fd);         return -1;     }     //服务器地址     struct sockaddr_in addr;     addr.sin_family = AF_INET;     addr.sin_port = htons(port);     addr.sin_addr.s_addr = inet_addr(strip.c_str());     //     for(; ;)     {         //连接服务器         int32_t ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr) );         if(-1 == ret)         {             int32_t err = errno;             if(EINTR == err)             {                 //connect被中断,继续重试                 //如果不处理 EINTR 错误的话,connect逻辑可以不用放到 for 循环中                 continue;             }             if(EINPROGRESS != err)             {                 std::cout << "connect err:" << errno << ", str:" << strerror(errno) <<  std::endl;                 goto exit;             }             //正在连接中             std::cout << "connecting..." << std::endl;             //处理结果             int32_t result = -1;     #if 1             //将 socket 加入到 poll 的可写集合中             struct pollfd wfd[1];             wfd[0].fd = fd;             wfd[0].events = POLLOUT;             //检测 socket 是否可写             result = poll(wfd, 1, 3000);     #elif 0             //设置超时时间             struct timeval tval;             tval.tv_sec = 3;             tval.tv_usec = 0;             //将 socket 加入到 select 的可写集合中             fd_set wfds;             FD_ZERO(&wfds);             FD_SET(fd,&wfds);             //检测 socket 是否可写             result = select(fd + 1, nullptr, &wfds, nullptr,&tval);     #endif             std::cout << "async connect result:" << result << std::endl;             // 失败             if(result <= 0 )             {                  std::cout << "async connect err:" << errno << ", str:" << strerror(errno) << std::endl;                 goto exit;             }             //检查socket 错误信息             int32_t temperr = 0;             socklen_t temperrlen = sizeof(temperr);             if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)&temperr, &temperrlen) )             {                 std::cout << "async connect...getsockopt err:" << errno << ", str:" << strerror(errno) <<  std::endl;                 goto exit;             }             if(0 != temperr)             {                 std::cout << "async connect...getsockopt temperr:" << temperr << ", str:" << strerror(temperr) << std::endl;                 goto exit;             }             //成功             std::cout << "async connect success..." << std::endl;             goto exit;         }         else         {              //连接成功             std::cout << "connect success..." << std::endl;             goto exit;                   }     } // end of  for(; ;) exit:     std::cout << "quit...." << std::endl;     close(fd);     return 0; }
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

  • 56.

  • 57.

  • 58.

  • 59.

  • 60.

  • 61.

  • 62.

  • 63.

  • 64.

  • 65.

  • 66.

  • 67.

  • 68.

  • 69.

  • 70.

  • 71.

  • 72.

  • 73.

  • 74.

  • 75.

  • 76.

  • 77.

  • 78.

  • 79.

  • 80.

  • 81.

  • 82.

  • 83.

  • 84.

  • 85.

  • 86.

  • 87.

  • 88.

  • 89.

  • 90.

  • 91.

  • 92.

  • 93.

  • 94.

  • 95.

  • 96.

  • 97.

  • 98.

  • 99.

  • 100.

  • 101.

  • 102.

  • 103.

  • 104.

  • 105.

  • 106.

  • 107.

  • 108.

  • 109.

  • 110.

  • 111.

  • 112.

  • 113.

  • 114.

  • 115.

  • 116.

  • 117.

  • 118.

  • 119.

  • 120.

  • 121.

  • 122.

  • 123.

  • 124.

  • 125.

  • 126.

  • 127.

  • 128.

  • 129.

  • 130.

  • 131.

  • 代码说明

如果不处理 EINTR 错误的话,connect 函数及后面的逻辑可以不用放到 for 循环中

检查 socket 是否可写,调用 select 或者 poll 函数都可以,上述代码中使用的是 poll 函数,将代码中的 #if 1 改成 #if 0  以及 #elif 0 改成 #elif 1 , 就是使用 select 函数检测 socket 是否可写了

测试

在另一台机器上执行 nc -l -v -k 192.168.70.20 5000 命令,启动一个服务器程序

在当前机器上执行 g++ -g -Wall -std=c++11 -o test_connect test_connect.cpp 进行编译

执行 ./test_connect 192.168.70.20 5000, 结果如下图

此时,服务器程序显示如下:

通过 test_connect 程序端的截图可以看出,调用 connect 函数之后,返回了 EINPROGRESS 错误码,然后调用  select/poll 函数返回 1, 表示 socket 可写,紧接着调用 getsockopt 函数检查 socket  错误信息,通过打印的信息知道,socket 无错误信息,即 连接成功

我们在服务器机器上按 CTRL + C 停止服务器程序,然后关闭 test_connect 程序,再次执行 ./test_connect  192.168.70.20 5000 ,结果如下图:

从上图可以看出,即使服务器程序已经退出了,调用 select/poll 之后还是返回 socket 可写,当继续调用 getsockopt 函数检查  socket 错误码,此时错误码是 111, 表示连接被拒绝,也即连接失败

这里要注意一个很重要的点, 在 Linux 上,即使 socket 没有连接成功,调用 select/poll 时,仍然返回 socket 是可写的,所以  除了调用 select/poll 检查 socket 可写之外,还需要调用 getsockopt 函数检查 socket 的错误码,错误码为 0  表示连接成功,其他表示连接失败

 

14    2021-05-31 07:30:47    Connect socket 函数