本文转载自微信公众号「 Linux开发那些事儿」,作者LinuxThings。转载本文请联系 Linux开发那些事儿公众号。
写过网络程序的同学,应该都知道 connect 函数,在 socket 开始读写操作之前,先要进行连接,也即 TCP 的三次握手 , 这个过程就是在 connect 函数中完成的, connect 函数本身是阻塞的,通过设置 socket 的选项及调用 select/poll 函数可以实现异步 connect 的功能
socket 默认是阻塞模式,处于阻塞模式时,调用 connect 函数之后, 会一直等待连接结果返回为止,要么成功,要么失败,connect 函数返回 0 时成功,返回 -1 失败
在局域网中,调用 connect 函数,基本上会立即返回结果,当服务器在国外时,connect 函数时会阻塞一段时间,大概几秒钟吧,具体的还要看当时的网络状况
Linux 下 connect 默认的超时时间大概在一分钟左右(不同的Linux版本略有差别),在实际的开发中,这个时间显得有点儿长了
对于服务器来说,需要为很多的客户端服务,要尽量减少阻塞,所以,一般都是采用 异步 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 表示连接成功,其他表示连接失败