TCP协议
# 格式

# 序号和应答确认序号
序号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来标识数据段本身,解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
序号和确认序号是确认应答机制的数据化表示,确认应答机制就是由序号和确认序号来保证的。
# 窗口大小
传输层 TCP 协议中,发送端和接收端都有发送缓冲区和接收缓冲区。当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么接收端的接收缓冲区会满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。所以发送端怎么知道发送数据量是合适的?我们知道对方接收缓冲区剩余空间大小即可。
所以,16 位窗口大小当中填的是自身接收缓冲区中剩余空间的大小。接收端在对发送端发来的数据进行响应时,就可以通过 16 位窗口大小告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度。
# 标志位
SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。只有在连接建立阶段,SYN 才被设置,正常通信时 SYN 不会被设置
FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段
ACK:该位为 1 时,确认应答的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力
PSH:该位为 1 时,告知接收端尽快将接收缓冲区当中的数据交付给上层
URG:该位为 1 时,表示该 TCP 报文段中包含了紧急数据(发送方应用程序向接收方应用程序紧急地发送一些重要的、需要优先处理的控制信息),配合紧急指针使用,其中
紧急数据末尾序号 = 本报文段序号 + 紧急指针值RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接重新建立连接
# 载荷分离和交付上层
如何报头和有效载荷分离?
tcp 协议标准长度为 20 字节,先读取 20 字节,转换为结构化数据提取标准报头中的 4 位首部长度 x,后续报头的剩余大小 = 4 * x - 20,将报头读取完毕后,剩下的就是有效载荷了。如何交付上层?
应用层的每一个网络进程都必须绑定一个端口号(服务端进程必须显示绑定一个端口号,客户端进程由系统分配一个端口号)。 而 TCP 的报头中涵盖了目的端口号,因此 TCP 可以提取出报头中的目的端口号,找到对应的应用层进程(哈希的方式维护了端口号与进程 ID 之间的映射关系),进而将有效载荷交给对应的应用层进程进行处理。
# 特点
- 有连接:建立连接,间接地保证可靠性
- 可靠:确认应答机制、超时重传机制、三次握手四次挥手等保证可靠性
- 面向字节流:
TCP 在内核中创建一个发送缓冲区和一个接收缓冲区; 调用 write 时, 数据会先写入发送缓冲区中(如果发送的字节数太长, 会被拆分成多个 TCP 的数据包发出; 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出 去);调用 read 从接收缓冲区拿数据。
TCP 的一个连接, 既有发送缓冲区, 也有接收缓冲区, 既可以读数据, 也可以写数据,叫做全双工。
# 确认应答机制
接收方在成功收到数据后,必须向发送方返回一个确认(ACK)信号。发送方只有在收到这个确认信号后,才会认为数据已经成功送达,从而发送下一个数据。确认应答机制不是保证双方通信的全部消息的可靠性,而是通过收到对方的应答消息,来保证自己历史发送的消息被对方可靠的收到了。
# 超时重传机制
发送方在发送一个数据包后启动一个定时器。如果在这个定时器超时之前,没有收到接收方返回的对应确认应答(ACK),发送方就认为这个数据包已经丢失,并会重新发送它。
- 主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B
- 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发
TCP 使用一个动态算法来实时估算最大超时时间,其主要依据是 RTT,即数据包从发送到收到 ACK 的总耗时。
# 连接管理机制
三次握手&四次挥手:图源 (opens new window) 
# 三次握手
三次握手的主要目的是:同步序列号、分配资源、建立可靠的双向通信通道。
注意:连接建立是由操作系统管理起来,先描述后组织的,需要成本的;三次握手这个过程,本质上就是一个双方共同确认、达成共识的协议过程。
为什么是三次?
验证全双工信道通畅的最小成本就是 3 次握手
为什么不是两次或者四次?
// todo
# 四次挥手
四次挥手的核心目的是:双方共同确认,安全可靠地关闭一个双向通信通道。断开连接要征得双方的同意,发送FIN表示不给对方发数据(用户的数据)了,要断开连接了。
四次挥手的状态变化
主动断开的一方,四次挥手完成,要维持一段时间的TIME_WAIT状态;被动断开的一方,两次回收完成的状态是CLOSE_WAIT状态
主动断开的一方为什么要维持一段时间的
TIME_WAIT状态?
维持的时间一般是2 * MSL(Maximum Segment Lifetime,报文最大生存时间),再进入 CLOSED 状态。
原因在于如果第四次挥手的报文丢包了,主动方在一段时间内仍然能够接收被动方重发的FIN报文并对其进行响应,能够较大概率保证最后一个ACK被被动方收到;另外,主动方四次挥手后进入TIME_WAIT状态,还可以保证滞留在网络中的报文消散。
例如:当服务器先断开,即主动断开,再重启该端口时会bind err,因为有TIME_WAIT维持时间,可以使用setsockopt()设置 socket 描述符的选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符(端口复用)
int setsockopt(int sockFd, int level, int optname, const void *optval, socklen_t optlen);
# 流量控制机制

TCP 支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制机制。接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "16 位窗口大小" 字段, 通过 ACK 端通知发送端;如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。
# 滑动窗口
为了高效率(如上图),而非串行地发送和确认,
发送端发送数据之后在没有接收到确认确认之前,会保存在发送缓存区中滑动窗口部分,此时发送缓冲区分为了四部分:

// todo
滑动窗口最开始的大小怎么设定?
滑动窗口大小要小于等于对方的接受能力大小,即win_start = ack_seq, win_end = win_start + tcp_win
接收应答时,如果不是左侧报文的确认,而是中间和结尾的怎么办?
有以下两种情况:
- 数据没丢,应答确认没丢
因为应答确认的定义就是应答序号之前的丢被接收到,所以没有影响 - 数据丢了
根据快速重传(Fast Retransmit) 算法,如果发送方连续收到 3 个或 3 个以上对同一个序列号的重复 ACK,就会推断这个期望的数据包已经丢失,发送方会立即超时重传认为丢失的报文段,从而大大提高效率。
# 拥塞控制
TCP 不仅考虑了主机的问题,而且还考虑了网络的问题,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,从而恶循环。
慢启动
// png
# 延迟应答&捎带应答
- 延迟应答:在发送方和接收方进行通信时,接收方的接收缓冲区收到了来自发送方的一批报文(滑动窗口机制),接收方收到第一个数据时,不会立刻进行 ACK,会延迟一会再进行发送!这个延迟时间不会超过超时重传的时间。延迟一会可能是更大的接收窗口,从而提高效率。
- 捎带应答:TCP 可以将确认信息(ACK)和有效数据合并到同一个报文中一起发送,而不是单独发送一个纯 ACK 包。
# 基于 TCP 的应用层协议
HTTP、HTTPS、SSH、Telnet、SMTP 等