传输层协议 TCP

TCP 协议

TCP 全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制

TCP 协议段格式

  • 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去
  • 32 位序号/32 位确认号
  • 4 位 TCP 报头长度: 表示该 TCP 头部有多少个 32 位 bit(有多少个 4 字节), 所以TCP 头部最大长度是 15 * 4 = 60
  • 6位标志位
  • 16 位窗口大小
  • 16 位校验和: 发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分.
  • 16 位紧急指针: 标识哪部分数据是紧急数据;
  • 40 字节头部选项

内核中对报头的描述

六个标志位

表示对应的报文类型,对应的六个比特位,要用就设置成1

  1. URG: 紧急指针是否有效
  2. ACK: 确认号是否有效
  3. PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
  4. RST: 通信异常对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
  5. **SYN:**请求建立连接; 我们把携带 SYN 标识的称为同步报文段
  6. **FIN:**通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段

ACK一般都是置1,因为确认需要是要用来表示自己在该确认序号之前的报文全部收到,通常确认序号要有效,第一次建立连接SYN然后等待对方发送SYN和ACK,然后对方再用ACK告诉自己收到了建立请求,双方都有意愿建立连接,FIN用于断开连接时告诉对方断开连接,双方都要断开,所以得先发ACK给对方,对方响应了然后发送ACK和FIN

三次握手能保证一定连接成功C端发送SYN对方能收到,对方会给予回应,自己也能收到,自己在告诉对方收到了,在这个过程中,C和S发了一次收了一次,能最小代价验证自己和对方全双工并且建立连接成功,如果中间一环出错就代表建立不成功无法通信

16位窗口大小

网络传输需要消耗资源,为了提高效率,对方接受不了那么多,就控制一下发送的量,16位窗口大小表示自己接受缓冲区的剩余空间的大小用来告诉对端自己的剩余大小

16位紧急指针

配合URG来用,数据发送到接收缓冲区后,它是一个字节流式的接受队列,提高优先级,优先处理,紧急指针表示偏移量位置存放紧急数据

确认应答(ACK)机制

TCP 将每个字节的数据都进行了编号. 即为序列号.

每一个 ACK 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.向对方发送数据的时候不知道对方有没有收到,对端会送一个带有确认需要的报文并将ACK标记为置1,表示该确认序号以前的报文全部收到

为什么又两个序号?

一个确认序号,一个序号,因为TCP是全双工,可以发也可以读,在接受的同时也可以发,我告诉对方收到了,我也可以发送别的数据,就像三次握手一样,本质是四次握手,但是有两次合并了,当成一条报文发送,这就是要有一对序号的原因,一个负责发带编号,一个用来确定自己有没有收到,可以捎带应答,我告诉你我收到了,我也可以携带数据给你

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收"的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine,thank you",那么这个时候 ACK 就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端

超时重传机制

主机 A 发送数据给 B 之后, 可能因为网络拥堵等原因, 数据无法到达主机 B;如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答, 就会进行重发,但是, 主机 A 未收到 B 发来的确认应答,也可能是因为 ACK 丢失了

因此主机 B 会收到很多重复数据. 那么 TCP 协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.那么, 如果超时的时间如何确定?

最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回",但是这个时间的长短, 随着网络环境的不同, 是有差异的.如果超时时间设的太长, 会影响整体的重传效率,如果超时时间设的太短, 有可能会频繁发送重复的包;

TCP 为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.

第一种可以用超时重传机制解决,但是第二种,因为确认序号的定义是在这个序号之前的报文全部收到,后续接收更大的序号也默认认为收到了

连接管理机制

在正常情况下, TCP 要经过三次握手建立连接, 四次挥手断开连接

