TCP协议

TCP 协议是网络通信中最核心的协议之一,位于七层网络协议的第四层:传输层。
TCP 数据包结构

整个 TCP 数据包的结构如上图所示,是一个嵌套结构,从外到内依次是以太网数据,IP数据,TCP数据。
以太网的数据包也叫帧是网络传输的基本单元,大小是固定的,最初是最大 1518 字节(18 字节标头 + 1500 字节数据),后来在以太网标头增加 4 字节可选标识变为最大 1522 字节。
IP 数据数据包则在以太网数据包里面,有自己的头信息至少 20 字节,所以IP数据包的数据大小为 1480 字节。
TCP 数据在IP数据包里面,一般有 20~60 字节的头信息,所以 TCP 数据包的大小一般为 1460~1420 字节。
TCP 头部信息

- SourcePort 和 DestinationPort:16 位源端口号和 16 位目的端口号。
- Sequence Number:32 位序号,一次 TCP 通信过程中某一个传输方向上的字节流的每个字节的编号,通过这个来确认发送的数据有序,比如现在序列号为 1000,发送了 1000,下一个序列号就是 2000。
- Acknowledgement Number:32 位确认号,用来响应 TCP 报文段,给收到的 TCP 报文段的序号加 1,三握时还要携带自己的序号。
- Data Offset:标识该 TCP 头部有多少个 4 字节,因为 4 位最大能表示 15,所以 TCP 头部最长是 60字节。
- Reserved:保留位—须置 0。
- Flags:
- NS—ECN-nonce。ECN 显式拥塞通知(Explicit Congestion Notification)是对 TCP 的扩展,定义于 RFC 3540(2003)。ECN 允许拥塞控制的端对端通知而避免丢包。ECN 为一项可选功能,如果底层网络设施支持,则可能被启用 ECN 的两个端点使用。在 ECN 成功协商的情况下,ECN 感知路由器可以在 IP 头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样。
- CWR—Congestion Window Reduced,定义于 RFC 3168(2001)。
- ECE—ECN-Echo 有两种意思,取决于 SYN 标志的值,定义于 RFC 3168(2001)。
- URG—为 1 表示高优先级数据包,紧急指针字段有效。
- ACK—为 1 表示确认号字段有效。
- PSH—为 1 表示是带有 PUSH 标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
- RST—为 1 表示出现严重差错。可能需要重新创建 TCP 连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN—为 1 表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
- FIN—为 1 表示发送方没有数据要传输了,要求释放连接。
- Window:16 位窗口大小,TCP 流量控制的一个手段,用来告诉对端 TCP 缓冲区还能容纳多少字节。
- Checksum:16 位校验和,由发送端填充,接收端对报文段执行 CRC 算法以检验 TCP 报文段在传输中是否损坏。
- Urgent Pointer:16 位紧急指针,一个正的偏移量,它和序号段的值相加表示最后一个紧急数据的下一字节的序号。
- Option:TCP 选项,可变长的可选信息,这部分最多包含 40 字节。选项的第一个字段为选项的类型 kind,有的 TCP 选项没有后面两个字段,仅包含 1 字节的 kind 字段。第二个字段 length(如果有的话)指定该选项的总长度,该长度包括 kind 字段和 length 字段占据的 2 字节,第三个字段 info(如果有的话)是选项的具体信息。
TCP链接建立和断开
TCP 使用三次握手过程建立连接,使用四次挥手过程断开连接。
三次握手

TCP 用三次握手(或称三路握手,three-way handshake)过程创建一个连接。在连接创建过程中,很多参数要被初始化,例如序号被初始化以保证按序传输和连接的强壮性。
TCP 连接的正常建立
通常是由一端(服务器端)打开一个套接字(socket)然后监听来自另一方(客户端)的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,客户端就能开始创建主动打开(active open)。
三次握手协议的过程
- 客户端向服务器端发送一个 SYN 包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数 A 作为消息序号(Sequence Number)。客户端的状态变成: SYN_SEND。
- 服务器端收到一个合法的 SYN 包后,把该包放入 SYN 队列中;回送一个 SYN/ACK。ACK 的确认码应为 A+1,SYN/ACK 包本身携带一个随机产生的序号 B。服务端状态变成 SYN_RECEVIED。
- 客户端收到 SYN/ACK 包后,发送一个 ACK 包,该包的序号被设定为 A+1,而 ACK 的确认码则为B+1。客户端状态变成 ESTABLISHED,服务端收到确认后也变成 ESTABLISHED。

看到有位博主的图片非常有意思,用来比喻三次握手非常形象,哈哈。
四次挥手

