动手学习TCP:4种定时器

时间:2023-12-15 21:18:20

上一篇中介绍了TCP数据传输中涉及的一些基本知识点。本文让我们看看TCP中的4种定时器。

TCP定时器

对于每个TCP连接,TCP管理4个不同的定时器,下面看看对4种定时器的简单介绍。

  • 重传定时器使用于当希望收到另一端的确认。
    • 该定时器是用来决定超时和重传的。
    • 由于网络环境的易变性,该定时器时间长度肯定不是固定值;该定时器时间长度的设置依据是RTT(Round Trip Time),根据网络环境的变化,TCP会根据这些变化并相应地改变超时时间。
  • 坚持定时器(persist)使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
  • 保活定时器(keepalive)可检测到一个空闲连接的另一端何时崩溃或重启。
  • 2MSL定时器测量一个连接处于TIME_WAIT状态的时间。

下面就介绍一下坚持定时器和保活定时器。

坚持定时器

TCP通过让接收方指明希望从发送方接收的数据字节数(即窗口大小)来进行流量控制。

如果窗口大小为 0会发生什么情况呢?这将有效地阻止发送方传送数据,直到窗口变为非0为止。

动手学习TCP:4种定时器

但是,由于TCP不对ACK报文段进行确认(TCP只确认那些包含有数据的ACK报文段),如果上图中通知发送方窗口大于0的[ACK]丢失了,则双方就有可能因为等待对方而使连接死锁。接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。

为防止这种死锁情况的发生,发送方使用一个坚持定时器 (persist timer)来周期性地向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查(window probe)。

实验代码

下面通过Python socket实现一个快的发送端和慢的接收端,然后通过Wireshark抓包来看看窗口更新通知和窗口探查。

客户端代码如下,用户输入字符,客户端将用户输入重复1000次然后发送给服务端,通过这种简单的重复来模拟一个快的发送端:

from socket import *
import time HOST = "192.168.56.102"
PORT =
ADDR = (HOST, PORT) client = socket(AF_INET, SOCK_STREAM)
client.connect(ADDR) while True:
input = raw_input() if input:
client.send(input*)
else:
client.close()
break

对于服务端,通过制定一个小的接收BUFFER,以及一个延时来模拟一个慢的接收端:

import sys
from socket import *
import time HOST = "192.168.56.102"
PORT =
BUFSIZ =
ADDR = (HOST, PORT) server = socket(AF_INET, SOCK_STREAM)
print "Socket created"
try:
server.bind(ADDR)
except error, msg:
print 'Bind failed. Error Code : ' + str(msg[]) + ' Message ' + msg[]
sys.exit() server.listen()
print 'Socket now listening'
conn, addr = server.accept() while True:
time.sleep()
try:
data = conn.recv(BUFSIZ)
if data:
print data
else:
conn.close()
break
except Exception, e:
print e
break

在开始运行代码之前还需要进行一些设置,默认情况下接收端的window size很大,实验中很难耗尽。

所以,为了看到实验效果,需要对系统进行一些设置。打开虚拟机中的注册表设置"regedit",然后找到选项"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters",设置"TcpWindowSize"为4096Bytes。

注意,实验结束后,一定要恢复"TcpWindowSize"的原始设置,不然可能会影响正常的网络访问。

关于更多TCP相关的注册表设置,可以参考这个链接

动手学习TCP:4种定时器

运行效果

下面运行代码,分别输入两个字符"a"和"b",通过Wireshark可以看到,在进行连接确认的时候,接收端已经给出了我们跟新后的可用窗口4096Bytes。

经过第一轮发送后,接收方的window size减少了1000;当两个数据包都处理完成后,window size又恢复到了4096。

动手学习TCP:4种定时器

第二轮测试中,发送端发送"1234567890"十个字符,从接收端的最后一个[ACK]包可以看到,最后接收端window size为1393,此次传输到此结束。

过了一段时间,当慢接收端处理完数据之后,接收端会发送窗口更新,通知发送端可以窗口为4096Bytes。

动手学习TCP:4种定时器

第三轮测试中,发送端发送更多的字符"1234567890987654321",这次接收端的可用窗口就被耗尽了,然后接收端发送一个[TCP ZeroWindow]的通知;这时,发送端停止发送,然后通过发送窗口探查。

当接收端有可用窗口的时候,接收端会发送窗口更新,数据传输继续。

注意,[TCP ZeroWindowProbe]和[TCP ZeroWindowProbeAck]的Seq和Ack号。

动手学习TCP:4种定时器

糊涂窗口综合症

基于窗口的流量控制方案,会导致一种"糊涂窗口综合症SWS(Silly Window Syndrome)"的状况。

当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小。 极端情况下,有效载荷可能只有1个字节;而传输开销有40字节(20字节的IP头+20字节的TCP头),加上物理帧头后,有效的数据传输比例就更小了,这就浪费了网络带宽,表现为糊涂窗口综合症。

