解读tcp协议

简介

字面上来讲,可能有人会认为tcp/ip是指tcp与ip两种协议。然而很多情况下,它只是利用ip进行通信所必须用到的协议群的统称。具体来说,ip或icmp、tcp、或udp、telnet或ftp一级http等都属于tcp/ip的协议。tcp/ip一词泛指这些协议、因此,有时也称tcp/ip为网际协议族

互联网进行通信时,需要相应的网络协议,TCP/IP原本就是为了使用互联网而开发指定的协议族。因此互联网的协议就是TCP/IP,TCP/IP就是互联网的协议

20180623152973702027950.png
image_1bfkgsm031nqo10h4hqo1mst19c39.png-20.8kB

概念

icmp协议(internet control message protocol): 用于在ip主机、路由之间传递控制消息。控制消息是指网络不通、主机是否可达、路由是否可用等网络本身的消息。
arp协议(address resolution protocol): 根据ip地址获取物理地址的一个tcp/ip协议。
MSL(Maximum Segment Lifetime): 报文段的最大生存时间,如果报文段在网络活动了MSL时间,还没有被接收,那么会被丢弃,在linux上MSL为30秒
RTO(retransmission trip timeout): 重传超时时间。主机发送一个TCP数据包后,如果迟迟没有收到ACK,主机多久会重传这个数据包。主机从发出数据包到第一次TCP重传开始,RFC将这段时间间隔称为retransmission timeout,简称RTO。
RTT(round trip time): 请求往返时间

TTL(time to live): 存在于IP协议头中的8位长度数据,值为数据包到达目的地之前允许经过的路由器跳数,通常用于判定数据包在网络中的时间是否太长而应被丢弃。每经过一个路由,该值减1,当TTL为0时,路由器就酱该数据包丢弃,并向源端发送一个ICMP差错报文,TTL可以防止数据包陷入路由环路

路由环路是指数据包在一系列路由器之间不断传输却始终无法到达其预期目的网络的一种现象
IP协议是一种不可靠的协议,无法进行差错控制。但IP协议可以借助其他协议来实现这一功能,如ICMP

TCP/IP4层模型

应用层、传输层、网络层、数据链路层

数据链路层

不同数据链路最大的区别是它们各自的最大传输单位(MTU)不同,MTU在以太网中是1500字节,FDDI中是4352字节,ATM中是9180字节

那么如何传输大于MTU的IP包呢?
为解决这个问题,IP包会进行分片处理,意思就是将较大的IP包分拆成较小的IP包,分片的包到对端目的地址后会再被组合起来传给上一层。即从IP的上层来看,MTU完全是透明的。

TCP数据报文构成


TCP报文: 由 TCP头部 和 TCP数据 组成。
TCP头部: 由 20字节的固定长度 和 可变长字段(选项和填充)组成。

TCP头部总长度: 由TCP头中的“数据偏移/头部长度”字段决定,最大60字节
选项和填充的长度: = TCP头部总长度 - 20字节的固定长度。由头部总长度的计算可知,TCP头部总长度最大为60字节,那么“选项和填充”字段的长度最大为40字节。填充是为了使TCP头部为4byte的整数倍

头部

端口号           确定目标应用
序号             解决乱序
确认序号          解决丢包问题
状态标志位        连接维护
窗口大小          流量控制 拥塞控制,明确指出现在允许对方发送的数据量,它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
校验和           占2个字节,由发送端填充,接收端对 TCP 报文段执行 CRC 算法,以检验 TCP 报文段在传输过程中是否损坏,如果损坏这丢弃,这是TCP可靠传输的一个重要保障
数据偏移(头部总长度)  总占用0.5个字节(4位),该字段指出tcp报文段的头部长度,该字段占4bit,取最大的1111时,也就是十进制的15,TCP头部偏移单位为4byte,那么TCP头部长度最长为15*4=60字节。
保留            占0.75个字节(6位)。 保留为今后使用,但目前应置为 0
紧急指针       只有当URG标志为为1时紧急指针有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加标识紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式

建立连接与断开连接

