前言


一、TCP面向字节流

  • 创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区:
  • 调用write时, 数据会先写入发送缓冲区中;
  • 调如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  • 调如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  • 调接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 调然后应用程序可以调用read从接收缓冲区拿数据;
  • 调另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工;

*由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

二、TCP粘包问题

1.什么是TCP粘包问题

因为TCP协议是面向字节流的,上一次和下一次的数据没有明显的数据边界,即TCP通过recv函数接收数据的时候不会区分此条数据是第一次发送的还是第二次发送的,而是直接按规定的大小进行读取,因此有可能读取到不完整的数据或粘在一起的数据。

  • 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包。
  • 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段。
  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中。
  • 站在应用层的角度, 看到的只是一串连续的字节数据。
  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包。
  • [ ] 粘包问题图解如下:

    如上图所示机器A向机器B发送的是1+1和2+2,但机器B在接收数据的时候无法区分第一条和第二条数据,所以将第一条和第二条数据粘在一起接收,即接收到1+12+2,这种情况就是数据粘包问题。

2.TCP粘包问题的解决办法

归根结底就是一句话, 明确两个包之间的边界。
①使用定长的字节发送(不推荐):调它的本质是设置固定的数据收发的字节大小,现实中我们发送数据有长有短,根据每条数据发送的长短给每条数据设置不同的收发长度显然不现实,所以此方法只适用于理论基础实现。
②在应用层当中,对应用程序产生的数据进行“包装”,即给应用数据加上头部描述信息,在应用数据的尾部加上分隔符。
eg:应用数据 = “aaabbbccc”
  应用数据 = 应用层描述符数据的头部+“aaabbbccc”+分隔符。

  • [ ] 思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
  • 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层,就有很明确的数据边界”。
  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。

三、TCP异常情况

①进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别。
②机器重启: 和进程终止的情况相同。
③机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放。

四、TCP协议

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

1.TCP协议段格式

  • 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去。
  • 32位序号/32位确认号: 后面详细讲。
  • 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60。
  • 6位标志位。
  • URG: 紧急指针是否有效
  • ACK: 确认号是否有效
  • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
  • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
  • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
  • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  • 16位窗口大小: 后面再说。
  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分。
  • 16位紧急指针: 标识哪部分数据是紧急数据。
  • 40字节头部选项: 暂时忽略。

2.确认应答(ACK)机制


TCP将每个字节的数据都进行了编号.,即为序列号:TCP报文按序到达。

每一个ACK都带有对应的确认序列号:意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。

  • [ ] TCP协议为什么要有序号:
  • 确保TCP报文按序到达
  • [ ] TCP协议为什么序号有两个,一个确认序号,一个序号:
  • 因为TCP是全双工通信,双方同时通信双方的序号不一样,确认序号是通信对方确认应答。

3.超时重传机制

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

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

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

4.连接管理机制

1、TCP三次握手

TCP协议的连接是面向连接,可靠传输,面向字节流的,而TCP之所以能保持可靠传输是因为三次握手建立和四次挥手断开连接。

1.从数据包名称和连接双方状态分析三次握手

首先我们通过数据包名称和连接双方的连接状态来了解三次握手的过程。

如下图所示:

