tcp协议中所有定时器。超时一词在软件领域用途非常广泛,是解决的很多问题利器。TCP设计精髓在于他自我管理的状态机,而要想状态机正常运行,超时必不可少,如connect flood攻击。
建立连接定时器
开始
TCP属于可靠连接,需要经历三次握手。

如上图所示,主要分为以下过程:
- 第一步:服务器准备好接受外来的连接。通常通过调用socket、bind和listen三个函数来完成,称之为被动打开(passive open)
- 第二步:客户端通过connect发起主动连接(active open)。导致客户端TCP发送一个SYN同步分节。一般SYN分节不携带数据,其所在的IP数据报只包含一个IP首部、一个TCP首部以及TCP选项(最为常见的是MSS分节,在这里并没有画出)
- 第三步:服务器确认客户的SYN,同时自己也发送一个SYN分节。服务器在单个分节中发送SYN和对客户端SYN的ACK确认。此时客户端已经处于ESTABLISHED状态
- 第四步:客户端必须确认服务端的SYN分节。确认成功,此时服务端也进入ESTABLISHED状态。
下图为正常的tcp三次握手抓包图
其中No对应列表的序号.
环境查看
结果分析
在这里有可能会出现两种情况超时
- 第一种:当TCP客户端发出请求没有收到SYN分节响应时,则会返回ETIMEOUT。即,调用connect函数时,内核会发送一个SYN分节,若无响应则等待6s再发送一个,若仍无响应则等待24s再发送一个。若总共等待75s(4.4BSD规定75s)后仍未收到响应则返回本错误。注意,不同系统对时间值的设置不相同。
- 第二种:当客户发出的SYN在中间的某个路由器上引发“destination unreachable”(目的地不可达)的ICMP错误时,则认为是一种软错误,不会终止。客户主机内核会保存该消息,并按照上述第一种情况来间接性继续发送SYN。在某个规定的时间(4.4BSD规定75s)后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
实验验证
- 系统:centos6.5
- 命令
1
2
3
- 实验结果
总时间为63s,这个时间还是难以忍受的,想想如果一个食堂因为你一个人等了1分多钟,估计你会被打shi
- 抓包: tcpdump -i lo ‘tcp[tcpflags] = tcp-syn’
在这里我们也看到了总共6次,重试了5次。而且也可以看到:1s-à2s-à4s-à8s-à16s方式来进行重试的。
解决方法
-
修改内核参数(强烈不推荐)
vim /etc/sysctl.conf
将net.ipv4.tcp_syn_retries = 5修改为 1,则可以将 connect 超时时间改为 3 秒,例如:# sysctl net.ipv4.tcp_syn_retries=1,之后使用命令sysctl -p /etc/sysctl.conf保存
. 应用层进行设置
- 使用alarm,指定超时时间,超时满后产生SIGALRM信号
- Select中阻塞等待I/O,此时套接字必须为非阻塞
- 使用较新的SO_RECVTIMEO和SO_SNDTIMEO
重传定时器(RTO)
开始
首先介绍一下滑动窗口。来自百度百科:滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。不同的滑动窗口协议窗口大小一般不同。发送方窗口内的序列号代表了那些已经被发送,但是还没有被确认的帧,或者是那些可以被发送的帧。下面举一个例子(假设发送窗口尺寸为2,接收窗口尺寸为1)。
在这里我们重点分析发送窗口,其原理是一个发送窗口,其中包括两部分内容:已发送但未确认和可以发送但还未发送的。
超时重传
-
超时重传原理
超时重传是TCP保持可靠性中不可缺少的一项。其原理是发送一个数据后就开启一个计时器,在一定时间内如果没有得到ACK分节(如上图Window Alreadly Sent 14bytes),则发送端重新发送数据报文,直到数据全部发送成功为止,此时会重置RTO。
当然需要注意的是,此份数据因为保存在内核,而且保存完成路由的信息,所以在重传时不会涉及到数据拷贝(应用进程到内核的双向复制),也不需要上文切换,所以只需要将数据从内核搬到网卡,重新发送即可。
- Linux下有两个重要的内核参数和TCP超时有关:
tcp_retries1:表示指定在底层IP接管之前(也就是不需要网络层参与)TCP最少执行的重传次数,默认值为3;
tcp_retries1:表示连接放弃前TCP最多执行的重传次数,默认值为15(
一般对应13-30分钟)。
坚持定时器(persist timer)
开始
TCP提供流量控制机制,其原理:TCP总是告知对端在任何时刻他一次能够从对端接受多少字节的数据,这也被为通告窗口(Advertised Window)。在任何时刻,该窗口指出接受缓冲区当前可用的空间量(可以使用
netstat -lpn查看),从而确保发送端发送的数据不会使接收端缓冲区溢出。
- 当前接受和发送缓冲区大小
- 系统默认和最大接受缓冲区大小
实验与分析
- 该窗口时刻动态变化的,当接受到来自发送端的数据时,窗口大小就减小;而接受端读取数据后,窗口会变大。通过窗口大小可能是0,此时会通知发送端不要在发送数据,那么问题来了,客户端什么时候再继续发送数据呢?一直傻等着吗?答案显然是:No。
- 当然, TCP规范中约定,即使对端为0窗口模式,但依旧接受以下几种报文 :零窗口探测报文、确认报文和携带紧急数据的报文段。因此,为解决这种死锁问题,此时需要一个坚持定时器。TCP为每个连接设置一个坚持定时器,只要TCP连接一方收到对方的0窗口通知,就启用该定时器。若定时器到期,就发送一个零窗口探测报文(仅携带一个字节的数据),对方在ACK这个探测报文时设置当前的窗口大小。
- 实验
- 服务端代码(centos6.7)
-
- 客户端代码
-
- 结果
解决方法
可以通过
SO_RCVBUF套接字来修改该TCP选项。(不建议)
延迟定时器(Delayed ACK)
开始
延迟应答定时器和ACK延滞(Delayed ACK Algorithm)算法息息相关。
ACK延滞算法思想是:TCP在接收到数据后并不立即发送ACK,而是等待一小段时间(典型值为50-200ms,即延迟定时器),然后才发送ACK。为提高网络传输效率,避免某一个时刻网络充斥着大量的ACK小报文,TCP期待在这一小段时间内自身有数据发送回对端,捎带ACK,从而节省一个TCP分节。延迟应答定时器就是为防止某一时刻没有数据,导致ACK一直等待不发送,造成死锁。
保活定时器(Keep Alive)
开始
- 首先需要明确一点的是TCP中的Keep Alive选项目的旨在检测对端主机是否崩溃或不可达(譬如电源故障等),不同于HTTP的Keep Alive,HTTP1.1引入的目的是复用连接,也就是所谓的长连接,降低频繁创建连接(即短连接)带来的性能问题。
参数分析
- 在Linux系统中,SO_KEEPALIVE选项涉及到三个相关参数,可以通过man tcp命令来查看。默认是7200s,即2小时
TCP_KEEPDILE:设置连接上如果没有数据发送的话,多久后发送keepalive探测分组,单位是秒
TCP_KEEPINTVL :前后两次探测之间的时间间隔,单位是秒
TCP_KEEPCNT :关闭一个非活跃连接之前的最大重试次数
实验与分析
客户端代码
服务端代码见上
- ==存在缺陷:==
- ==2小时时间太长==
- ==可能网络抖动,导致之前的分节丢失,实际上该连接依旧有效,导致对端误以为终止,进而结束一个有效连接。==
- ==针对这个问题,目前解决方案两种:==
- ==第一种:修改内核参数,但是必须要注意大多数内核是基于整个内核维护的这些时间参数的,而不是基于每个套接字,因此如果把无活动周期从2小时修改为自定义的值,将会影响到该主机上所有开启本选项的套接字(默认不开启该选项)。==
- 第二种:避免使用该选项,应用层维护:
- 心跳包(hearbeat)
- 乒乓包(pingpong):和心跳包类似,除了携带标志位外,还可以携带少量数据==
- 使用TCP的紧急数据位(URG)携带心跳包
FIN_WAIT_2定时器
开始
FIN_WAIT_2定时器发生在TCP四次挥手过程,首先来看一下TCP四次挥手。
:首先终止一端发送完FIN分节点,等待对端回复ACK时的状态。该状态不应该过多,而且停留的时间很短。
FIN-WAIT-2状态:收到对端的ACK确认后,在FIN-WAIT-1状态直接转至FIN-WAIT-2状态,此时等待对端的FIN包。有时ACK和FIN一起,此时会跳过该状态
参数分析
- 对于状态的查看可以使用命令:==netstat -nat|awk ‘{print awk $NF}’|sort|uniq -c|sort -n==来进行排序查看
- 如果对端一直不发送FIN分节,FIN-WAIT-2状态将可能会一直存在。但实际上内核参数 tcp_fin_timeout 会控制超时时间。本机(cento6.5)默认值为==60s==
- 而且如果不是为了在半关闭状态下继续接受数据,连接长时间处于FIN-WAIT-2状态并无益处。其中若客户端执行半关闭后,未等服务器关闭连接就强行退出。此时该连接将由内核接管,被称之为 孤儿连接 。Linux为了防止孤儿连接长时间停留在内核中,将由两个内核参数来控制。
- tcp_max_orphan:表示最大孤儿连接数
- tcp_fin_timeout:表示存活时间
- tcp_orphan_retries:表示重试次数
TIME_WAIT定时器
开始
在tcp有关的网络编程中,最难理解且最容易碰到的就是TIME_WAIT状态了。由于TIME_WAIT时长为2MSL(最长分节生命周期),因此也被称为2MSL超时。
任何TCP实现都必须为MSL选择一个值。RFC1122建议为2m,但源自伯克利实现的系统,传统上改用30s。综合起来,TIME_WAIT状态将持续在1m到4m。MSL是任何IP数据报能够在因特网存活的最长时间,尽管IP报文含有一个8位、称为跳限的字段,最大值为255,但仍旧假设具有最大跳限的分组在网络上存在的时间不可能超过MSL秒。
分析
- 可靠的实现全双工连接的终止
结合上图,假设客户端(客户端为主动断开一方)丢失了服务端发来的FIN,服务器必然会重新发送该分节,客户端必须要维护状态信息,以允许他重新发送最终的那个ACK。要是客户端不维护状态信息,他将会响应一个RST,该分节被服务器解释成一个错误。然而,此时可能还有数据流在传输。因此如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流。那么他必须正确处理连接终止序列4个分节中任何一个分节丢失情况。
- 允许老的重复分节在网络中消失
因为每个数据包在网络上最长生命周期为MSL,2MSL可以确任何一个数据包都将消失。这样可以防止来自某个连接的老的重复分组在该连接已终止后再现。因为端口、IP资源都会回收,下次在复用。想想,如果你结束了一次支付宝访问,紧接着我又用了你刚才的IP和端口号去请求同一个网站,原先来不及发送的消息结果被我接受了,我有可能就得到你的密码。
解决方法
- 使用SO_REUSEADDR和SO_REUSRPORT
- SO_REUSRADDR选项而言:
- 在所有TCP服务器程序中,调用bind之前设置SO_REUSRADDR套接字选项
- 当编写一个可同一时刻在同一主机运行多次的多播应用程序时,设置SO_REUSRADDR套接字选项,并将锁参加的多播组的地址作为本地IP地址捆绑。( 这个可以参照nginx的设计 )
- SO_REUSRPORT选项而言:
- 主要是针对SO_REUSRADDR绑定通配地址和端口后,不能再绑定更为详细的IP和端口
-
使用SO_LINGER选项
本选项指定close函数对面向连接的协议如何操作。默认操作是close立即返回,但是如果有数据残留在套接字发送缓冲区,系统将试着把这些数据发送给对端,然后在继续进行正常的FIN序列。
然而SO_LINGER选项可以改变这个默认设置。其数据结构如下:
struct
引用
《TCP/IP详解》
《Unix网络编程:卷2》
《计算机网络自顶向下方法》