预备知识
TCP 头格式
TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里我只说四元组。
注意上图中的四个非常重要的东西:
- Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
- Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。
- Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
- TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的。
TCP 特点
- 三次握手
- 四次挥手
- 可靠连接
- 丢包重传
- TCP连接状态
TCP的一个核心是:可靠传输协议。
三次握手
- 第一步:client 发送 syn 到server 发起握手;
- 第二步:server 收到 syn后回复syn+ack给client;
- 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack。
ack总是seq+len(包的大小),这样发送方明确知道server收到那些东西了,但是特例是三次握手和四次挥手,虽然len都是0,但是syn和fin都要占用一个seq号,所以这里的ack都是seq+1。
tcp在传输过程中都有一个ack,接收方通过ack告诉发送方收到那些包了。这样发送方能知道有没有丢包,进而确定重传
- 关于ISN的初始化。ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL - Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
- 关于建连接时SYN超时。试想一下,如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
当len 不为0 时候,Client 和Server是这样保证数据传输的。
Seq的增加是和传输的字节数相关的。上图中,三次握手后,先来了一个len:43的包,Server返回的ack=seq(Client):1000 + len:43 = 1043。表示服务器收到第一个数据包。TCP依靠Seq 和Ack ,保障了TCP传输数据包的完整性。
四次挥手
因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。 �简单说来是 “先关读,后关写”,一共需要四个阶段。以CLIENT发起关闭连接为例:
- 服务器读通道关闭� : client主动发送Fin包给server
- 客户机写通道关闭� : server回复Ack(对应第一步fin包的ack)给client,表示server知道client要断开了
- 客户机读通道关闭� : server发送Fin包给client,表示server也可以断开了
- 服务器写通道关闭 :client回复Ack给server。
丢包重传
重传这部分完全摘抄自TCP 的那些事儿(上) ,总结实在太好了!
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传机制
一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。
对此有两种选择:
- 一种是仅重传timeout的包。也就是第3份数据。
- 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。
*这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长(在下篇会说TCP是怎么动态地计算出timeout的)
快速重传机制
Fast Retransmit
于是,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下:
Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。
SACK 方法
另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。
这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。
注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡》
Duplicate SACK – 重复收到数据的问题
Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。RFC-2883 里有详细描述和示例。下面举几个例子(来源于RFC-2883)
D-SACK使用了SACK的第一个段来做标志,
如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK
如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK
示例一:ACK丢包
下面的示例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
3000-3499 3000-3499 3500 (ACK dropped)
3500-3999 3500-3999 4000 (ACK dropped)
3000-3499 3000-3499 4000, SACK=3000-3500
** 示例二,网络延误**
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为ACK已到了3000,所以,这个SACK是D-SACK——标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
500-999 500-999 1000
1000-1499 (delayed)
1500-1999 1500-1999 1000, SACK=1500-2000
2000-2499 2000-2499 1000, SACK=1500-2500
2500-2999 2500-2999 1000, SACK=1500-3000
1000-1499 1000-1499 3000
1000-1499 3000, SACK=1000-1500
可见,引入了D-SACK,有这么几个好处:
1)可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
2)是不是自己的timeout太小了,导致重传。
3)网络上出现了先发的包后到的情况(又称reordering)
4)网络上是不是把我的数据包给复制了。
知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。
TCP连接状态
图片来源自TCP网络关闭的状态变换时序图
建立连接
- 正常情况
1.1 S调用listen开启监听,S的状态由CLOSED--->LISTEN。
1.2 C调用connect发起连接,即发送SYN_1给S,C的状态由CLOSED---->SYN_SENT。
1.3 S收到SYN_1,把此连接信息放入未完成连接队列(incomplete connection queue),回复SYN_2和ACK_1(SYN_1 + 1),S的状态由LISTEN--->SYN_RCVD
1.4 C收到ACK_1和SYN_2,回复ACK_2(SYN_2 + 1),C的状态由SYN_SEND--->ESTABLISHED。
1.5 S收到ACK_2,把此连接信息从未完成连接队列移除,并放入完成连接队列(complete connection queue),由listen函数的backlog控制完成连接队列大小。S的状态由SYN_RCVD--->ESTABLISHED。
1.6 S可以调用accept从完成连接队列获取已建立好的连接。
- 同时建立连接
2.1 S1和S2同时发送SYN(其中S1发送SYN_1,S2发送SYN_2),S1和S2同时由LISTEN--->SYN_SENT。
2.2 S1收到SYN_2,回复SYN_1和ACK_1(SYN_2 + 1),S1的状态由SYN_SEND--->SYN_RCVD。
2.3 S2收到SYN_1,回复SYN_2和ACK_2(SYN_1 + 1),S2的状态由SYN_SEND--->SYN_RCVD。
2.4 S2收到ACK_1和SYN_1,S2的状态由SYN_RCVD--->ESTABLISHED。
2.5 S1收到ACK_2和SYN_2,S1的状态由SYN_RCVD--->ESTABLISHED。
断开连接
- 正常情况
1.1 C调用close,发送FIN_1,C的状态由ESTABLISHED--->FIN_WAIT_1。
1.2 S收到FIN_1,发送ACK_1(FIN_1 + 1),S的状态由ESTABLISHED--->CLOSE_WAIT。
1.3 C收到ACK_1,C的状态由FIN_WAIT_1--->FIN_WAIT_2。
1.4 S调用close,发送FIN_2,S的状态由CLOSE_WAIT--->LAST_ACK。
1.5 C收到FIN_2, 发送ACK_2(FIN_2 + 1),C的状态由FIN_WAIT_2--->TIME_WAIT。
1.6 S收到ACK_2,S状态由CLOSE_WAIT--->CLOSED。
1.7 C经过2个MSL(Maximum Segment Lifetime),之所以要等待2个MSL,是因为S在发送FIN_2后,会等待1个MSL,如果1个MSL内未收到ACK_2,则S认为FIN_2丢包了,会重发FIN_2,S重发的FIN_2到达C最大的时间为2个MSL,因此如果C在2个MSL内未收到FIN_2,则认为S已收到ACK_2。C的状态由TIME_WAIT--->CLOSED。- 特殊情况
2.1 C调用close,发送FIN_1,C的状态由ESTABLISHED--->FIN_WAIT_1。
2.2 S收到FIN_1,S调用close,发送FIN_2的同时带上ACK_1(FIN_1 + 1),S的状态由ESTABLISHED--->LAST_ACK。
2.3 C收到FIN_2和ACK_1,发送ACK_2(FIN_2 + 1),C的状态由FIN_WAIT_1--->TIME_WAIT。
2.4 S收到ACK_2,S状态由CLOSE_WAIT--->CLOSED。
2.5 C经过2MSL,C的状态由TIME_WAIT--->CLOSED。- 同时关闭
3.1 S1和S2同时调用close,发送FIN(其中S1发送FIN_1,S2发送FIN_2),S1和S2的状态都由ESTABLISHED--->FIN_WAIT_1。
3.2 S1收到FIN_2,发送ACK_1(FIN_2 + 1),S1的状态由FIN_WAIT_1--->CLOSING。
3.3 S2收到FIN_1,发送ACK_2(FIN_1 + 1),S2的状态由FIN_WAIT_2--->CLOSING。
3.4 S2收到ACK_1,S2的状态由CLOSING--->TIME_WAIT。
3.5 S1收到ACK_2,S1状态由CLOSING--->TIME_WAIT。
3.6 S1和S2经过2MSL,S1和S2的状态由TIME_WAIT--->CLOSED。
Ref:
TCP网络关闭的状态变换时序图
TCP 的那些事儿(上)
https://nmap.org/book/tcpip-ref.html