TCP 协议详解

本博客 hjy-xh,转载请申明出处

除了三次握手,四次挥手,TCP 还有很多细节需要掌握,知其然知其所以然

  • TCP 为什么可靠?————连接确认!关闭确认!收到数据确认!各种确认!!
  • 因为网络或其他原因,对方收不到数据怎么办?–超时重试
  • 网络情况千变万化,超时时间怎么确定?–根据RTT动态计算
  • 反反复复,不厌其烦的重试,导致网络拥塞怎么办?—慢启动,拥塞避免,快速重传,快速恢复
  • 发送速度和接收速度不匹配怎么办?–滑动窗口
  • 滑动窗口滑的过程中,他一直告诉我处理不过来了,不让传数据了怎么办?–ZWP
  • 滑动窗口滑的过程中,他处理得慢,就理所当然的每次让我发很少的数据,导致网络利用率很低怎么办?—Nagle

基本概念

TCP 连接的特点

  • 面向连接
  • 可靠传输
  • 提供字节流服务

TCP 如何保证可靠性

  • 分块传输:数据被分割成最合适的数据块(UDP的数据长度不变)
  • 等待确认:通过定时器等待接收端发送确认请求,收不到确认则重发
  • 确认回复:收到确认后发送确认回复
  • 数据校验:保持首部和数据的校验和,检测数据传输过程有无变化
  • 乱序重排:接收端能重排序数据,以正确的顺序交给应用端
  • 重复丢弃:接收端能丢弃重复的数据包
  • 流量缓冲:两端有固定大小的缓冲区(滑动窗口),防止速度不匹配造成数据丢失

首部格式

TCP 数据被封装在 IP 数据报中

  • TCP 首部数据通常包含20个字节(不包括任选字段)
  • 第1-2两个字节:源端口号
  • 第3-4两个字节:目的端口号

    源端口号+ip首部中的源ip地址+目的端口号+ip首部中的目的ip地址,唯一的确定了一个 TCP 连接,对应编码级别的socket

  • 第5-8四个字节:32位序号。TCP 提供全双工服务,两端都有各自的序号,编号:解决网络包乱序的问题

    序号如何生成:TCP 基于时钟生成一个序号,每4微秒加一,到2^32-1时又从0开始(不能是固定写死的,否则断网重连时序号重复使用会乱套)

  • 第9-12四个字节:32位确认序列号。上次成功收到数据字节序号加1,ack为1才有效,确认号:解决丢包的问题
  • 第13位字节:首部长度。因为任选字段长度可变
  • 后面6bite:保留
  • 随后6bite:标识位。控制各种状态
    • URG:为1时,表示紧急指针有效
    • ACK:确认标识,连接建立成功后,总为1。为1时确认号有效
    • PSH:接收方应尽快把这个报文交给应用层
    • RST:复位标识,重建连接
    • SYN:建立新连接时,该位为0
    • FIN:关闭连接标识
  • 第15-16两个字节:窗口大小。接收端期望接收的字节数。解决流量控制的问题
  • 第17-18两个字节:校验和。由发送端计算和存储,由接收端校验。解决数据正确性问题
  • 第19-20两个字节:紧急指针

连接的建立与释放

三次握手、四次挥手

TCP 连接是全双工的,数据在两个方向上能同时传递,这是决定握手和挥手次数一个基本原因

Q: 为什么是三次握手?

A: 要确保双方,同时能发数据和收数据

  • 第一次握手:证明了发送方能发数据
  • 第二次握手:ack确保了接收方能收数据,syn确保了接收方能发数据
  • 第三次握手:确保了发送方能收数据
  • 实际上是四个维度的信息交换,不过中间两步合并为一次握手了,四次握手浪费,两次握手不能保证“双方同时具备收发功能”

Q: 为什么是四次挥手

A: TCP 支持半关闭(发送一方结束发送还能接收数据的功能),每个方向都要单独关闭,收到关闭通知需要发送确认回复

Q: 为什么要支持半关闭