20171030150930099869977.png

三次握手是最前面的三条线表示的过程,四次挥手是最后面的四条线表示的过程,里面涉及到几个关键词SYN,ACK,FIN,MSS

  • SYN是主要用在三次握手过程中的,FIN用在四次挥手过程中,ACK在三次握手和四次挥手过程中的作用就是对收到的SYN和FIN做一个确认(SYN,FIN等存在于TCP头里,0/1表示有无此标记)
  • FIN和SYN会有一个序号,比如上面的J,K等,对应的ACK也有一个序号,值是FIN或SYNC序号+1
  • MSS是表示每一个tcp报文里数据字段的最大长度,不包括tcp头的大小

tcp建立连接需要三次握手

20180625152991549237070.png

断开连接需要四次挥手

20180625152991550062206.png

  • 主动关闭连接的一方,调用close();协议层发送FIN包
  • 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方等待被动关闭一方的应用程序,调用close操作
  • 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;
  • 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态
  • 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

通过上面的一次socket关闭操作,你可以得出以下几点

  1. 主动关闭连接的一方,也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态
  2. 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接
  3. TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;
  4. 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

所以,这里凭你的直觉,TIME_WAIT并不可怕,CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作

TCP的半打开(half-open)

TCP的半打开是一种非常常见的链接异常情况:一方已经关闭或异常终止连接,而另外一方却还不知道。
这种情况在:server调用close()关闭连接后,client在收到FIN包后,并没有关闭链接,此时这个TCP链接就是半打开的,此时client再次发送数据对端就会在TCP协议层触发RST回包,例如下面的tcpdump抓包:
20180625152991622814133.png

当然还有经常会发生的就是,另一端网络断开和断电等导致的TCP处于半打开状态。

因为客户端的多样性已经网络的复杂,服务器端很容易就产生很多半打开的连接,半打开链接的检测,有很多方法,

  • 通过应用层,对每一个连接添加定时器监听,超时无数据就进行关闭操作。
  • 通过TCP协议层的keepalive选项来开始TCP的保活定时器,但保活不是TCP标准规范的(但很多tcp的实现中实现了此功能),TCP RFC中给出了3个不使用保活定时器的理由:
    • 在出现一个短暂的差错情况下,可能会是一个非常好的连接被释放掉;
    • 保活功能会耗费不必要的带宽;
    • 在按流量计费的情况下,会花掉更多的money;

下面是通过SO_KEEPALIVE选项来设置TCP连接支持保活的代码:

1
2
3
4
5
6
7
8
int open = 1; // 开启keepalive属性. 缺省值: 0(关闭)
int idle = 60; // 如果在60秒内没有任何数据交互,则进行探测. 缺省值:7200(s)
int interval = 5; // 探测时发探测包的时间间隔为5秒. 缺省值:75(s)
int retry_cnt = 2; // 探测重试的次数. 全部超时则认定连接失效..缺省值:9(次)
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &open, sizeof(open));
setsockopt(s, SOL_TCP, TCP_KEEPIDLE, idle, sizeof(idle));
setsockopt(s, SOL_TCP, TCP_KEEPINTVL, interval, sizeof(interval));
setsockopt(s, SOL_TCP, TCP_KEEPCNT, retry_cnt, sizeof(retry_cnt));

TCP的复位报文段

引用TCP/IP详解的话:无论何时,一个报文段发往基准的连接(referenced connection)出现错误,TCP都会发出一个复位报文段
主要有三种情况:

  • 前面提到的半打开状态:一个连接的另一端已经关闭,此时发送数据到对端,就会触发RST回包;
  • 到不存在的端口的连接请求:请求连接一个并没有监听的端口,TCP则会返回RST(UDP将会产生一个ICMP端口不可达的信息)。
  • 异常终止一个连接:在关闭连接的时候不是通过FIN报文进行正常关闭,而是通过直接发送RST进行连接关闭。两者的区别:
    • 通过FIN包进行关闭,又称:有序释放(orderly release),因为close()/shutdown()都会将缓冲区中的数据全部发送出去之后,才会发送FIN。
    • 通过RST复位包进行关闭,又称:异常释放(abortive release)。异常释放的特点:
      • 丢弃掉发送缓冲区中的全部数据,立刻发送RST报文;
      • RST接收方会区分另一端执行的是正常关闭,还是异常关闭。
        直接通过发送RST报文进行连接的异常关闭的代码如下,具体细节详见下节的TCP的延迟关闭:

PSH

PSH是一个PUSH标识,简单理解这是一次数据传输。
PUSH标识告诉接收方网络不用等待更多的数据包,直接将数据”推送”到正在接收的socket中。
TCP有一些延迟发送的策略,比如nagle算法,这些策略会使得网络更有效率,代价就是会有一定的延迟。一个对延迟敏感的应用通常会通过Push标识来关闭这些策略从而使数据尽快发送。
在Linux中,这些可通过setsocketopt() 标识 TCP_QUERYACKTCP_NODELAY完成

流量控制

为了解决报文发送的乱序和可靠性传输问题,TCP引入了滑动窗口机制。

滑动窗口机制

2019040215541867388674.png

窗口分为发送方窗口和接收方窗口。发送方收到确认应答的情况下,将窗口滑动到确认应答的序列号的位置。这样可以顺序的将多个段同时发送提高通信性能。

  • 流量控制。接收方通过ACK时的window字段来告诉发送方自己的窗口大小,从而控制发送方的发送速度,防止发送方过载接收方
  • 可靠性保证。可靠性指数据的完整性及报文顺序,TCP通过”确认重传”机制保障数据传输顺序及可靠性

Nginx中limit_rate等限速指令皆依赖滑动窗口机制实现

由于TCP延迟确认机制及累计确认机制的存在,数据报与ACK数量并不是一一对应的,比如发送了1,2,3数据报后收到ACK2 表示1,2都收到了

TCP的发送/接收窗口大小与TCP的写/读缓冲区有关,即发送/接收窗口占用的是TCP读写缓冲区的空间,但绝不是相等

1
2
3
4
5
6
7
8
9
10
11
读缓冲区最小值、默认值、最大值,单位字节,覆盖net.core.rmem_max
net.ipv4.tcp_rmem = 4096 87380 6291456
写缓冲区最小值、默认值、最大值、单位字节、覆盖net.core.wmem_max
net.ipv4.tcp_wmem = 4096 16384 4194304
系统无内存压力、启动压力模式阈值、最大值,单位为页的数量
net.ipv4.tcp_mem = 1541646 20555528 3083292
开启压力调节模式,开启后会自动调整缓冲区
net.ipv4.tcp_moderate_rcvbuf = 1

滑动窗口信息存在TCP头部的Window中

20190402155414277236288.png

滑动窗口动态调节

image_1cgsacghb1lr915fct8ojvt1b7dm.png-21.7kB

动画演示

动画演示

动画演示2

动画演示3

重传机制

报文传输过程中,难免会遇到丢失的情况,这时我们就需要对报文进行重传,TCP报文重传同时存在多种机制

超时重传

每一次发送一个TCP报文段时,就会启动重传定时器,当超时时间到达时,发送方还未收到对端的ACK确认,就重传该报文段。

其中的超时时间我们称之为RTO(retransmit timeout),RTO时间长度是由RTT计算得来的。

快速重传

如果发送方连续收到3次DUP ACK,发送方就认为这个seq的包丢失了,立即进行重传。

举个例子如接收方接收到了1、 3、 4,而2没有收到,就会立即向发送方重复发送三次ACK=2的确认请求重传。如果发送方连续收到3个相同序号的ACK,就重传该报文段,而不用等待超时 。

如果传输过程中出现乱序的情况,如何保障接收方数据顺序及完整性呢?

滑动窗口传输过程中有严格的顺序控制,假设4,5,6三个待接收的报文段,先收到了5,6,接收方不会回复5,6包的确认,而是根据tcp协议的规定,当接收方收到乱序片段时,需要重复发送ACK,此时会发送报文4 seq 的ACK,表明需要报文4,如果此后接收方接收到报文7,那么仍然要回报文4seq的ACK,如果连续发送3个DUP ACK,发送方认为这个片段已经丢失,进行快速重传

