TCP 包头格式
TCP 头的格式如图:
可参看:TCP协议以及其报头结构分析 、TCP 包头详解
- 16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)。
- 32位序号:一次TCP通信过程中的一个传输方向上的字节流的每个字节的编号。
- 32位确认号:用作对另一个发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1.假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对主机B发送来的TCP报文段的确认号。反之,B发出的TCP报文段也同时携带自己的序号和对A发送来的报文段的确认号。
- 4位首部长度:标识该TCP头部有多少个32bit(4字节),四位最大表示15,所以TCP报头最长是60字节。
- 6位标志位 :
- URG:表示紧急指针是否有效。
- ACK:表示确认号是否有效。我们称携带ACK标志的TCP报文段为确认报文段。
- PSH:提示接收端应用程序应该立即从TCP接受缓冲区中读走数据,为接受后续数据腾出空间,如果不将接收到的数据读走,它们就会一直停留在TCP报文段。
- RST:表示要求对方重新建立连接。我们称携带RST标志的TCP报文段为复位报文段。
- SYN:表示请求建立一个新连接。称携带SYN标志位的TCP报文段为同步报文段。
- FIN:表示通知对方本端要关闭连接了。称携带FIN标志的TCP报文段为结束报文段。
- 16位窗口大小:这是TCP流量控制的一个手段。这里的窗口指的是接受通告的窗口。告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样就对方就可以控制发送数据的速度。
- 16位校验和:由发送端填充,接收端对TCP报文端执行CRC算法以检验TCP报文段在传输过程中是否损坏(检验部分包括报头和数据部分)。
- 16位紧急指针:为一个正的偏移量。它的序号字段的值相加表示最后一个紧急数据的下一个字节的序号。即紧急指针相对于当前序号的偏移。
- 剩下40字节为选项字段。
序号解决包的乱序问题。
确认序号确认对方收到,没有收到则重新发送。
状态位维护连接的状态。SYN发起连接,ACK回复,RST重新连接,FIN结束连接。
窗口大小用于流量控制。
通过对 TCP 头的解析,要掌握 TCP 协议,重点应该关注以下几个问题:
- 顺序问题 ,稳重不乱;
- 丢包问题,承诺靠谱;
- 连接维护,有始有终;
- 流量控制,把握分寸;
- 拥塞控制,知进知退。
TCP 的三次握手
TCP 的连接建立,我们常常称为三次握手。
A:您好,我是 A。
B:您好 A,我是 B。
A:您好 B。
B收到A发来的 您好 B。三次握手完成
常称为“请求 -> 应答 -> 应答之应答”的三个回合。
什么要三次,而不是两次?为了可靠,为什么不是四次?
两次握手:存在A的请求连接B的报文在网络中滞留,延误到连接释放后才到达B,B以为是A新发起的连接请求,同意建立连接,浪费了资源:端口、内存等。
三次握手可防止上述情况发生,因为A不确认,B就不会建立连接。四次握手是可以的,四十次都可以,关键四百次也不能保证就真的可靠了,三次握手只要双方的消息都有去有回,就基本就认为连接可以了。
大部分情况下,A和B建立连接后,A会马上发数据。如果不发,B可以开启keepalive,发送探活包。B也可以对于长时间不发包的客户端,主动关闭。
三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP 包的序号的问题。
A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1开始呢?因为这样往往会出现冲突。
TCP三次握手状态机
1、客户端和服务端都处于 CLOSED 状态。
客户端: CLOSED ·服务端: CLOSED
2、服务端主动监听某个端口
客户端: CLOSED ·服务端: LISTEN
3、客户端主动发起连接 SYN
客户端: SYN-SENT ·服务端: LISTEN
4、服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN
客户端: SYN-SENT ·服务端: SYN-RCVD
5、客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK
客户端: ESTABLISHED ·服务端: SYN-RCVD
6、服务端收到 ACK 的 ACK 之后
客户端: ESTABLISHED ·服务端: ESTABLISHED
此时客户端服务端都是一发一收了
TCP 四次挥手
好说好散,这常被称为四次挥手。
A:B 啊,我不想玩了。
B:哦,你不想玩了啊,我知道了。
B:A 啊,好吧,我也不玩了,拜拜。
A:好的,拜拜。
整个连接就关闭了,上面是和平分手的场面,但也会有异常
异常1:A 说完“不玩了”之后,直接跑路,会导致问题,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。
异常2:A 说完“不玩了”,B 直接跑路,也是有问题的,A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。
为了解决这些问题,TCP 协议专门设计了几个状态来处理这些问题。如下图状态时序图。
TCP四次挥手状态机
1、初始状态A、B均处于 ESTABLISHED 状态。
A: ESTABLISHED B: ESTABLISHED
2、A 说“不玩了”
A: FIN_WAIT_1 B: ESTABLISHED
3、B 收到“A 不玩”的消息后,发送知道了到A
A: FIN_WAIT_1 B: CLOSE_WAIT
4、A 收到“B 说知道了”
A: FIN_WAIT_1 B: CLOSE_WAIT
如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
5、如果 B 没有跑路,发送了“B 也不玩了”的请求到达 A
A: FIN_WAIT_2 B: LAST-ACK
6、A 发送“知道 B 也不玩了”的 ACK
A: TIME-WAIT(等待2MSL) B: LAST-ACK
按说 A 这时可以跑路了,但是直接跑路有问题:
问题1 :最后的这个 ACK 万一 B 收不到呢?
则 B 会重新发一个“B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。
问题2 :A 直接跑路,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都死翘翘,再空出端口来。
7、B 收到 A 发送“知道 B 也不玩了”的 ACK,并且 A 也等够 2MSL
A: CLOSED B: CLOSED
MSL是Maximum Segment Lifetime,报文最大生存时间它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
还有一个异常情况:B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?
按照 TCP 的原理,B 当然还会重发 FIN,这个时候A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑
TCP 状态机
将连接建立和连接断开的两个时序状态图综合起来,就是这个著名的 TCP 的状态机
加黑加粗的部分,是上面说到的主要流程,其中阿拉伯数字的序号,是连接过程中的顺序,而大写中文数字的序号,是连接断开过程中的顺序。加粗的实线是客户端 A 的状态变迁,加粗的虚线是服务端 B 的状态变迁。
小结
TCP 包头很复杂,主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制;
连接的建立是经过三次握手,断开的时候四次挥手,状态图
参考资料:
趣谈网络协议(极客时间)链接:
http://gk.link/a/106nW
TCP协议以及其报头结构分析:
https://blog.csdn.net/MBuger/article/details/74078777
TCP 包头详解:
https://blog.51cto.com/13854765/2163296
GitHub链接:
https://github.com/lichangke/LeetCode
知乎个人首页:
https://www.zhihu.com/people/lichangke/
CSDN首页:
https://me.csdn.net/leacock1991
欢迎大家来一起交流学习