TCP 全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传
输进行一个详细的控制。
对于TCP的学习主要就是要知道TCP协议报头之中各个字段的作用
除了数据之外总共报头加起来是20个字节
16位源端口号与目的端口号
这是最容易理解的两个字段,见名知意。
源端口:发送数据方的端口号。
目的端口:需要接收数据方的端口号
确认应答机制
TCP是有确认应答机制,如果服务端向客户端发送数据时,当客户端成功接收时需要向服务端发送确认的报文。如果服务端在一段时间后没有收到客户端发来的确认应答,就会触发超时重传。将数据重新再发一次给客户端。
确认应答一方会将报头中标记位标记位ACK
32位序号
理解这个字段我们首先要知道TCP是字节流的传输协议,所以在每次发送数据时是将每个字节拷贝给对方的,序号就需要将发送的起始字段写在序号当中
32位确认序号
确认序号就是接收方向发送方发送的确认收到的信息,用于确认资源是否收到
4位首部长度
TCP 首部长度字段用于指示 TCP 首部的实际长度。由于 TCP 首部存在一些可选字段,这些可选字段的长度是可变的,所以需要一个专门的字段来明确首部的总长度,以便接收方能够准确地从接收到的 TCP 报文段中提取出数据部分。
作用
- 准确解析报文:接收方通过读取这个 4 位首部长度字段的值,能够知道 TCP 首部在整个报文段中所占的字节数,从而正确地划分出首部和数据的边界,进而对报文进行准确解析和处理。
- 适应可变选项:TCP 协议为了满足不同的应用需求,定义了一些可选字段,如窗口扩大选项、时间戳选项等。这些可选字段的使用会使 TCP 首部长度发生变化,首部长度字段的存在使得接收方能够适应这种变化,正确处理包含不同可选字段的 TCP 报文。
标记位
主要用于接收方进行识别发送的是那种消息,主要是有6个标记位
TCP(传输控制协议)的 6 个标记位分别是 URG、ACK、PSH、RST、SYN、FIN,以下是对它们的详细介绍:
- URG(Urgent)
- 含义:紧急指针标志位。当 URG=1 时,表示该数据包中有紧急数据,需要尽快处理。紧急数据的位置由紧急指针字段指出。
- 作用:用于通知接收方,此数据包中的某些数据是紧急的,需要优先处理,而不是按照常规的顺序处理数据。比如在远程控制等场景中,用户可能需要立即发送一些紧急指令给服务器,这时就可以设置 URG 标志位。
- ACK(Acknowledgment)
- 含义:确认标志位。当 ACK=1 时,确认号字段才有效,表示接收方已经成功收到了发送方发送的数据,并希望发送方继续发送下一个数据。在连接建立后,所有的数据传输过程中,ACK 通常都为 1。
- 作用:在 TCP 可靠传输机制中起着关键作用,它允许接收方告诉发送方哪些数据已经被成功接收,发送方可以根据这些确认信息来决定是否重传数据,从而保证数据的可靠传输。
- PSH(Push)
- 含义:推送标志位。当 PSH=1 时,提示接收方应该尽快将数据交付给应用层,而不是等待缓冲区填满后再交付。
- 作用:常用于实时性要求较高的应用场景,如实时视频流、音频流等,让接收方尽快将接收到的数据传递给应用程序进行处理,以减少数据在缓冲区中的等待时间,提高数据处理的实时性。
- RST(Reset)
- 含义:复位标志位。当 RST=1 时,表示 TCP 连接出现了严重错误,需要重新建立连接。比如当接收方收到一个无法识别的数据包,或者连接出现异常时,就会发送 RST=1 的数据包给对方,要求重置连接。
- 作用:用于在 TCP 连接出现故障或异常时,快速终止当前连接,避免数据混乱和错误的进一步扩散。例如,当服务器发现客户端的请求存在严重错误,无法继续处理时,就可以发送 RST 包来中断连接。
- SYN(Synchronize)
- 含义:同步标志位。在 TCP 连接建立时,SYN 用于发起连接请求。当 SYN=1,ACK=0 时,表示这是一个连接请求数据包,发送方请求与接收方建立连接;当 SYN=1,ACK=1 时,表示这是对连接请求的响应数据包,同意建立连接。
- 作用:是 TCP 三次握手过程中的重要标志位,用于实现客户端和服务器之间的连接初始化和同步,确保双方能够正确地建立连接,并协商连接的参数。
- FIN(Finish)
- 含义:结束标志位。当 FIN=1 时,表示发送方已经没有数据要发送了,请求关闭连接。但在关闭连接之前,发送方仍然可以接收对方发送的数据。
- 作用:用于在数据传输完成后,有序地关闭 TCP 连接。发送方发送 FIN 包告知接收方数据传输结束,接收方收到 FIN 包后,会发送 ACK 确认,并在自己的数据传输完成后,也发送 FIN 包给发送方,完成连接的关闭过程。
连接管理机制
三次握手
三次握手其实就是客户端与服务端建立连接的方式
一方向另一方请求连接时会向对方发送一个空数据的报文,且标记位会标记SYN,
TCP 三次握手是建立 TCP 连接的过程,具体如下:
-
第一次握手
- 客户端行为:客户端向服务器发送一个带有 SYN(同步)标志位的 TCP 数据包,该数据包中还会包含一个随机生成的序列号(Sequence Number),记为 seq=x。这个序列号用于标识客户端发送的数据字节流中的位置,后续的数据传输都将基于这个序列号进行编号和确认。
- 含义:客户端向服务器表明自己想要建立连接,并告知服务器自己初始的序列号。此时客户端进入 SYN_SENT 状态,等待服务器的响应。
-
第二次握手
- 服务器行为:服务器接收到客户端发送的 SYN 包后,会检查该包的合法性。如果合法,服务器会向客户端发送一个带有 SYN 和 ACK 标志位的 TCP 数据包。在这个数据包中,服务器会将客户端的序列号 seq=x 加 1 作为确认号(Acknowledgment Number),即 ack=x+1,表示服务器已经成功收到客户端的 SYN 请求,并期望客户端发送序列号为 x+1 的数据。同时,服务器也会生成一个自己的随机序列号 seq=y,用于标识服务器发送的数据字节流。
- 含义:服务器向客户端确认已经收到了客户端的连接请求,并发送自己的初始序列号,同时告诉客户端可以发送下一个序列号的数据了。此时服务器进入 SYN_RCVD 状态。
-
第三次握手
- 客户端行为:客户端接收到服务器发送的 SYN+ACK 包后,会检查确认号是否正确以及 SYN 标志位是否为 1 等信息。如果确认无误,客户端会向服务器发送一个带有 ACK 标志位的 TCP 数据包,将服务器的序列号 seq=y 加 1 作为确认号 ack=y+1,发送给服务器,同时将自己的序列号 seq=x+1 发送给服务器。
- 含义:客户端向服务器表示已经收到了服务器的连接确认,并且双方的序列号和确认号都已经协商成功,此时客户端进入 ESTABLISHED 状态。服务器收到这个 ACK 包后,也会进入 ESTABLISHED 状态,至此,TCP 连接建立成功,双方可以开始进行数据传输。
四次挥手
四次挥手就是双方进行关闭连接的方法
-
第一次挥手
- 主动关闭方行为:主动关闭连接的一方(假设为客户端)发送一个带有 FIN 标志位的 TCP 数据包,表示自己已经没有数据要发送了,请求关闭连接。此时客户端会停止向服务器发送数据,但仍然可以接收服务器发送的数据。该数据包中包含客户端当前的序列号 seq=u。
- 含义:客户端告知服务器,自己的数据发送完毕,准备关闭连接。客户端发送 FIN 包后,进入 FIN_WAIT_1 状态,等待服务器的响应。
-
第二次挥手
- 被动关闭方行为:服务器接收到客户端发送的 FIN 包后,会发送一个带有 ACK 标志位的确认数据包给客户端,确认号 ack=u+1,序列号为服务器当前的 seq=v,表示已经收到客户端的关闭请求。此时服务器仍然可以继续向客户端发送剩余的数据。
- 含义:服务器向客户端确认已经收到关闭请求。客户端收到这个 ACK 包后,进入 FIN_WAIT_2 状态,等待服务器发送 FIN 包。服务器在发送完剩余的数据后,才会发送 FIN 包来关闭连接。
-
第三次挥手
- 被动关闭方行为:当服务器完成所有数据的发送后,会向客户端发送一个带有 FIN 标志位的 TCP 数据包,请求关闭连接,序列号为 seq=w(如果服务器在发送完数据后没有产生新的数据,那么 w=v+1;如果有新的数据产生,w 就是新数据的序列号)。
- 含义:服务器告知客户端,自己的数据也发送完毕,准备关闭连接。服务器发送 FIN 包后,进入 LAST_ACK 状态,等待客户端的最后一次确认。
-
第四次挥手
- 主动关闭方行为:客户端接收到服务器发送的 FIN 包后,会发送一个带有 ACK 标志位的确认数据包给服务器,确认号 ack=w+1,序列号为客户端当前的 seq=u+1。然后客户端进入 TIME_WAIT 状态,等待一段时间(通常为 2 倍的 MSL,即最长报文段寿命)后,才会真正关闭连接。
- 含义:客户端向服务器确认已经收到服务器的关闭请求,双方都同意关闭连接。服务器收到这个 ACK 包后,就会关闭连接。客户端在 TIME_WAIT 状态等待的目的是为了确保最后一个 ACK 包能够被服务器正确接收,如果在这个时间内没有收到服务器的重传请求,就可以认为连接已经成功关闭,资源可以被释放。
TIME_WAITZ状态
现在做一个测试,首先启动 server,然后启动 client,然后用 Ctrl-C 使 server 终止,这时马上再运行 server, 结果是
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 的时间请读者参考 UNP 2.7 节;
想一想, 为什么是 TIME_WAIT 的时间是 2MSL?
• MSL 是 TCP 报文的最大生存时间, 因此 TIME_WAIT 持续存在 2MSL 的话
• 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
• 同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失, 那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然可以重发 LAST_ACK);解决 TIME_WAIT 状态引起的 bind 失败的方法(作业)。
解决 TIME_WAIT 状态引起的 bind 失败的方法
在 server 的 TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
• 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
• 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量 TIME_WAIT 连接.
• 由于我们的请求量很大, 就可能导致 TIME_WAIT 的连接数很多, 每个连接都会占用一个通信五元组(源 ip, 源端口, 目的 ip, 目的端口, 协议). 其中服务器的 ip 和端口和协议是固定的. 如果新来的客户端连接的 ip 和端口号和 TIME_WAIT 占用的链接重复了, 就会出现问题.
使用 setsockopt()设置 socket 描述符的 选项 SO_REUSEADDR 为 1, 表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符
CLOSE_WAIT 状态
如何才会出现CLOSE_WAIT现象呢?
首先我们要知道,关闭连接时,先关闭文件描述法就是上图的左边一方,后关闭文件描述符是上图的右边。我们暂且将左边作为客户端,右边作为服务端。
当要关闭连接时,首先需要关闭建立的文件描述符,客户端关闭后,让服务端不关闭文件描述符,服务端就会进入FIN_WAIT_2,服务端就会进入CLOSE_WAIT。
验证:
首先先把服务端的关闭文件描述符代码注释,不让关闭文件。
让双方正常连接
关闭客户端出现close_wait现象
过一段时间FIN_WAIT_2消失
其实这也是一种资源泄露,服务端创建TCP的连接会一直存在,直到服务器被关闭。总的来说这就是一个bug,在写代码时一定要注意关闭我们创建的文件。
滑动窗口与窗口大小
如何知道向对方发送多少字节的数据呢?这时候滑动窗口就起到作用了,滑动窗口是创建TCP套接字中管理一块空间与接收缓冲区相关联,用于记录还可以接收多少字节的数据。
所以发送数据时一定是先发送到滑动窗口再由滑动窗口拷贝到接收缓冲区,滑动窗口的大小不是固定的,它受到多种因素的影响而动态变化。
讨论了确认应答策略, 对每一个发送的数据段, 都要给一个 ACK 确认应答. 收到 ACK 后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是 4000 个字节(四个段)。
• 发送前四个段的时候, 不需要等待任何 ACK, 直接发送;
• 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
• 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
• 窗口越大, 则网络的吞吐率就越高;
丢包策略
如果一次性发送这么多数据,丢包的概率就会大大增加,为了应对丢包问题TCP协议都采用了这种非常高效的做法
比如
服务器知道自己发送的数据,当对方发来的确认应答中缺少了4000确认序号,它就知道3001数据丢包了,就会再发一次,直到收到4000确认序号。
流量控制
收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
• 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过 ACK 端通知发送端;
• 窗口大小字段越大, 说明网络的吞吐量越高;
• 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
• 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
• 如果接收端缓冲区满了, 就会将窗口置为 0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
接收端如何把窗口大小告诉发送端呢? 回忆我们的 TCP 首部中, 有一个 16 位窗口字段,就是存放了窗口大小信息;
那么问题来了, 16 位数字最大表示 65535, 那么 TCP 窗口最大就是 65535 字节么?实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是 窗口字段的值左移 M 位;计算机中左移其实就是扩大2的M次方。
拥塞控制
虽然 TCP 有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据, 是很有可能引起雪上加霜的.
TCP 引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
发送开始的时候, 定义拥塞窗口大小为 1;
• 每次收到一个 ACK 应答, 拥塞窗口加 1;
• 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.
• 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
• 此处引入一个叫做慢启动的阈值
• 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长比
当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回 1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当 TCP 通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是 TCP 协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
延迟应答
我们知道,接收缓冲区的数据是从滑动窗口中来的,当数据到了滑动窗口中需要过一段时间将数据拷贝到接收缓冲区,而延迟应答底层思想就是等数据多拷贝到接收缓冲区后,让滑动窗口有更多空间接收数据,此时再发送ack,接收的数据就会增大,进而提高效率。
• 假设接收端缓冲区为 1M. 一次收到了 500K 的数据; 如果立刻应答, 返回的窗口就是 500K;
• 但实际上可能处理端处理的速度很快, 10ms 之内就把 500K 数据从缓冲区消费掉了;
• 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
• 如果接收端稍微等一会再应答, 比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是 1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
• 数量限制: 每隔 N 个包就应答一次;
• 时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般 N 取 2, 超时时间取 200ms;
捎带应答
捎带应答就是在ack时不仅仅是ack,而且携带要发送的数据。这也是一种提高效率的方法
粘包问题
当发送方连续发送多个数据包时,接收方可能会收到粘在一起的数据,也就是多个原本独立的数据包被合并成一个数据包接收,或者一个数据包被拆分成多个部分与其他数据包混合接收,这种现象就被称为粘包问题。简单来说就是找不到边界了。
最简单的做法就是在发送的资源前面添加一个数据长度字段。