无论是快速重传还是超时重传都会面临一个问题,那就是发送端到底需要重传那些报文段

简单来说就是发送端不清楚丢失段后面传送的数据是否成功送到,往深了说的话因为TCP的确认系统不是特别好处理这种不连续确认的情况,只有低于ACK number的片段会受到确认,out-of-order的片段只能是等待。这种情况下我们只有两种选择,要么只重传ack对应的数据报,要么ACK seq后的所有数据报都重传。为了能选择性的只重传丢失的部分,TCP引入了SACK机制

SACK (selective Acknowledgment)

SACK是TCP选项,需要双方都开启SACK。它使得接收方能够告诉发送方哪些报文段丢失,哪些报文段重传了,哪些报文段已经提前收到等等信息,根据这些信息TCP就可以只重传那些真正丢失的报文段

DSACK

RFC2883中对SACK进行了扩展,DSACK可以在SACK选项中描述重复收到的报文段。

DSACK的作用让发送方了解更多的网络情况,比如是本次重传的原因是ACK丢了还是发送的数据段丢了,重复发送就表示是ACK丢了

TCP-IP详解: SACK选项

拥塞控制

虽然流量控制可以避免发送方过载接收方,但是却无法避免网络过载,这是因为接收窗口只反映了服务器个体的状况,却无法反映网络整体的状况。拥塞是由于网络中的路由器超载而引起的严重延迟现象。拥塞的发生会造成数据的丢失,数据的丢失会引起超时重传,而超时重传的数据又会进一步加剧拥塞,如果不加以控制,最终将会导致系统崩溃。

慢启动

TCP引入了慢启动算法,为避免网络过载,慢启动构造了拥塞窗口(cwnd)的概念,用来表示发送方在得到接收方确认前,最大允许传输的未经确认的数据。拥塞窗口只是发送方的一个内部参数,无需通知给接收方

cwnd初始窗口往往比较小,然后开始进行指数级增长

  • 每收到一个ACK,cwnd=cwnd+1
  • 每过一个RTT,cwnd=cwnd*2

拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。

2018082115347922097811.png
(拥塞窗口扩大)

拥塞避免

当窗口大于threshold后,开始线性扩展拥塞窗口

  • 每收到一个ACK,cwnd=cwnd+1/cwnd
  • 每过一个RTT,cwnd=cwnd+1

从慢启动的介绍中,我们能看到,发送方通过对拥塞窗口大小的控制,能够避免网络过载,在此过程中,丢包与其说是一个网络问题,倒不如说是一种反馈机制,通过它我们可以感知到发生了网络拥塞,进而调整数据传输策略,实际上,这里还有一个慢启动阈值「ssthresh」的概念,如果「cwnd」小于「ssthresh」,那么表示在慢启动阶段;如果「cwnd」大于「ssthresh」,那么表示在拥塞避免阶段,此时「cwnd」不再像慢启动阶段那样呈指数级整整,而是趋向于线性增长,以期避免网络拥塞,此阶段有多种算法实现,通常保持缺省即可,这里就不一一说明了。

image_1cgs9ua5515f5118sq5qi4124c9.png-161.1kB

拥塞发生

在慢启动的过程中,随着「cwnd」的增加,可能会出现网络过载,其外在表现就是丢包,一旦出现此类问题,「cwnd」的大小会迅速衰减,以便网络能够缓过来。

当拥塞发生后(丢包),急速降低拥塞窗口

  • RTO超时,threshold=cwnd/2,cwnd=1
  • fast retransmit,收到三个duplicate ACK,cwnd=cwnd/2,threshold=cwnd

20180821153479222231.png
(拥塞与丢包)

发送窗口的大小取决于两个方面的因素,一个是接收方的处理能力,另一个是网络的处理能力。接收方的处理能力由确认报文所通告的窗口大小(即可用的接收缓存的大小)来表示;网络的处理能力由发送方所设置的变量拥塞窗口来表示。