糊涂窗口综合症可能由接收端或者发送端引起,不同的起因需要不同的解决方案,更多内容可以参考此处

保活定时器

跟据TCP协议,当发送端和接收端都不主动释放一个TCP连接的时候,该连接将一直保持。即使一端出现了故障,由于另一端没有收到任何通知,TCP连接也会一直保持,这样就会造成TCP连接资源的浪费。

TCP keepalive

为了解决这个问题,大多数的实现中都是使服务器设置保活计时器。

保活计时器通常设置为2小时。若服务器过了2小时还没有收到客户的信息,它就发送探测报文段。若发送了10个探测报文段(每一个相隔75秒)还没有响应,就假定客户出了故障,因而就终止该连接。

在Linux系统中,有三个跟TCP keepalive相关的参数:

tcp_keepalive_intvl (integer; default: ; since Linux 2.4)
The number of seconds between TCP keep-alive probes. tcp_keepalive_probes (integer; default: ; since Linux 2.2)
The maximum number of TCP keep-alive probes to send before giving up and killing the connection if no
response is obtained from the other end. tcp_keepalive_time (integer; default: ; since Linux 2.2)
The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes. Keep-
alives are sent only when the SO_KEEPALIVE socket option is enabled. The default value is seconds (
hours). An idle connection is terminated after approximately an additional minutes ( probes an interval
of seconds apart) when keep-alive is enabled.

在Socket编程中,可以通过设置"TCP_KEEPCNT","TCP_KEEPIDLE"和"TCP_KEEPINTVL"选项来更改上述的三个系统参数:

from socket import *
import time HOST = "192.168.56.102"
PORT = 8081
ADDR = (HOST, PORT) client = socket(AF_INET, SOCK_STREAM) #TCP_KEEPCNT overwrite tcp_keepalive_probes,默认9(次)
#TCP_KEEPIDLE overwrite tcp_keepalive_time,默认7200(秒)
#TCP_KEEPINTVL overwrite tcp_keepalive_intvl,默认75(秒)
client.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
client.setsockopt(SOL_TCP, TCP_KEEPCNT, 5)
client.setsockopt(SOL_TCP, TCP_KEEPINTVL, 5)
client.setsockopt(SOL_TCP, TCP_KEEPIDLE, 10)
client.connect(ADDR) while True:
input = raw_input() if input:
client.send(input*1000)
else:
client.close()
break

TCP keepalive 包

下面是一段网络上抓取的TCP keepalive包,接下来看看TCP keepalive包的内容。

动手学习TCP:4种定时器

  • 根据规范,TCP keepalive保活包不应该包含数据,但也可以包含1个无意义的字节,比如0x0。
  • TCP保活探测包Seq号是将前一个TCP包的Seq号减去1。

当然,也有人认为保活定时器不合理,给出了不使用保活定时器的理由:

  • 在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉
  • 耗费了不必要的带宽
  • 在按分组计费的情况下会在互联网上花掉更多的钱

HTTP Keep-Alive

在HTTP早期 ,每个HTTP请求都要求打开一个TCP连接,并且使用一次之后就断开这个TCP连接。

这种方式会带来一些问题,尤其是包含图片,JS,CSS的复杂网页,一个完整的页面需要很多个请求才能完成,如果每一个HTTP请求都需要新建并断开一个TCP,这样就会消耗很多服务器的TCP连接资源。

为了缓解这个问题,HTTP 1.1中出现了Keep-Alive这个特性,开启HTTP Keep-Alive之后,能复用已有的TCP链接,当前一个请求已经响应完毕,服务器端没有立即关闭TCP链接,而是等待一段时间接收浏览器端可能发送过来的第二个请求,开启Keep-Alive能节省的TCP建立和关闭的消耗。

动手学习TCP:4种定时器

下面看看我访问一个网页后,通过Wireshark抓取的数据包。

HTTP/1.1之后默认开启Keep-Alive, 在HTTP的头域中增加Connection选项。当设置为"Connection:keep-alive"表示开启,设置为"Connection:close"表示关闭。

在上图中,服务器经过了大概2分钟的时间,然后发出关闭TCP连接的请求。

现在,基本所有的应用服务器都支持设置打开Keep-Alive,以及Keep-Alive timeout的设置。

总结

本文介绍了TCP中的4种定时器,并详细的介绍了坚持定时器和保活定时器。

在保活定时器的介绍中,对比介绍了HTTP的Keep-Alive特性。HTTP协议的Keep-Alive意图在于连接复用;TCP的keepalive机制在于保活、心跳,检测连接错误,两者的作用完全不同。

因为TCP keepalive不能满足实时性的要求,很多应用程序会在应用层实现heart beat(心跳包)来确认TCP连接的可用性。