TCP基本认识
TCP基本概念
Transmission Control Protocol
TCP是一个面向连接的、可靠的和基于字节流的传输层协议:
- 面向连接:通信的socket是一一对应的,并且只能是一对一;
- 可靠:TCP可以保证报文是无损坏、无冗余和按次序的;
基于字节流:TCP是没有消息边界的,比如发送多个数据包可能接收端只接收一次;
TCP报文内容

序列号:建立连接时随机生成一个数作为初始值,通过SYN包发送给接收端,每发送一次数据,序列号累加上传递数据的字节数;
- 确认应答号:发送端接收到确认应答号表明该序号之前的序列号的数据包都已经收到,确认应答号是接收端发过来的「期待」下一次接收的序列号;
控制位:
源地址
- 源端口
- 目标地址
- 目标端口
其中,源地址、目标地址存放在IP头部;源端口、目标端口存放在TCP头部
TCP与UDP的区别
UDP报文的内容

相比TCP简单许多,头部只有8字节:
包长度:头部+数据的总长度;
校验和:保证首部和数据无误;
TCP和UDP的区别
- 连接
- TCP传输之前需要建立连接;
- UDP通过地址和端口直接传输;
- 消息边界
- TCP面向流,没有消息边界;
- UDP有消息边界,以包为单位进行发送和接收;
- 可靠性
- TCP保证可靠传输,保证数据无缺失、无冗余和按次序到达;
- UDP不保证可靠交付;
- 控制机制
- TCP有流量控制、拥塞控制
- UDP没有控制机制
- 服务对象的数量
- 一个TCP连接只有两个端点;
- UDP支持一对多和多对多交互通信;
- 首部开销不同
- TCP不使用首部选项的时候首部大小为20字节,使用选项则更大;
- UDP首部为8字节;
- 分片不同
- TCP主要用于需要保证可靠交付的场景,如文件传输,HTTP/HTTPS等
- UDP常用于需要实时性更强的比如多媒体通信和一对多的广播通信
TCP首部和UDP首部的差异
为什么TCP有首部长度字段而UDP没有?
因为TCP首部有可变长的选项字段,而UDP首部固定8字节大小不变。
为什么UDP有包长度字段而TCP没有?
TCP数据部分的长度通过:IP包总长度-IP首部-TCP首部 来得到,UDP数据长度直接通过UDP包长度-UDP首部长度得到。虽然UDP也可以通过TCP同样的方式来计算,但是为了方便硬件的处理,首部长度最好为4字节的整数倍,补上包长度的两字节,UDP首部刚好为8字节。
TCP连接的建立
TCP三次握手
- 最初客户端和服务端都处于CLOSE状态,直到服务端开始LISTEN某个端口进入LISTEN状态;
- 之后客户端发送一个报文,序列号自动生成,SYN为1,发送后客户端处于SYN_SENT状态(图上是SYS_SENT,写错);
- 服务端收到客户端的报文后变为SYN_REVD状态,随后发送一个报文,ACK和SYN为1,序列号自动生成,确认应答号为客户端序列号+1;
- 客户端收到服务端的报文后变为ESTABLISHED状态,随后发送一个报文,ACK为1,确认应答号为服务端序列号+1的确认报文,因为客户端此时是ESTABLISHED状态,所以本次的报文可以携带应用层数据;

为什么是三次握手?
主要由三个原因:
- 首要原因是防止历史的连接请求初始化了连接:如果一个旧的SYN报文比最新的SYN报文先到达服务端,服务端返回的确认应答号将不是客户端最新的SYN报文序列号+1,客户端判断本次连接为历史连接,可以在第三次握手时发送RST报文终止历史连接的建立;
- 其次是三次握手刚好可以同步双方的初始序列号,三次握手对于客户端和服务端都有一个发送和确认序列号的过程;
- 避免资源的浪费:如果只有两次握手,客户端在网络阻塞时可能发送多个SYN报文,服务端由于收不到客户端的ACK,只能收到一个SYN就建立一次连接,可能建立很多冗余连接浪费资源。
为什么初始序列号ISN不相同?
防止历史报文被新连接所接受,产生数据错乱。MTU和MSS的区别
MTU:Maximum Transmission Unit,IP网络包的最大长度
MSS:Maximum Segment Size,TCP数据部分的最大长度
当IP层有一个超过MTU的数据包要发送,就会将这个包分片,每个片都小于MTU。
在IP层分片的缺陷:当分片的一部分丢失时,由于IP层没有超时重传机制,需要重传所有的分片。
TCP协议为了解决这个缺陷,设计了MSS,在建立连接前协商好MSS值的大小,使得MSS加上TCP首部和IP首部的大小不大于MTU,这样发送数据超过MSS时就会先分为多个不大于MSS的TCP数据包,这样超时数据重新传送时也是以MSS为单位,避免了重传整个数据段。
TCP连接的断开
TCP四次挥手

