写过网络程序的同学,应该都知道 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;
}
  • 代码说明

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

如何实现异步 connect的更多相关文章

  1. ICOPclient版本号,异步connect

    之前在网上看到一个服务端的ICOP模块,比較小巧,感觉还不错,后来在工作中,需要开发一个挂号的程序,监视大量server执行情况,初期连接数大概六七百,我就把这个ICOP模块改造成了一个client版 ...

  2. Netty源码分析之客户端启动过程

    一.先来看一下客户端示例代码. public class NettyClientTest { public void connect(int port, String host) throws Exc ...

  3. 【深入浅出Linux网络编程】 “实践 -- TCP & UDP”

    通过上一篇博客的学习,你应该对基于epoll的事件触发机制有所掌握,并且通过阅读sio.c/sio.h应该也学会了如何封装epoll以及如何通过设计令epoll更加实用(用户回调,用户参数). 简单回 ...

  4. [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching)

     [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching) http://www.360doc.com/content/12/0428/17/6187784 ...

  5. Joynet示例:知乎爬虫(搜索关键字相关回答,并下载其中的---图(mei)片(nv))

    先贴爬虫程序下载地址:http://pan.baidu.com/s/1c2lxl1e 下载解压后 可直接运行 其中的 run.bat:或者你手动打开命令行执行:Joynet examples\Spid ...

  6. boost.ASIO-可能是下一代C++标准的网络库

    曾几何时,Boost中有一个Socket库,但后来没有了下文,C++社区一直在翘首盼望一个标准网络库的出现,网络上开源的网络库也有不少,例如Apache Portable Runtime就是比较著名的 ...

  7. 使用boost io_service时,需要注意的东西

    boost::asio 在创建io_service时,可以指定线程数,如果没有指定,默认是一个线程,也就是io_service run的那个线程,如果没有任务运行,该线程会退出. 如果在创建的时候指定 ...

  8. 个人对于epollhup的理解

    1.原因 由于最近对于异步connect函数的测试,发现提前将一个套接字加入epoll监听队列会不断爆出epollhup事件 2.示例 ........  iEpoll = epoll_create( ...

  9. P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解

    1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...

随机推荐

  1. Redis之品鉴之旅(七)

    分布式锁 1)阻塞锁: 尝试在redis中创建一个字符串结构缓存,方法传入的key,value为锁的过期时间timeout的时间戳. 若redis中没有这个key,则创建成功(即抢到锁),然后立即返回 ...

  2. VirtualBox VM 空间瘦身记(vmdk)

    本文地址:https://www.ebpf.top/post/shrink_vbox_vmdk_size 在使用 VirtualBox( VMDK 模式)管理虚拟机的时候,我们经常会遇到一些编译安装场 ...

  3. Python读取网页表格数据

    学会了从网格爬取数据,就可以告别从网站一页一页复制表格数据的时代了. 说个亲身经历的事: 以前我的本科毕业论文是关于"燃放烟花爆竹和空气质量"之间关系的,就要从环保局官网查资料. ...

  4. F1西班牙大奖赛-加泰罗尼亚赛道地图及简介

    背景 银石双赛结束,第二轮三连赛的最后一场将转战西班牙,第50届F1西班牙大奖赛将于本周末(正赛2020-08-15)在加泰罗尼亚赛道上演. 作为近年来F1承办季前测试的赛道,所有人都对这里再熟悉不过 ...

  5. 【UE4 C++】学习笔记汇总

    UE4 概念知识 基础概念--文件结构.类型.反射.编译.接口.垃圾回收.序列化[导图] GamePlay架构[导图] 类的继承层级关系[导图] 反射机制 垃圾回收机制/算法 序列化 Actor 的生 ...

  6. HTML中的emment

    emment的优点和使用方法: 优点:Emment是前端开发者必备的工具,使用它可以大大提高前端开发效率. 使用方法:Emment的使用方法非常简单,直接在编辑器上输入HTML和CSS代码的缩写,然后 ...

  7. [技术博客] BeautifulSoup4分析网页

    [技术博客] BeautifulSoup4分析网页 使用BeautifulSoup4进行网页文本分析 前言 进行网络爬虫时我们需要从网页源代码中提取自己所需要的信息,分析整理后存入数据库中. 在pyt ...

  8. [对对子队]会议记录5.17(Scrum Meeting4)

    今天已完成的工作 何瑞 ​ 工作内容:实现选择组件,搭建第7关 ​ 相关issue:实现选择组件 ​ 相关签入:feature:完成Lv7的UI搭建 吴昭邦 ​ 工作内容:实现选择组件,搭建第7关 ​ ...

  9. 2021.7.29考试总结[NOIP模拟27]

    T1 牛半仙的妹子图 做法挺多的,可以最小生成树或者最短路,复杂度O(cq),c是颜色数. 我考场上想到了原来做过的一道题影子,就用了并查集,把边权排序后一个个插入,记录权值的前缀和,复杂度mlogm ...

  10. numpy读取本地数据和索引

    1.numpy读取数据 np.loadtxt(fname,dtype=np.float,delimiter=None,skiprows=0,usecols=None,unpack=False) 做一个 ...