TCP 连接终止使用四次挥手过程(或称四次握手,four-way handshake),在这个过程中连接的每一侧都独立地被终止。当一个端点要停止它这一侧的连接,就向对侧发送 FIN,对侧回复 ACK 表示确认。拆掉一侧的连接过程需要一对 FIN 和 ACK,分别由两侧端点发出。终止链接可以由双方任意方发起,下面只是按照客户端方发起描述。
四次挥手协议过程
- 客户端向服务器端发送一个 FIN 包,请求关闭连接,SEQ=A。客户端的状态变成 FIN1_WAIT。
- 服务器端收到后返回 ACK=A+1,服务端状态变成 CLOSE_WAIT,客户端状态变为 FIN_WAIT2。
- 服务端向客户端器端发送一个 FIN 包,请求关闭连接,SEQ=B。客户端的状态变成 LAST_ACK。
- 客户器端收到后返回 ACK=B+1。服务端状态变成 CLOSE,客户端状态变为 TIME_WAIT。
发出 FIN 的一侧,如果给对侧的 FIN 响应了 ACK(TIME_WAIT),那么就会等待 2*MSL 时间,然后关闭连接。在这段超时等待时间内,本地的端口不能被新连接使用(避免延时到达的包与随后的新连接混淆)。参数 tcp_max_tw_buckets 控制并发的 TIME_WAIT 的数量,默认值是 180000,如果超限,系统会把多的 TIME_WAIT 状态的连接清理掉。
PS:用 wireshark 之类分析 TCP 数据包的时候会发现四次挥手过程往往只有 3 个数据包,这是因为 TCP 规定 ACK 可以捎带在其他数据包当中,所以你看到的主动断开连接一方本应收到的 ACK,是被对方的 FIN 包捎带过来的,就变成了三个包了。但是如果当被动方还有数据未发送完的时候就会有如图所示的四次交互了,也是因为这样才不能称为三次挥手。
ACK 和滑动窗口
可靠传输 ACK
通常在每个 TCP 报文段中都有一对序号和确认号。TCP 报文发送者称自己的字节流的编号为序号(Sequence Number),称接收到对方的字节流编号为确认号(Acknowledgement Number)。
发送的时候,TCP 协议为每个包编号(Sequence Number),以便接收的一方按照顺序还原。万一发生丢包,也可以知道丢失的是哪一个包。
第一个包的编号是一个随机数。为了便于理解,这里就把它称为 1 号包。假定这个包的负载长度是 100 字节,那么可以推算出下一个包的编号应该是 101(握手、挥手阶段是+1)。这就是说,每个数据包都可以得到两个编号:自身的编号,以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。
TCP 报文的接收者为了确保可靠性,在接收到一定数量的连续字节流后发送确认。这是对 TCP 的一种扩展,称为选择确认(Selective Acknowledgement),ACK 的值=收到包序号+数据长度。选择确认使得 TCP 接收者可以对乱序到达的数据块进行确认。
发送窗口
接收方在“窗口大小”域指出还可接收的字节数量。发送方在没有新的确认包的情况下至多发送“窗口大小”允许的字节数量。接收方可修改“窗口大小”的值。
通过 ACK 以及发送窗口进行传输数据过程如下图所示。

滑动窗口
TCP 以 1 个段为单位,每发送一个段进行一次确认应答的处理。这样的传输方式有一个缺点,就是包的往返时间越长通信性能就越低。
为解决这个问题,TCP 引入了窗口这个概念。确认应答不再是以每个分段,而是以更大的单位进行确认,转发时间将会被大幅地缩短。也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送,类似于并行发送。如下图所示:

在接收到所有的 ACK 之后(其实并不是每一个报文段都会回复 ACK,可能会对两个报文段发送一个 ACK,也可能会对多个报文段发送 1 个 ACK),窗口就会指向下一个要发送的数据区间,直至数据发送完毕。其实即使这样传输数据,在传输大量数据,如上传大文件的时候,如果客户端与服务端的网络延迟较高,还是会产生较大的耗时,因为即使并行传输,但并行的量是受限的,并行批次之间的网络延迟还是无法忽略的。

