问题场景:
linux客户端和服务器进行TCP连接,配置错误的TCP服务器和端口,等待连接操作返回需要2分钟左右,这个时间就有点久了,考虑服务器正常运行的情况建立连接花不了几秒,比如顶多等待15秒,如果连接还没成功,就认为和服务器连接失败,直接返回,而不用等待2分钟后再返回。
实际测试过程中发现有如下几种情况(Ubuntu系统):
(说明:只测试连接,所以可以直接使用telnet ip port方式测试连接效果,如果需要计算从连接到执行的大概时间,可以通过如下方式:date; telnet ip port; date)
1、如果是同一局域网明确存在的主机地址,但该主机地址不存在所要访问的服务监听端口,客户端直接收到RST报文结束。

2、如果是同一局域网,但不存在目标主机,抓包显示很快发送完6个ARP查询,无结果返回No route to host。

arp尝试发送间隔时间这个搜索无果…
3、对于是非同一局域网的目标主机访问,无论是目标主机不存在,还是目标主机存在,但端口不存在,其返回结果都是一样的,都是第一次失败后再尝试连续发送6次SYN包无果后超时返回

上面重传SYN包的次数由如下配置决定
cat /proc/sys/net/ipv4/tcp_syn_retries
6
该字段的内核文档说明如下
Number of times initial SYNs for an active TCP connection attempt will be retransmitted. Should not be higher than 127. Default value is 6, which corresponds to 63seconds till the last retransmission with the current initial RTO of 1second. With this the final timeout for an active TCP connection attempt will happen after 127seconds.
这里的说明中最后一个重传包在63秒,跟上面抓包显示一致,而后说最后超时在127秒发生,也就是connect接口实际返回时已经过去了的超时时间,说明在最后63秒发送了最后一次syn包之后,系统在等待了64秒(从图中看TCP握手SYN包超时重试按照2的幂来计算的)之后无收到响应包才返回。
如果将该配置修改成2次进行测试
echo 2 > /proc/sys/net/ipv4/tcp_syn_retries
修改之后抓包显示,就只有2次重传了

虽然这样修改重传次数少了,间接等待的时间少了,不过在正常工作环境一般不会随意修改这个值。
可以通过两种编程方式来修改默认连接超时等待时间
一、通过setsockopt来设置套接字SO_SNDTIMEO属性,部分代码如下:
timeout.tv_sec = 20;
timeout.tv_usec = 0;
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
if (-1 == ret)
{
printf("[FILE:%s] [FUNC:%s] [Line:%d] \n", __FILE__, __func__, __LINE__ );
return -1;
}
ret = connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (-1 == ret)
{
if (errno == EINPROGRESS)
{
printf("[FILE:%s] [FUNC:%s] [Line:%d] connect timeout \n", __FILE__, __func__, __LINE__ );
}
return ret;
}
使用上述代码测试后,针对非子网不可访问目标主机进行通信,就不会等待默认2分钟之久了,超时20S就会返回。
该方式对于发送数据接口一样适用,不过对于超时返回的情况下,errno返回值会不一样,像connect则是置为EINPROGRESS,发送操作则是置为EAGIN或EWOULDBLOCK。
二、将套接字设置成非阻塞,由select接口来设置超时等待时间,部分代码如下:
block_flag = 1;
if (ioctl(sockfd, FIONBIO, &block_flag) != 0)
{
printf("[FILE:%s] [FUNC:%s] [Line:%d] ioctl failed. \n", __FILE__, __func__, __LINE__ );
return -1;
}
ret = connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (-1 == ret)
{
if (errno == EINPROGRESS)
{
FD_ZERO(&set);
FD_SET(sockfd, &set);
timeout.tv_sec = 20;
timeout.tv_usec = 0;
if (select(sockfd + 1, NULL, &set, NULL, &timeout) > 0)
{
(void)getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &err, &len);
if (0 == err)
{
ret = 0;
}
}
else
{
printf("[FILE:%s] [FUNC:%s] [Line:%d] timeout \n", __FILE__, __func__, __LINE__ );
}
}
}
block_flag = 0;
if (ioctl(sockfd, FIONBIO, &block_flag) != 0)
{
printf("[FILE:%s] [FUNC:%s] [Line:%d] \n", __FILE__, __func__, __LINE__ );
}
connect接口会立即返回EINPROGRESS错误,不过底层会继续进行TCP三次握手连接,应用需要使用select接口来判断连接成功还是失败,同样可以通过select的第三个参数来设置连接超时等待时间。
这里说到非阻塞,有一点需要理解,就是原本套接字是阻塞的,如果connect阻塞住了,CPU就会挂起该线程,只能去处理其他线程;如果设置成非阻塞,但其后select只是处理这一路连接,则仍旧是阻塞等待连接成功或失败为止,和前面的唯一区别是可设置等待时间。
一般设置非阻塞主要用于下面三种场景:
1、设置连接超时,就如前面所说的
2、多路复用,比如要建立多个连接,不用等这一路连接成功,才能建立下一个连接,这里注意是一个线程建立多个连接
3、在连接过程中处理其他事务
设置超时操作还好,这里的主要问题是如何判断连接是否建立成功了呢?这个通过查看《UNIX网络编程》了解,需要考虑两种情况,
1、目标主机是本机地址,则可能connect后已经建立连接,这种情况可以直接返回建立连接后的套接字
2、通过select来判断是否连接建立成功,如果连接建立成功,则套接字可写,不过书中有说道如果发生错误,则套接字可读可写,所以不能单纯的通过可写来判断,那么只能通过判断是否有发生错误来判断连接是否建立成功,如代码中通过getsockopt接口来获取错误信息。
需要注意连接成功后,需要将套接字重新设置为阻塞模式。前面讲的第一种方式其实也需要有类似的操作,否则后面的发送操作也会沿用开始设定的超时机制,除非明确就是要这样的。
1、
编码过程中一开始主机地址和端口使用的变量名分别是server和port,不过看man手册示例代码的时候,基本是命名为host和server,可见对于server我的理解是有偏差的。
想想一个功能一般称为服务,比如FTP服务,对应的端口就是服务端口,就如linux下的服务端口文件/etc/services。
2、
connect接口如果返回出错,想要重新连接,需要使用新的套接字,不能使用原有的套接字继续连接。
欢迎关注我的公众号:一周思进