发送窗口大小=min(接收方窗口,拥塞窗口)

TCP协议中window size与吞吐量
TCP/IP网络与协议

抓包分析

下面通过tcpdump来查看tcp建立连接的过程: 用nc命令来进行测试

1
2
3
4
server:nc –l 9000
client:nc 9000
[anonymalias@qcloud ~]$sudo tcpdump -i lo port 9000 -X –S

tcpdump默认情况下,只会在SYN报文段中显示绝对的sequence number, 其他报文段只会显示相对的sequence number,所以需要加上-S才能在握手以外的阶段显示绝对sequence number。
-X参数用于解析和显示每一个包的包头和包体。这里只显示IP header + TCP packet, 不显示链路层link level的头部。

建立连接

20180625152991526145649.png

图中第一个tcp建立连接的请求包中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,SYN置为1,表示是一个连接请求包;
图中第二个tcp包为服务器对连接请求的应答包,6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK和SYN均置为1,表示连接请求的应答包。TCP协议规定,链接建立后,所有的报文的ACK控制位必须置为1;
图中第三个tcp包为客户端对应答包的确认包,6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK置为1。

对于tcp建立链接的过程,控制位SYN只有在链接建立前两次握手中会置为1, 其他阶段的包该控制位不能设置。控制位ACK,在第二次握手开始及以后会一直置为1,直到最后一个报文包。

断开连接

20180625152991555462675.png
tcpdump的结果如下:
图中第一个tcp的请求包中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK和FIN控制位置为1, client发起关闭tcp连接的请求。
图中第二个tcp包,为服务器响应客户端的关闭包,其中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN, ACK和FIN控制位置为1, 且回包的ack = 前一个包seq + 1,
图中第三个tcp包,为客户端对服务器关闭链接的应答,其中6个控制位字段:URG|ACK|PSH|RST|SYN|FIN,ACK控制位被置为1,这个包代表链接正式关闭;

TCP规定,FIN报文段即使不携带数据,它也消耗掉一个序号。
TCP关闭时的状态两点需要说明:

  • CLOSE_WAIT状态
    在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态,此时该tcp连接就处于半关闭状态。
  • TIME_WAIT状态
    在客户端收到服务器端最后的连接释放报文段后,客户端不会立即进入CLOSED状态。必须经过2MSL(Maximum Segment Lifetime)后,才进入CLOSED状态。
    • 为了确保客户端发送的最后一个ACK报文能够到达服务器端。
    • 防止TCP连接过程中所说的“已失效的连接请求报文段“出现在本连接中。客户端在发送完最后一个ACK报文后,再经过2MSL,就可以是本连接持续时间内的所有报文都在从网络中消失。这样就可以使下一个新的连接中不会出现旧的连接请求报文。

一次完整的建立-通信-断开过程

20180625152991410111070.png

FAQ

为什么连接的时候是三次握手,关闭的时候却是4次握手

连接时服务端接收到客户端连接请求报文后,可以直接发送SYN+ACK,其中ACK报文是用来应答,SYN报文用来同步。
但是关闭时,当被动关闭方收到FIN报文时,可能有报文还没有发送完,所以并不能立刻关闭socket,只能先回复ACK报文,表示你发送的FIN我收到了,等到被动关闭方报文都发送完了,才会发送FIN给主动关闭方,这中间有个时间差。
最后再加上last ack, 一共是4次握手。

为什么TIME_WAIT状态要经过2个MSL(最大报文生存时间)才能返回到CLOSED状态

  • 保障主动关闭方最后ACK发送到被动关闭方
    被动关闭方向主动关闭方发送FIN报文后进入LAST ACK状态,并设定超时定时器,如果超时前依旧没有收到主动关闭方的最后ACK报文,则重传FIN报文。主动关闭方收到FIN后重置TIME_WAIT时间,重新进入最后ACK发送环节。FIN报文有重传次数限制,超过后不再重传。