滑动窗口文字描述起来还是很费劲,可以看B站的动画,非常形象。
慢启动
服务器发送数据包,当然越快越好,最好一次性全发出去。但是,发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多因素都会导致丢包。线路不好的话,发得越快,丢得越多。
最理想的状态是,在线路允许的情况下,达到最高速率。但是我们怎么知道,对方线路的理想速率是多少呢?答案就是慢慢试。
TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢包的情况,调整速率:如果不丢包,就加快发送速度;如果丢包,就降低发送速度。
Linux 内核里面设定了(常量 TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送 10个 数据包,即"发送窗口"的大小为 10。然后停下来,等待接收方的确认,再继续发送。
发送方通过 ACK 中的“Ack Num”及“窗口大小”,再加上自己已经发出的数据包的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。
数据包的遗失处理
每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化。
举例来说,现在收到了 4 号包,但是没有收到5号包。ACK 就会记录,期待收到 5 号包。过了一段时间,5 号包收到了,那么下一轮 ACK 会更新编号。如果 5 号包还是没收到,但是收到了 6 号包或 7 号包,那么 ACK 里面的编号不会变化,总是显示 5 号包。这会导致大量重复内容的 ACK。
如果发送方发现收到三个连续的重复 ACK 或者超时了还没有收到任何 ACK,就会确认丢包,即 5 号包遗失了,从而再次发送这个包。通过这种机制,TCP 保证了不会有数据包丢失。

其他
TCP Dump
用于抓包分析,可以参照tcpdump详细教程,或者使用命令 man tcpdump 进行查看。
常用如:sudo tcpdump -nn net 100.83 -XX -w my.cap
实例分析

这是通过 wireshark 打开的 TCP dump 文件中的一次完整请求过程。
三次握手过程

首先看第一个是数据包是客户端发送链接请求 Syn=1,Seq=0

服务的收到请求后,发送 SYN+ACK,Ack=1,Syn=1,Seq=0

客户端收到 SYN+ACK 后返回 ACK,Seq=1,Ack=1。到这里三次握手过程就已经建立完成。
数据传输

首先是客户端发送请求数据,Seq=1,Next Seq=716(Seq + TCP payload)。

服务的收到请求后的 ACK,Seq=1, Ack = 716。
后面的两个数据包就是服务端向客户端发送的结果数据,与客户端发起请求过程一致,只不过是由服务端发起。
四次挥手过程

客户端发送断开连接请求,Fin=1,Seq=716

服务端发送 ACK+FIN,Fin=1,Ack=717,Seq=1206

客户端发送 ACK,Ack=1207,这样两边的连接就都会断开了。
状态编码
下表为 TCP 状态码列表,以 S 指代服务器,C 指代客户端,S&C 表示两者,S/C 表示两者之一:
状态 | 端 | 说明 |
---|---|---|
LISTEN | S | 服务器等待从任意远程 TCP 端口的连接请求。侦听状态。 |
SYN-SENT | C | 客户在发送连接请求后等待匹配的连接请求。通过 connect() 函数向服务器发出一个同步(SYNC)信号后进入此状态。 |
SYN-RECEIVED | S | 服务器已经收到并发送同步(SYNC)信号之后等待确认(ACK)请求。 |
ESTABLISHED | S&C | 服务器与客户的连接已经打开,收到的数据可以发送给用户。数据传输步骤的正常情况。此时连接两端是平等的。这称作全连接。 |
FIN-WAIT-1 | S&C | (服务器或客户)主动关闭端调用 close() 函数发出 FIN 请求包,表示本方的数据发送全部结束,等待 TCP 连接另一端的 ACK 确认包或 FIN&ACK 请求包。 |
FIN-WAIT-2 | S&C | 主动关闭端在 FIN-WAIT-1 状态下收到 ACK 确认包,进入等待远程 TCP 的连接终止请求的半关闭状态。这时可以接收数据,但不再发送数据。 |
CLOSE-WAIT | S&C | 被动关闭端接到FIN后,就发出 ACK 以回应 FIN 请求,并进入等待本地用户的连接终止请求的半关闭状态。这时可以发送数据,但不再接收数据。 |
CLOSING | S&C | 在发出 FIN 后,又收到对方发来的 FIN 后,进入等待对方对己方的连接终止(FIN)的确认(ACK)的状态。少见。 |
LAST-ACK | S&C | 被动关闭端全部数据发送完成之后,向主动关闭端发送 FIN,进入等待确认包的状态。 |
TIME-WAIT | S/C | 主动关闭端接收到 FIN 后,就发送 ACK 包,等待足够时间以确保被动关闭端收到了终止请求的确认包。【按照 RFC 793,一个连接可以在 TIME-WAIT 保证最大四分钟,即最大分段寿命(maximum segment lifetime)的 2 倍】 |
CLOSED | S&C | 完全没有连接。 |
维基百科
Tcp的三次握手和四次挥手
为什么tcp 连接断开只有3个包?
TCP 协议简介
太厉害了,终于有人能把TCP/IP 协议讲的明明白白了