- 初始都为ESTABLISHED状态;
- 客户端想要断开连接,发送FIN报文,进入FIN_WAIT_1状态;
- 服务端收到FIN报文后,进入CLOSE_WAIT状态,并应答ACK报文;
- 客户端收到ACK后进入FIN_WAIT_2状态;
- 服务端处理完剩余数据后,发送FIN报文,进入LAST_ACK状态;
- 客户端收到FIN报文后,应答ACK报文,进入TIME_WAIT状态,再进过2MSL后进入CLOSE转态;
- 服务端收到ACK后进入CLOSE状态;
为什么需要四次挥手?
因为被动关闭一方收到FIN报文时可能还有数据没处理和发送完,所以被动关闭一方的ACK和FIN需要分开发送,所以需要比三次握手多一次。为什么需要TIME_WAIT状态?
TIME_WAIT状态的主要目的有两个:
- 主要目的是避免使用相同四元组的历史数据包被新连接接收导致数据错乱;
- 其次是为了保证被动关闭的一端收到它自己发出的FIN报文的ACK,避免被动关闭一方一直处于LAST_ACK状态;
来看一下《UNIX网络编程》在描述为什么需要TIME_WAIT状态时的一段话:
Since the duration of the TIME_WAIT state is twice the MSL, this allows MSL seconds for packet in one direction to be lost, and another MSL seconds for the reply to be lost. By enforcing this rule, we are guaranteed that when we successfully establish a TCP connecton, all old duplicates from previous incarnations of the connection have expired in the network.
这段文字说明了TIME_WAIT状态持续2MSL的时间可以让一个TCP连接的两端发出的报文都从网络中消失,从而保证下一个使用了相同四元组的tcp连接不会被上一个连接的报文所干扰。
为什么在发送了最后一个ACK报文之后需要等待2MSL时长就可以确保没有任何属于当前连接的报文还存活于网络之中?(前提是在这2MSL时间内不再收到对方的FIN报文,但即使收到了对端的FIN报文也并不影响我们的讨论,因为如果收到FIN则会回复ACK并重新计时)
设想有一个处于拆链过程中的TCP连接,两端分别是A和B,A是主动关闭连接的一端,A刚刚向对端发送了FIN报文的ACK,此时正处于TIME_WAIT状态;而B是被动关闭的一端,此时正处于LAST_ACK状态,在收到最后一个ACK之前它会一直重传FIN报文直至超时。随着时间的流逝,A发送给B的ACK报文将会有两种结局:
- ACK报文在网络中丢失,这种情况下,B会重传FIN;
- 重传成功,A收到FIN会重置TIME_WAIT状态并重新发送ACK,相当于又回到了初始状态,不用考虑;
- 重传失败,相当于主动关闭一方的ACK丢失后,被动关闭一方的重传FIN又丢失了,这种概率过小,忽略它比较划算;
- ACK报文被B接收到。假设A发送了ACK报文后过了一段时间t之后B才收到该ACK,则有 0 < t <= MSL。因为A并不知道它发送出去的ACK要多久对方才能收到,所以A至少要维持MSL时长的TIME_WAIT状态才能保证它的ACK从网络中消失。同时处于LAST_ACK状态的B因为收到了ACK,所以它直接就进入了CLOSED状态,而不会向网络发送任何报文。所以晃眼一看,A只需要等待1个MSL就够了,但仔细想一下其实1个MSL是不行的,因为在B收到ACK前的一刹那,B可能因为没收到ACK而重传了一个FIN报文,这个FIN报文要从网络中消失最多还需要一个MSL时长,所以A还需要多等一个MSL。
综上所述,TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二个MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。
常见问题
为什么TCP是流式传输?
因为TCP需要保证可靠传输,根据缓冲区大小将多个数据包一起发送或接收可以在保证可靠性的同时大大减少确认报文的数量。