time wait持续2MSL时间,(MSL是数据报最大生存时间,这是一个经验值而非计算值,在linux下通常设为30s)
2MSL=最后ACK传输时间最大时间MSL+FIN重传时间最大时间MSL

  • 防止延迟数据包扰乱当前连接
    假设没有TIME_WAIT状态,主动关闭方收到FIN后进入closed状态。
    此时如果收到被动关闭方的延迟数据报后,主动关闭方会将数据丢弃并以RST进行响应,RST到达被动关闭方时,由于被动关闭方也没有这条连接的记录了,所以RST会被丢弃,这样似乎没什么问题。
    但是,如果这两台主机间用同样的端口号建立一条新连接,这条延迟数据报就看似属于这条新连接,如果数据报中数据的任意一个序列号碰巧落在新连接的当前接收窗口中,这部分数据就会被接收,从而对新连接造成破坏。
    TIME_WAIT状态确保了在原有连接的所有数据报从网络消失之前,不会再次使用原来用过的套接字对(两个IP地址及相应的端口号),以此来防止这类问题的发生。

发现大量TIME_WAIT状态连接

20171213151315032815649.png
举例,完成一个HTTP服务器,压测时服务端发现TIME_WAIT过多

分析

TIME_WAIT只会产生在主动断开的一方,由此判定是服务器端主动断开了连接。

在HTTP1.1协议中,有个 Connection 头,Connection有两个值,close和keep-alive,这个头就相当于客户端告诉服务端,服务端你执行完成请求之后,是关闭连接还是保持连接,保持连接就意味着在保持连接期间,只能由客户端主动断开连接。还有一个keep-alive的头,设置的值就代表了服务端保持连接保持多久。

HTTP默认的Connection值为close,那么就意味着关闭请求的一方几乎都会是由服务端这边发起的。那么这个服务端产生TIME_WAIT过多的情况就很正常了。

虽然HTTP默认Connection值为close,但是现在的浏览器发送请求的时候一般都会设置Connection为keep-alive了。所以,也有人说,现在没有必要通过调整参数来使TIME_WAIT降低了。

解决

方法1 增加connection:keep-alive
在压测程序发出的HTTP协议头中添加connection:keep-alive

方法2 系统调优
google一下排在最前的策略是修改/etc/sysctl.conf
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1

那么这几个参数到底有什么效果?

net.ipv4.tcp_timestamps
RFC 1323 在 TCP Reliability一节里,引入了timestamp的TCP option,两个4字节的时间戳字段,其中第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送的数据的时间。有了这两个时间字段,也就有了后续优化的余地。
tcp_tw_reuse 和 tcp_tw_recycle就依赖这些时间字段。

net.ipv4.tcp_tw_reuse
当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接

通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接“连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。

那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。

这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。

net.ipv4.tcp_tw_recycle(该选项自linux4.12移除)
当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据RTT动态计算出来,但是远小于2MSL。

有了这个配置,还是需要保障丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。

这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。

由此,如果客户端处于NAT的网络(多个客户端,同一个IP出口的网络环境),如果配置了tw_recycle,就可能在一个RTO的时间内,只能有一个客户端和自己连接成功(不同的客户端发包的时间不一致,造成服务端直接把数据包丢弃掉)。

参考
你遇到过TIME_WAIT的问题吗?
Coping with the TCP TIME-WAIT state on busy Linux servers
再叙TIME_WAIT

我们平时所说连接池可以复用,是不是意味着等到上个连接time_wait结束后才能再次使用

所谓连接池复用,复用的一定是活跃的连接,所谓活跃,第一表明连接池里的连接都是ESTABLISHED的,第二,连接池作为上层应用,会有定时的心跳去保持连接的活性。既然连接都是活跃的,那么也就不存在time_wait的情况。
time_wait只会出现在主动关闭的一方,是关闭连接后才进入的状态。既然连接都已经关闭了,那么这条连接肯定不在连接池里了,即被连接池释放了

SYN攻击

什么是 SYN 攻击(SYN Flood)

在三次握手过程中,服务器发送 SYN-ACK 之后,收到客户端的 ACK 之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD 状态。当收到 ACK 后,服务器才能转入 ESTABLISHED 状态.

