动手学习TCP:4种定时器

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

TCP定时器

对于每一个TCP链接,TCP管理4个不一样的定时器,下面看看对4种定时器的简单介绍。编程

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

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

坚持定时器

TCP经过让接收方指明但愿从发送方接收的数据字节数(即窗口大小)来进行流量控制。服务器

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

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

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

实验代码

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

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

from socket import *
import time

HOST = "192.168.56.102"
PORT = 8081
ADDR = (HOST, PORT)

client = socket(AF_INET, SOCK_STREAM)
client.connect(ADDR)

while True:
    input = raw_input()
    
    if input:
        client.send(input*1000)
    else:
        client.close()
        break

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

import sys
from socket import *
import time

HOST = "192.168.56.102"
PORT = 8081
BUFSIZ = 100
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[0]) + ' Message ' + msg[1]
    sys.exit()

server.listen(1)
print 'Socket now listening'
conn, addr = server.accept()

while True:
    time.sleep(3)
    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相关的注册表设置,能够参考这个连接

运行效果

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

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

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

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

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

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

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

糊涂窗口综合症

基于窗口的流量控制方案,会致使一种"糊涂窗口综合症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: 75; since Linux 2.4)
       The number of seconds between TCP keep-alive probes.

tcp_keepalive_probes (integer; default: 9; 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: 7200; 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 7200 seconds (2
       hours).  An idle connection is terminated after approximately an additional 11 minutes (9 probes an interval
       of 75 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 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创建和关闭的消耗。

下面看看我访问一个网页后,经过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链接的可用性。

相关文章
相关标签/搜索