客户端在发送SYN数据包后客户端的状态变为SYN_SENT,当服务端接收到客户端发送的SYN数据包后,服务端的状态变为SYN_RECV,当客户端接收到服务端的SYN数据包和ACK数据包后,客户端的状态变为ESTABLISHED,当服务端接收到客户端发送的ACK数据包时,服务端的状态变为ESTABLISHED,此时客户端和服务端已完成三次握手,即建立了双向连接。

  • [ ] 客户端状态转化:
  • [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段。
  • [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据。
  • [ ] 服务状态转化:
  • [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接。
  • [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文。
  • [ ] 为什么是三次握手?

2、TCP三次握手包序管理

1.抓网络数据包
  • 在windows平台下,可以使用wireshark软件来抓取网络数据包
  • 在linux平台下,使用 tcpdump 来抓取网络数据,TCP和UDP的包都可以使用此命令抓取

    1)万能公式:tcpdump -i any port [端口] -s 0 -w xxx.dat

    2)使用root用户进行抓包

    如上2)中的命令是对某个端口进行抓包,并将结果放入xxx.dat中,但我们不能直接对xxx.dat中的数据进行分析,需要借助windows平台下的wireshark软件来分析
2. 分析TCP网络数据包

我们使用上篇socket编程写的单进程的客户端和服务端的代码,使用tcpdump抓包分析三次握手的过程。

首先在root用户下输入 tcpdump -i any port 20000 -s 0 -w tcp.dat ,先进行抓包,再让客户端服务端程序跑起来,防止三次握手结束,没有抓到,让客户端服务端跑一会后终止程序,我们可以看到如下图所示:

可以看到一共抓到了84个数据包,并产生了一个tcp.dat文件,通过rz tcp.dat将tcp.dat传到windows下使用wireshark进行分析,如下图所示:

3. 分析TCP包序号


为什么TCP需要包序号?

本质上是为了维护可靠传输,客户端维护了一套序号,服务端也维护了一套序号

  • client–>server:消耗(seq)的是客户端维护的序号,服务端告诉客户端自己收到数据的时候,是确认(ACK)客户端的序号

    • server–>client:消耗(seq)的是服务端维护的序号,客户端告诉服务端自己收到数据的时候,是确认(ACK)服务端的序号。

观察如下图所示抓到的包,分析TCP包序号的变化:

分析如下图所示:

  • 纯ACK数据包不消耗序号
  • TCP数据也消耗序号,一个字节消耗一个序号
  • 确认序号ACK=消息发送方的序号+数据长度
  • 确认序号的作用是:告知消息发送方,期望下次发送数据从哪一个序号开始发送

    注意:TCP三次握手中,协商双方的其实序号并不一定是从0号序号开始,可以从任意位置开始,只要双方协商好就行。

    TCP可靠的原因就在于序号,丢了那个数据都可以知道,因为一个字节占一个序号。

3、TCP四次挥手


通过数据包名和连接双方的状态分析四次挥手的过程如下图所示:


数据传输完毕以后,双方都可以释放连接。在最开始的时候,客户端与服务器都是处于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状态。
  • [ ] 服务端状态转化:
  • [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT。
  • [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)。
  • [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接。

4、理解TIME_WAIT状态


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

  • TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
  • 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口。
  • [ ] 主动断开方为TIME_WAIT状态为什么要等待2MSL后才变为CLOSED状态呢?
当主动断开方为TIME_WAIT状态时,如果主动断开方发送的ACK数据包丢失,则过MSL后,被动连接方会重新发送FIN数据包,让主动断开连接方重新发送ACK数据包,若主动断开连接方MSL后将状态变为CLOSED则无法收到重发的FIN数据包,无法重新发送ACK数据包。

2MSL = 丢失的ACK的MSL + 重传的FIN的MSL

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

TIME_WAIT状态存在于主动断开连接方

如果服务端位于主动断开连接方,此时一定在四次握手的过程中拥有TIME_WAIT状态;即使当服务端进程已经结束了,但是服务端之前使用TCP协议针对针对的连接还是TIME_WAIT状态,换句话说,服务端之前绑定的端口还没有被网络协议栈的TCP协议释放掉,导致服务端无法快速的重启(端口已被站用问题)

解决办法:使用setsockopt函数,让端口重用
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:侦听套接字listen_fd
level:SOL_SOCKET 套接字选项
optname:SO_REUSEADDR 重用端口(只有当服务端的状态为TIME_WAIT时才能重用)
optval:1
optlen:opt的大小

5、理解 CLOSE_WAIT 状态

假设客户端主动断开连接,服务端被动断开连接。

1)客户端进程发出连接释放报文FIN=1,并且停止发送数据。此时,客户端进入FIN-WAIT1(终止等待1)状态。这时候客户端处于一个半关闭的状态,即客户端已经没有数据需要发送了,但是服务器若要发送数据,客户端依然需要接受。

2)服务器收到连接释放报文后,发送确认报文ACK=1。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。进入CLOSE_WAIT后说明服务器准备关闭连接。

3)客户端收到服务器的确认请求之后,此时客户端就进入了FIN-WAIT2(终止等待2)状态,等待服务器发送连接释放报文。(在这个之前还需要接受服务器发送的最后的数据)。

4)当服务器真正调用close关闭连接时, 会向客户端发送FIN=1, 此时服务器进入LAST_ACK(最后确认)状态, 等待客户端的最后一次ACK回复。

5)客户端收到服务器的链接释放报文之后,必须发出确认报文ACK=1。此时,客户端就进入了TIME-WAIT(时间等待)状态,等待用户关闭套接字。注意此时TCP链接还没有释放,必须经过2*MSL(报文最大生命周期)的时间后,当客户端撤销相应的TCP后,才进入CLOSED状态。

6)服务器只要收到客户端发出的确认,彻底关闭连接,立即就进行CLOSED状态,于是就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

小结: 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题。


总结

以上就是今天要讲的内容,本文详细介绍了传输层TCP协议使用,网络提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!

————————————————
版权声明:本文为CSDN博主「森明帮大于黑虎帮」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_44918090/article/details/119575085

最后修改:2022 年 04 月 04 日
如果觉得我的文章对你有用,请随意赞赏