SYN 攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。

SYN 攻击是一种典型的 DoS/DDoS 攻击。

如何检测 SYN 攻击

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

如何防御 SYN 攻击

SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害,常见的防御 SYN 攻击的方法有如下几种:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies技术

TCP在listen时的参数backlog的意义

linux内核中会维护两个队列:
  1)半连接队列:接收到一个SYN建立连接请求,处于SYN_RCVD状态
  2)已完成队列:已完成TCP三次握手过程,处于ESTABLISHED状态
  当有一个SYN到来请求建立连接时,就在未完成队列中新建一项。当三次握手过程完成后,就将套接口从未完成队列移动到已完成队列。
  backlog曾被定义为两个队列的总和的最大值,也曾将backlog的1.5倍作为未完成队列的最大长度
一般将backlog指定为5

TCP和UDP的区别

TCP是有连接的,两台主机在进行数据交互之前必须先通过三次握手建立连接;而UDP是无连接的,没有建立连接这个过程
TCP是可靠的传输,TCP协议通过确认和重传机制来保证数据传输的可靠性;而UDP是不可靠的传输
TCP还提供了拥塞控制、滑动窗口等机制来保证传输的质量,而UDP都没有
TCP是基于字节流的,将数据看做无结构的字节流进行传输,当应用程序交给TCP的数据长度太长,超过MSS时,TCP就会对数据进行分段,因此TCP的数据是无边界的;而UDP是面向报文的,无论应用程序交给UDP层多长的报文,UDP都不会对数据报进行任何拆分等处理,因此UDP保留了应用层数据的边界

为什么要采用三次握手,而不是两次握手

这主要是为了防止已失效的连接请求报文段突然又传送到服务器,产生错误。
假设TCP使用两次握手,有这么一种情况,客户A发送连接请求,但因连接请求报文丢失而未收到确认,于是A会再次重传一次连接请求,此时服务器端B收到再次重传的连接请求,建立了连接,然后进行数据传输,数据传输完了后,就释放了此连接。假设A第一次发送的连接请求并没有丢失,而是在网络结点中滞留了太长时间,以致在AB通信完后,才到达B。此时这个连接请求其实已经是被A认为丢失的了。如果不进行第三次握手,那么服务器B可能在收到这个已失效的连接请求后,进行确认,然后单方面进入ESTABLISHED状态,而A此时并不会对B的确认进行理睬,这样就白白的浪费了服务器的资源。

连接该怎么理解,是否存在连接实体

连接是不存在的,是一个逻辑上便于理解的概念。连接就是两端的状态维护,中间过程没有所谓的连接,一旦传输失败,一端收到消息,才知道状态的变化

消息边界

TCP 是流式的数据传输,消息没有边界,需要应用层自己去定义消息边界,而 UDP 是数据报传输,所以协议保证了一次只能接收一个数据报

 面向无连接的 UDP协议是面向报文的有边界的报文的协议。发送方的UDP对应用程序交下来的报文,在添加头部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。

 面向连接的TCP协议属于无边界的字节流协议,用户每次调用接收发送函数接口时,不一定都能接收发送一条完整的消息,而是必须对裸字节流进行拆分、组合(同于基于有边界报文的UDP协议的应用程序有很大差别)。

read/recv/recvfrom:TCP,客户端连续发送数据,只要服务端的这个函数的缓冲区足够大,会一次性接收过来(客户端是分好几次发过来,是有边界的,而服务端却一次性接收过来,所以证明是无边界的);

  UDP:客户端连续发送数据,即使服务端的这个函数的缓冲区足够大,也只会一次一次的接收,发送多少次接收多少次(客户端分几次发送过来,服务端就必须按几次接收,从而证明,这种UDP的通讯模式是有边界的)

参考

TCP协议
浅谈TCP/IP网络编程中socket的行为
Go语言TCP网络编程(详细)
[Golang] 从零开始写Socket Server(1): Socket-Client框架
TCP相关面试题总结
TCP连接的建立和关闭详解