如何实现异步 Connect

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

具体的代码如下:

  1. /* 
  2.     异步 connect 测试代码, test_connect.cpp 
  3. */ 
  4. #include <stdint.h> 
  5. #include <sys/types.h> 
  6. #include <sys/socket.h> 
  7. #include <sys/select.h> 
  8. #include <poll.h> 
  9. #include <sys/un.h> 
  10. #include <netinet/in.h> 
  11. #include <netinet/tcp.h> 
  12. #include <arpa/inet.h> 
  13. #include <unistd.h> 
  14. #include <fcntl.h> 
  15. #include <string.h> 
  16. #include <netdb.h> 
  17. #include <errno.h> 
  18. #include <stdarg.h> 
  19. #include <poll.h> 
  20. #include <limits.h> 
  21. #include <iostream> 
  22. using namespace std; 
  23.  
  24. int32_t main(int32_t argc, char *argv[]) 
  25.     if(argc < 3) 
  26.     { 
  27.         std::cout << "argc < 3..." << std::endl; 
  28.         return -1; 
  29.     } 
  30.     std::string strip = argv[1]; 
  31.     uint32_t port = atoi(argv[2]); 
  32.     //创建 socket 
  33.     int32_t fd = socket(AF_INET, SOCK_STREAM, 0); 
  34.     if(-1 == fd) 
  35.     { 
  36.         std::cout << "create socket error:" << errno << std::endl; 
  37.         return -1; 
  38.     } 
  39.     //将 socket 设置成非阻塞 
  40.     int32_t flag = fcntl(fd, F_GETFL, 0); 
  41.     flag |= O_NONBLOCK; 
  42.     if(-1 == fcntl(fd, F_SETFL, flag)) 
  43.     { 
  44.         std::cout << " set socket nonblock error:" << errno << std::endl; 
  45.         close(fd); 
  46.         return -1; 
  47.     } 
  48.     //服务器地址 
  49.     struct sockaddr_in addr; 
  50.     addr.sin_family = AF_INET; 
  51.     addr.sin_port = htons(port); 
  52.     addr.sin_addr.s_addr = inet_addr(strip.c_str()); 
  53.     // 
  54.     for(; ;) 
  55.     { 
  56.         //连接服务器 
  57.         int32_t ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr) ); 
  58.         if(-1 == ret) 
  59.         { 
  60.             int32_t err = errno; 
  61.             if(EINTR == err) 
  62.             { 
  63.                 //connect被中断,继续重试 
  64.                 //如果不处理 EINTR 错误的话,connect逻辑可以不用放到 for 循环中 
  65.                 continue
  66.             } 
  67.             if(EINPROGRESS != err) 
  68.             { 
  69.                 std::cout << "connect err:" << errno << ", str:" << strerror(errno) <<  std::endl; 
  70.                 goto exit; 
  71.             } 
  72.             //正在连接中 
  73.             std::cout << "connecting..." << std::endl; 
  74.             //处理结果 
  75.             int32_t result = -1; 
  76.     #if 1 
  77.             //将 socket 加入到 poll 的可写集合中 
  78.             struct pollfd wfd[1]; 
  79.             wfd[0].fd = fd; 
  80.             wfd[0].events = POLLOUT; 
  81.             //检测 socket 是否可写 
  82.             result = poll(wfd, 1, 3000); 
  83.     #elif 0 
  84.             //设置超时时间 
  85.             struct timeval tval; 
  86.             tval.tv_sec = 3; 
  87.             tval.tv_usec = 0; 
  88.             //将 socket 加入到 select 的可写集合中 
  89.             fd_set wfds; 
  90.             FD_ZERO(&wfds); 
  91.             FD_SET(fd,&wfds); 
  92.             //检测 socket 是否可写 
  93.             result = select(fd + 1, nullptr, &wfds, nullptr,&tval); 
  94.     #endif 
  95.             std::cout << "async connect result:" << result << std::endl; 
  96.             // 失败 
  97.             if(result <= 0 ) 
  98.             {  
  99.                 std::cout << "async connect err:" << errno << ", str:" << strerror(errno) << std::endl; 
  100.                 goto exit; 
  101.             } 
  102.             //检查socket 错误信息 
  103.             int32_t temperr = 0; 
  104.             socklen_t temperrlen = sizeof(temperr); 
  105.             if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)&temperr, &temperrlen) ) 
  106.             { 
  107.                 std::cout << "async connect...getsockopt err:" << errno << ", str:" << strerror(errno) <<  std::endl; 
  108.                 goto exit; 
  109.             } 
  110.             if(0 != temperr) 
  111.             { 
  112.                 std::cout << "async connect...getsockopt temperr:" << temperr << ", str:" << strerror(temperr) << std::endl; 
  113.                 goto exit; 
  114.             } 
  115.             //成功 
  116.             std::cout << "async connect success..." << std::endl; 
  117.             goto exit; 
  118.         } 
  119.         else 
  120.         { 
  121.              //连接成功 
  122.             std::cout << "connect success..." << std::endl; 
  123.             goto exit;           
  124.         } 
  125.     } // end of  for(; ;) 
  126. exit: 
  127.     std::cout << "quit...." << std::endl; 
  128.     close(fd); 
  129.     return 0; 
  • 代码说明

如果不处理 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 表示连接成功,其他表示连接失败

 

责任编辑:武晓燕 来源: Linux开发那些事儿
相关推荐

2018-05-14 13:51:39

RDS Binlog架构Kafka集群

2022-06-22 08:16:29

异步非阻塞框架

2023-08-02 08:03:08

Python线程池

2013-05-21 13:33:02

Android游戏开发异步音乐播放

2012-04-20 10:05:16

WCF

2017-05-11 20:20:59

JavascriptPromiseWeb

2023-03-10 14:56:37

Linuxconnect系统

2024-03-13 14:35:33

Spring事件异步

2011-02-24 12:53:51

.NET异步传统

2013-06-27 11:16:27

Android异步加载

2023-08-30 08:43:42

asyncioaiohttp

2021-08-16 15:49:31

开发框架单线程异步

2015-09-16 15:11:58

C#异步编程

2009-11-09 10:50:30

WCF异步调用

2009-08-21 09:20:44

C#异步套接字

2009-08-21 10:13:02

C#异步初步

2021-02-09 09:51:58

异步传递数据

2012-12-28 14:32:34

Android开发Handler异步处理

2021-11-01 22:36:04

JavaScript

2024-02-01 08:42:55

点赞
收藏

51CTO技术栈公众号