三次握手,本质是四次握手,一方发起连接SYN,然后对端收到后,发送ACK应答,然后再发送SYN向对方请求连接,但是捎带应答使两个报文合成一条发送过去,最后对端收到货,返回ACK,表示同意连接,至此双方意愿达成可进通信
四次挥手,一方发起FIN向对方表示要断开连接,对方发送ACK表示接收到了断开请求,然后自己在向先有断开意愿的一方发送FIN断开请求,对端收到后,返回ACK表示同意,至此双方都断开对方的连接,结束通信

服务端状态转化:

  • **[CLOSED -> LISTEN]**服务器端调用 listen 后进入 LISTEN 状态, 等待客户端连接
  • [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送 SYN 确认报文.
  • [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED 状态, 可以进行读写数据了.
  • [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用 close), 服务器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT(谁先关闭谁就CLOSE_WAIT,依旧占用文件描述符没有被释放,要手动关,不然fd泄漏
  • **[CLOSE_WAIT -> LAST_ACK]**进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)
  • **[LAST_ACK -> CLOSED]**服务器收到了对 FIN 的 ACK, 彻底关闭连接.

客户端状态转化:

  • **[CLOSED -> SYN_SENT]**客户端调用 connect, 发送同步报文段
  • [SYN_SENT -> ESTABLISHED] connect 调用成功, 则进入 ESTABLISHED 状态, 开始读写数据
  • **[ESTABLISHED -> FIN_WAIT_1]**客户端主动调用 close 时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1
  • [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段
  • **[FIN_WAIT_2 -> TIME_WAIT]**客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出 LAST_ACK
  • [TIME_WAIT -> CLOSED] 客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入 CLOSED 状态.

下图是 TCP 状态转换的一个汇总:

较粗的虚线表示服务端的状态变化情况,较粗的实线表示客户端的状态变化情况,CLOSED 是一个假想的起始点, 不是真实状态

理解 TIME_WAIT 状态

现在做一个测试,首先启动 server,然后启动 client,然后用 Ctrl-C 使 server 终止,这时马上再运行 server, 结果是:

这是因为,虽然 server 的应用程序终止了,但 TCP 协议层的连接并没有完全断开,因此不能再次监 听同样的 server 端口. 我们用 netstat 命令查看一下:

TCP 协议规定,主动关闭连接的一方要处于 TIME_ WAIT 状态,等待两个MSL(maximum segment lifetime)的时间后才能回到 CLOSED 状态.我们使用 Ctrl-C 终止了 server, 所以 server 是主动关闭连接的一方, 在TIME_WAIT 期间仍然不能再次监听同样的 server 端口,MSL 在 RFC1122 中规定为两分钟,但是各操作系统的实现不同, 在 Centos7 上默认配置的值是60s,可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值;

想一想, 为什么是 TIME_WAIT 的时间是 2MSL?

MSL 是 TCP 报文的最大生存时间, 因此 TIME_WAIT 持续存在 2MSL 的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的),同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然可以重发 LAST_ACK)

解决 TIME_WAIT 状态引起的 bind 失败的方法(作业)

在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的

服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量 TIME_WAIT 连接.由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多, 每个连接都会占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议). 其中服务器的 ip 和端口和协议是固定的. 如果新来的客户端连接的 ip 和端口号和 TIME_WAIT 占用的链接重复了, 就会出现问题.

使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符

相关推荐
LuDvei几秒前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ7 分钟前
【Linux 系统】进程间的通信方式
linux·服务器
Abona9 分钟前
C语言嵌入式全栈Demo
linux·c语言·面试
心理之旅19 分钟前
高校文献检索系统
运维·服务器·容器
Lenyiin22 分钟前
Linux 基础IO
java·linux·服务器
The Chosen One98527 分钟前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
Hill_HUIL37 分钟前
学习日志22-静态路由
网络·学习
大佐不会说日语~1 小时前
使用Docker Compose 部署时网络冲突问题排查与解决
运维·网络·spring boot·docker·容器
Kira Skyler1 小时前
eBPF debugfs中的追踪点format实现原理
linux
2501_927773072 小时前
uboot挂载
linux·运维·服务器