A: 原因如下:

  • 客户端需要通知服务端,它的数据已经传输完毕,同时仍要接收来自服务端的数据
  • 使用半关闭的单连接效率要比使用两个TCP 连接更好

TCP 连接和关闭对应的状态

  • 服务端等待客户端连接时,处于Listen监听状态
  • 客户端主动打开请求,发送SYN时处于SYN_SENT发送状态
  • 客户端收到syn和ack,并回复ack时,处与Established状态等待发送报文
  • 服务端收到ack确认后,也处于Established状态等待发送报文
  • 客户端发送fin后,处于fin_wait_1状态
  • 服务端收到fin并发送ack时,处于close_wait状态
  • 客户端收到ack确认后,处于fin_wait_2状态
  • 服务端发送fin后,处于last_ack状态
  • 客户端收到fin后发送ack,处于time_wait状态
  • 服务端收到ack后,处于closed状态

time_wait状态

  • 也称为2MSL等待状态,MSL:Maximum Segment LifetIme,报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间,根据不同的tcp实现自行设定(常用值为30s,1min,2min。linux一般为30s)
  • 主动关闭的一方发送最后一个ack所处的状态
  • 这个状态必须维持2MSL等待时间
    • 预留足够的时间给接收端收ack。设想一个场景,最后这个ack丢失了,接收方没有收到,这时候接收方会重新发送fin给发送方,这个等待时间就是为了防止这种情况发生,让发送方重新发送ack
    • 在这2MSL等待时间内,该连接(socket,ip+port)将不能被使用

复位报文段

一个报文段从源地址发往目的地址,只要出现错误,都会发出复位的报文段,首部字段的RST是用于“复位”的。这些错误包括以下情况:

  • 端口没有在监听
  • 异常中止:通过发送RST而不是fin来中止连接

数据的传输

TCP 传输的数据分类

TCP 处理的数据包括两类,有不同的特点,需要不同的传输技术:

  • 交互数据传输:量小,报文段为微小分组,大量微小分组,在广域网传输会增加拥堵的出现
  • 成块数据传输:量大,报文段常常满

交互数据的传输技术

  • 经受时延的确认
    • TCP 收到数据后,并不会立马发送ack确认,会与将要发送的数据一起发送,以减少开销
  • Nagle算法
    • 解决什么问题:微小分组导致在广域网出现的拥堵问题
    • 核心:减少了通过广域网传输的小分组数目
    • 原理:要求一个 TCP 连接上最多只能有一个未被确认的未完成的分组,该分组的确认到达之前,不能发送其他分组。TCP 收集这些分组,确认到来之前以一个分组的形式发出去
    • 优点:自适应。确认到达的快,数据发送越快。确认慢,发送更少的组
    • 使用注意:局域网很少使用该算法。且有些特殊场景需要禁用该算法

成块数据传输

主要使用滑动窗口协议,该协议解决了发送方和接收方速率不匹配时,保证可靠传输和包乱序的问题

机制:

  • 接收方根据目前缓冲区大小,通知发送方目前能接收的最大值
  • 发送方根据接收方的处理能力来发送数据

接收方给发送方的这个值成为窗口大小

tcp缓冲区的数据结构如下:

拥塞窗口是为了防止出现发送方发送速度过快,导致中转路由器拥堵的问题

机制:

  • 发送方增加一个拥塞窗口(cwnd),每次收到ack,窗口值加1,
  • 取拥塞窗口和接收方发来的窗口大小取最小值发送

这个机制存在的问题:

  • 零窗口
    当接收方处理速度慢,发送方发送速度快,窗口大小就慢慢被调为0

  • 糊涂窗口综合征
    接收方太忙,取不完数据,导致发送方越来越小,这是因为数据比tcp和ip头小太多,网络利用率太低

    此时避免对小的窗口大小做响应

    • 发送端:使用前面说到的Nagle算法
    • 接收端:窗口大小小于某个值,直接ack(0),阻止发送数据,等到窗口变大后再发

参考

一文彻底搞懂 TCP三次握手、四次挥手过程及原理