TCP 协议全解析:握手、挥手、重传与流控的深度剖析
目录
TCP协议
TCP协议格式
序号与确认序号
保证可靠性:确认应答机制
TCP传递报文的通用情况
报文丢失怎么办?
为什么要有两套序号,一套序号不行吗?
结论
Reason
1. TCP 是全双工的
2. 确保可靠性和有序性
3. 处理重传和丢包
举个简单例子
窗口大小
六个标志位
重传机制
超时重传(Retransmission Timeout, RTO)
快重传
连接管理机制
三次握手
四次挥手
滑动窗口
流量控制
拥塞控制
延迟应答
捎带应答
面向字节流
粘包问题
TCP异常情况
TCP小结
TCP/UDP对比
TCP协议
TCP全称为"传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制;
TCP协议格式
TCP报头当中各个字段的含义如下:
- 源/目的端口号:表示数据是从哪个进程来,并发送到对端主机上的哪个进程。
- 32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段。
- 4位TCP报头长度:表示该TCP报头的长度,以4字节为单位。
- 6位保留字段:TCP报头中暂时未使用的6个比特位。
- 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段。
- 16位检验和:由发送端填充,采用CRC校验。接收端校验不通过,则认为接收到的数据有问题。(检验和包含TCP首部+TCP数据部分)
- 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用。
- 选项字段:TCP报头当中允许携带额外的选项字段,最多40字节。
TCP报头当中的6位标志位:
- URG:紧急指针是否有效。
- ACK:确认序号是否有效。
- PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走。
- RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段。
- SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段。
- FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段。
任何一层协议都必须解决两个问题:如何分离报头和有效载荷分离?如何交付有效载荷?
TCP协议中通过端口来指定要交付的进程。解析基础报头,再根据偏移量(首部长度)移动指针,从而可以将报头与有效载荷分离。
TCP如何将报头与有效载荷进行分离?
当TCP从底层获取到一个报文后,虽然TCP不知道报头的具体长度,但报文的前20个字节是TCP的基本报头,并且这20字节当中涵盖了4位的首部长度。
因此TCP是这样分离报头与有效载荷的:
- 当TCP获取到一个报文后,首先读取报文的前20个字节,并从中提取出4位的首部长度,此时便获得了TCP报头的大小size。
- 如果size的值大于20字节,则需要继续从报文当中读取size − 20字节的数据,这部分数据就是TCP报头当中的选项字段。
- 读取完TCP的基本报头和选项字段后,剩下的就是有效载荷了。
需要注意的是,TCP报头当中的4位首部长度描述的基本单位是4字节,这也恰好是报文的宽度。4位首部长度的取值范围是0000 ~ 1111,因此TCP报头最大长度为15 × 4 = 60 字节,因为基本报头的长度是20字节,所以报头中选项字段的长度最多是40字节。
如果TCP报头当中不携带选项字段,那么TCP报头的长度就是20字节,此时报头当中的4位首部长度的值就为20 ÷ 4 = 5 ,也就是0101
TCP如何决定将有效载荷交付给上层的哪一个协议?
应用层的每一个网络进程都必须绑定一个端口号。
- 服务端进程必须显式绑定一个端口号。
- 客户端进程由系统动态绑定一个端口号。
而TCP的报头中涵盖了目的端口号,因此TCP可以提取出报头中的目的端口号,找到对应的应用层进程,进而将有效载荷交给对应的应用层进程进行处理。
说明一下: 内核中用哈希的方式维护了端口号与进程ID之间的映射关系,因此传输层可以通过端口号快速找到其对应的进程ID,进而找到对应的应用层进程。
序号与确认序号
可靠性的本质
1、具有应答
在进行网络通信时,一方发出的数据后,它不能保证该数据能够成功被对端收到,因为数据在传输过程中可能会出现各种各样的错误,只有当收到对端主机发来的响应消息后,该主机才能保证上一次发送的数据被对端可靠的收到了,这就叫做真正的可靠
2、通信中最新的报文永远没有应答,它的可靠性无法保证
TCP要保证的是双方通信的可靠性,虽然此时主机A能够保证自己上一次发送的数据被主机B可靠的收到了,但主机B也需要保证自己发送给主机A的响应数据被主机A可靠的收到了。因此主机A在收到了主机B的响应消息后,还需要对该响应数据进行响应,但此时又需要保证主机A发送的响应数据的可靠性…,这样就陷入了一个死循环。
保证可靠性:确认应答机制
TCP传递报文的通用情况
32位序号
如果双方在进行数据通信时,只有收到了上一次发送数据的响应才能发下一个数据,那么此时双方的通信过程就是串行的,效率可想而知。
因此双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了,此时也就能保证这些报文被对方收到了。
但在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的。但报文有序也是可靠性的一种,因此TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的。
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号
- 比如现在发送端要发送3000字节的数据,如果发送端每次发送1000字节,那么就需要用三个TCP报文来发送这3000字节的数据。
- 此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号,因此分别填的是1、1001和2001。
此时接收端收到了这三个TCP报文后,就可以根据TCP报头当中的32位序列号对这三个报文进行顺序重排(该动作在传输层进行),重排后将其放到TCP的接收缓冲区当中,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了。
- 接收端在进行报文重排时,可以根据当前报文的32位序号与其有效载荷的字节数,进而确定下一个报文对应的序号。
32位确认序号
TCP报头当中的32位确认序号是告诉对端,我当前已经收到了哪些数据,你的数据下一次应该从哪里开始发。
以刚才的例子为例,当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001。
之后主机B对主机A发来的其他报文进行响应时,发给主机A的响应当中的32位确认序号的填法也是类似的道理。
注意:
- 响应数据与其他数据一样,也是一个完整的TCP报文,尽管该报文可能不携带有效载荷,但至少是一个TCP报头。
报文丢失怎么办?
还是以刚才的例子为例,主机A发送了三个报文给主机B,其中每个报文的有效载荷都是1000字节,这三个报文的32位序号分别是1、1001、2001。
如果这三个报文在网络传输过程中出现了丢包,最终只有序号为1和2001的报文被主机B收到了,那么当主机B在对报文进行顺序重排的时候,就会发现只收到了1-1000和2001-3000的字节数据。此时主机B在对主机A进行响应时,其响应报头当中的32位确认序号填的就是1001,告诉主机A下次向我发送数据时应该从序列号为1001的字节数据开始进行发送。
注:确认序号的意义是 保证XXXX序号前的可靠性即1001前的数据都接收到了
因此发送端可以根据对端发来的确认序号,来判断是否某个报文可能在传输过程中丢失了。
为什么要有两套序号,一套序号不行吗?
结论
TCP 需要两套序号,分别是:
-
字节序号(Sequence Number,SEQ):标识每一个数据字节在数据流中的位置
-
确认序号(Acknowledgment Number,ACK):告诉对方“我已经收到哪一段数据了,下一个希望收到的位置是哪里”
如果只用一套序号,很难同时解决 可靠传输 和 双向通信 的问题,容易造成混乱。
Reason
1. TCP 是全双工的
-
TCP 连接两端是同时收发数据的
-
如果只有一套序号,发送方和接收方必须共享同一个计数器
-
这样会造成:你发数据时,我的 ACK 也占用同一套序号,导致序号跳来跳去,不容易判断哪段数据是对应哪一端的
➡ 设计成两套序号,就可以分别独立计数:
-
SEQ 用来标记我发出去的数据字节流
-
ACK 用来告诉对方我收到了哪些字节
2. 确保可靠性和有序性
TCP 需要做到:
-
不丢包:丢了要重传
-
不乱序:即使网络乱序也要按顺序还原
如果只用一套序号:
-
发送端不知道哪些字节已经被确认
-
也无法明确区分“这是新数据”还是“对方只是告诉我收到的数据”
而有了 SEQ + ACK:
-
发送端可以用 SEQ 确认每个字节是否被成功接收
-
接收端通过 ACK 告诉发送端“我收到了前 N 个字节,你可以继续发下一个”
3. 处理重传和丢包
假设有丢包:
-
发送端用 SEQ 判断“哦,我这段数据没被确认,我得重发”
-
接收端用 ACK 告诉发送端“我只收到到这里,继续给我后面的数据”
如果只有一套序号,很难区分“这次是重复发送的数据还是新的数据”。
举个简单例子
比如:
-
主机A发 SEQ=1 的数据(1000字节),B 收到后,发 ACK=1001
-
这样 A 就知道“前 1000 字节已收到,可以发 1001 之后的了”
如果只有一套序号,A 可能不知道 ACK 1001 是:
-
对方收到了 1~1000 字节
还是 -
对方自己发送了第 1001 字节的数据
就会混乱。
窗口大小
TCP本身是具有接收缓冲区和发送缓冲区的:
- 接收缓冲区用来暂时保存接收到的数据。
- 发送缓冲区用来暂时保存还未发送的数据。
- 这两个缓冲区都是在TCP传输层内部实现的。
TCP的发送缓冲区和接收缓冲区存在的意义
发送缓冲区和接收缓冲区的作用:
- 数据在网络中传输时可能会出现某些错误,此时就可能要求发送端进行数据重传,因此TCP必须提供一个发送缓冲区来暂时保存发送出去的数据,以免需要进行数据重传。只有当发出去的数据被对端可靠的收到后,发送缓冲区中的这部分数据才可以被覆盖掉。
- 接收端处理数据的速度是有限的,为了保证没来得及处理的数据不会被迫丢弃,因此TCP必须提供一个接收缓冲区来暂时保存未被处理的数据,因为数据传输是需要耗费资源的,我们不能随意丢弃正确的报文。此外,TCP的数据重排也是在接收缓冲区当中进行的。
经典的生产者消费者模型:
- 对于发送缓冲区来说,上层应用不断往发送缓冲区当中放入数据,下层网络层不断从发送缓冲区当中拿出数据准备进一步封装。此时上层应用扮演的就是生产者的角色,下层网络层扮演的就是消费者的角色,而发送缓冲区对应的就是“交易场所”。
- 对于接收缓冲区来说,上层应用不断从接收缓冲区当中拿出数据进行处理,下层网络层不断往接收缓冲区当中放入数据。此时上层应用扮演的就是消费者的角色,下层网络层扮演的就是生产者的角色,而接收缓冲区对应的就是“交易场所”。
- 因此引入发送缓冲区和接收缓冲区相当于引入了两个生产者消费者模型,该生产者消费者模型将上层应用与底层通信细节进行了解耦,此外,生产者消费者模型的引入同时也支持了并发和忙闲不均。
窗口大小
-
作用:TCP使用窗口大小来控制发送方的数据发送速度。接收端会根据其接收缓冲区的剩余空间来告诉发送方它能够接收多少数据,从而避免过多的数据填满接收缓冲区。
-
例如:如果接收端的缓冲区剩余1000字节空间,它会告知发送方:“我能接收1000字节”。如果发送方在接收到这个窗口大小为1000字节的信号后,发送方就会调整发送速率,避免发送过多的数据。
-
窗口大小字段:当接收端的缓冲区已满时,发送方会收到窗口大小为0的通知,告知发送方暂停发送数据,直到接收端处理了一部分数据,窗口大小重新恢复。
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被打满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。
因此TCP报头当中就有了16位的窗口大小,这个16位窗口大小当中填的是自身接收缓冲区中剩余空间的大小,也就是当前主机接收数据的能力。
接收端在对发送端发来的数据进行响应时,就可以通过16位窗口大小告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度。
- 窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
- 窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
- 如果窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了。
理解现象:
- 在编写TCP套接字时,我们调用read/recv函数从套接字当中读取数据时,可能会因为套接字当中没有数据而被阻塞住,本质是因为TCP的接收缓冲区当中没有数据了,我们实际是阻塞在接收缓冲区当中了。
- 而我们调用write/send函数往套接字中写入数据时,可能会因为套接字已经写满而被阻塞住,本质是因为TCP的发送缓冲区已经被写满了,我们实际是阻塞在发送缓冲区当中了。
- 在生产者消费者模型当中,如果生产者生产数据时被阻塞,或消费者消费数据时被阻塞,那么一定是因为某些条件不就绪而被阻塞。
六个标志位
- TCP报文的种类多种多样,除了正常通信时发送的普通报文,还有建立连接时发送的请求建立连接的报文,以及断开连接时发送的断开连接的报文等等。
- 收到不同种类的报文,我们需要对应执行动作,比如正常通信的报文需要放到接收缓冲区当中等待上层应用进行读取,而建立和断开连接的报文本质不是交给用户处理的,而是需要让操作系统在TCP层执行对应的握手和挥手动作。
- 也就是说不同种类的报文对应的是不同的处理逻辑,所以我们要能够区分报文的种类。而TCP就是使用报头当中的六个标志字段来进行区分的,这六个标志位都只占用一个比特位,为0表示假,为1表示真
SYN
- 报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文。
- 只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置。
ACK
- 报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认。
- 一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据具有一定的确认能力,因此双方在进行数据通信时,可以顺便对对方上一次发送的数据进行响应
FIN
- 报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文。
- 只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置。
URG
双方在进行网络通信的时候,由于TCP是保证数据按序到达的,即便发送端将要发送的数据分成了若干个TCP报文进行发送,最终到达接收端时这些数据也都是有序的,因为TCP可以通过序号来对这些TCP报文进行顺序重排,最终就能保证数据到达对端接收缓冲区中时是有序的。
TCP按序到达本身也是我们的目的,此时对端上层在从接收缓冲区读取数据时也必须是按顺序读取的。但是有时候发送端可能发送了一些“紧急数据”,这些数据需要让对方上层提取进行读取,此时应该怎么办呢?
此时就需要用到URG标志位,以及TCP报头当中的16位紧急指针。
- 当URG标志位被设置为1时,需要通过TCP报头当中的16位紧急指针来找到紧急数据,否则一般情况下不需要关注TCP报头当中的16位紧急指针。
- 16位紧急指针代表的就是紧急数据在报文中的偏移量。
- 因为紧急指针只有一个,它只能标识数据段中的一个位置,因此紧急数据只能发送一个字节,而至于这一个字节的具体含义这里就不展开讨论了。
recv函数的第四个参数flags有一个叫做MSG_OOB的选项可供设置,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层如果想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项。
与之对应的send函数的第四个参数flags也提供了一个叫做MSG_OOB的选项,上层如果想发送紧急数据,就可以使用send函数进行写入,并设置MSG_OOB选项。
PSH
报文当中的PSH被设置为1,是在告诉对方尽快将其接收缓冲区当中的数据交付给上层。
RST
- 报文当中的RST被设置为1,表示需要让对方重新建立连接。
- 在通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的RST标志位就会被置1,表示要求对方重新建立连接。
- 在双方建立好连接进行正常通信时,如果通信中途发现之前建立好的连接出现了异常也会要求重新建立连接。
重传机制
超时重传(Retransmission Timeout, RTO)
双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制。
丢包的两种情况
1、发送的数据丢包
- 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发
2、接收的应答丢包
主机A未收到B发来的确认应答,也可能是因为ACK丢失了因此主机B会收到很多重复数据.
那么TCP协议需要能够识别出那些包是重复的包,并 且把重复的丢弃掉. 这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果.
那么,如果超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证"确认应答一定能在这个时间内返回
- 但是这个时间的长短,随着网络环境的不同,是有差异的.
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间
- Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控 制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传.
- 如果仍然得不到应答,等待4*500ms进行重传.依次类推,以指数形式递增.
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接.
注:TCP 超时重传的初始估算为传递最大报文长度(MSS)时间的两倍
快重传
当主机B(接收方)就收三个重复确认后,主机A(发送方)就会重传丢失的 1001~2000 字节的数据。
连接管理机制
三次握手
双方在进行TCP通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手。
以服务器和客户端为例,当客户端想要与服务器进行通信时,需要先与服务器建立连接,此时客户端作为主动方会先向服务器发送连接建立请求,然后双方TCP在底层会自动进行三次握手。
- 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接。
- 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1。
- 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应。
需要注意的是,客户端向服务器发起的连接建立请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的连接建立请求后,服务器也需要向客户端发起连接建立请求,请求建立从服务器到客户端方法的通信连接。
为什么是三次握手?
三次握手是验证双方通信信道的最小次数:
- 因为TCP是全双工通信的,因此连接建立的核心要务实际是,验证双方的通信信道是否是连通的。
- 而三次握手恰好是验证双方通信信道的最小次数,通过三次握手后双方就都能知道自己和对方是否都能够正常发送和接收数据。
- 在客户端看来,当它收到服务器发来第二次握手时,说明自己发出的第一次握手被对方可靠的收到了,证明自己能发以及服务器能收,同时当自己收到服务器发来的第二次握手时,也就证明服务器能发以及自己能收,此时就证明自己和服务器都是能发能收的。
- 在服务器看来,当它收到客户端发来第一次握手时,证明客户端能发以及自己能收,而当它收到客户端发来的第三次握手时,说明自己发出的第二次握手被对方可靠的收到了,也就证明自己能发以及客户端能收,此时就证明自己和客户端都是能发能收的。
- 既然三次握手已经能够验证双方通信信道是否正常了,那么三次以上的握手当然也是可以验证的,但既然三次已经能验证了就没有必要再进行更多次的握手了。
三次握手能够保证连接建立时的异常连接挂在客户端:
异常连接的原因
尽管三次握手的目的是确保连接的建立,但是在实际网络环境中,有一些情况可能会导致连接“挂起”或出现异常,特别是在客户端上。这些问题通常与网络状况、系统资源、应用程序逻辑等因素有关。
1. 网络延迟或丢包
-
延迟过高:如果网络延迟过高,客户端可能会在等待服务器响应时超时,而未能完成三次握手。这种情况下,连接可能会处于未建立的状态。
-
丢包:如果在三次握手过程中,任何一个包丢失,客户端将无法接收到 ACK 包或 SYN 包,从而导致它一直等待重传。
2. 服务端未能及时响应
-
如果服务器由于过载、网络故障或其他原因未能及时响应客户端的 SYN 请求(即没有发送 SYN + ACK),客户端会一直等待,直到超时。
-
即便服务器没有问题,也可能因为防火墙、路由器等中间设备丢弃了 SYN 包或 ACK 包,导致客户端处于等待状态。
3. SYN Flood 攻击
-
在 SYN Flood 攻击中,攻击者发送大量 SYN 请求,但不完成三次握手。这导致服务器保持大量半开连接,服务器资源被占用。
-
对于客户端来说,如果它发送的 SYN 包未能得到及时响应,它可能会等待很长时间,造成“挂起”状态。
4. 客户端应用层的问题
-
某些应用层程序在发起连接请求后,可能没有正确处理异常或超时情况,导致连接在客户端处于挂起状态。
-
比如,某些程序可能不会检测到连接未完成,而只是一直等待响应。
因此,这里给出两个建立连接时采用三次握手的理由:
- 三次握手是验证双方通信信道的最小次数,能够让能建立的连接尽快建立起来。
- 三次握手能够保证连接建立时的异常连接挂在客户端(风险转移)。
三次握手时的状态变化
三次握手时的状态变化如下:
- 最开始时客户端和服务器都处于CLOSED状态。
- 服务器为了能够接收客户端发来的连接请求,需要由CLOSED状态变为LISTEN状态。
- 此时客户端就可以向服务器发起三次握手了,当客户端发起第一次握手后,状态变为SYN_SENT状态。
- 处于LISTEN状态的服务器收到客户端的连接请求后,将该连接放入内核等待队列中,并向客户端发起第二次握手,此时服务器的状态变为SYN_RCVD。
- 当客户端收到服务器发来的第二次握手后,紧接着向服务器发送最后一次握手,此时客户端的连接已经建立,状态变为ESTABLISHED。
- 而服务器收到客户端发来的最后一次握手后,连接也建立成功,此时服务器的状态也变成ESTABLISHED。
至此三次握手结束,通信双方可以开始进行数据交互了。
套接字和三次握手之间的关系
- 在客户端发起连接建立请求之前,服务器需要先进入LISTEN状态,此时就需要服务器调用对应listen函数。
- 当服务器进入LISTEN状态后,客户端就可以向服务器发起三次握手了,此时客户端对应调用的就是connect函数。
- 需要注意的是,connect函数不直接参与底层的三次握手,connect函数的作用只是发起三次握手。当connect函数返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败。
- 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用accept函数将这个建立好的连接获取上来。
- 三次握手是在 客户端与服务器的 TCP 协议栈 之间进行的,完全由底层的网络协议栈处理。当客户端调用
connect()
,操作系统会完成三次握手,直到连接建立成功。客户端和服务器之间的三次握手并不依赖于accept()
。 accept()
只是 接受 由三次握手已经建立好的连接,它并不参与 握手的实际过程。它的作用是在三次握手完成后,将这个已建立的连接交给应用程序进行数据处理- 当服务器端将建立好的连接获取上来后,双方就可以通过调用read/recv函数和write/send函数进行数据交互了。
三次握手的本质:四次握手优化
为什么四次握手能优化成三次握手呢?
因为服务器面对客户端的连接请求要无脑接受(接受合理请求)
-
服务器在收到客户端 SYN 后,本质上 必然要回复 ACK(确认号 = 客户端 ISN+1)。
-
同时,服务器自己也要发出一个 SYN(带上自己的初始序列号 ISN)。
第二次握手和第三次握手就可以合并成为一次握手即 ACK+SYN(捎带应答,既确认又请求)
为什么服务器的 ACK 和 SYN 可以合并为一个报文
ACK 和 SYN 其实可以放在同一个 TCP 报文里完成,这就是 SYN+ACK,原因有几个:
-
TCP 报头允许多个标志位同时置位
-
TCP 报头中有独立的标志位:
SYN
和ACK
。 -
它们之间并不冲突,可以在同一个报文里同时置 1。
-
这样,一个报文既能表示“确认客户端的 SYN”,也能表示“我自己也要发起 SYN”。
-
-
减少一次往返时延 (RTT)
-
如果分开发,需要四次报文交互;
-
合并后只需要三次报文,大大提高了连接建立效率。
-
-
逻辑上是相互独立的操作
-
ACK 是“确认号”的语义;
-
SYN 是“我要建立连接”的语义;
-
二者没有依赖关系,完全可以在同一报文中表达。
-
-
实现层面更高效
-
操作系统内核的 TCP 协议栈在收到客户端 SYN 后,本来就要生成一个 ACK 确认;
-
同时,服务器自己也必须发一个 SYN 报文来告诉客户端它的初始序列号(ISN);
-
与其发两个包,不如一次性把 ACK 和 SYN 一起带上,减少资源消耗和带宽占用。
-
四次挥手
四次挥手的过程
由于双方维护连接都是需要成本的,因此当双方TCP通信结束之后就需要断开连接,断开连接的这个过程我们称之为四次挥手
还是以服务器和客户端为例,当客户端与服务器通信结束后,需要与服务器断开连接,此时就需要进行四次挥手。
- 第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1,表示请求与服务器断开连接。
- 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应。
- 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求。
- 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应。
四次挥手结束后双方的连接才算真正断开。
为什么是四次挥手?
- 由于TCP是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。在断开连接时不仅要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手。
- 需要注意的是,四次挥手当中的第二次和第三次挥手不能合并在一起,因为第三次挥手是服务器端想要与客户端断开连接时发给客户端的请求,而当服务器收到客户端断开连接的请求并响应后,服务器不一定会马上发起第三次挥手,因为服务器可能还有某些数据要发送给客户端,只有当服务器端将这些数据发送完后才会向客户端发起第三次挥手。
四次挥手时的状态变化
四次挥手时的状态变化如下:
- 在挥手前客户端和服务器都处于连接建立后的ESTABLISHED状态。
- 客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为FIN_WAIT_1。
- 服务器收到客户端发来的连接断开请求后对其进行响应,此时服务器的状态变为CLOSE_WAIT。
- 当服务器没有数据需要发送给客户端的时,服务器会向客户端发起断开连接请求,等待最后一个ACK到来,此时服务器的状态变为LAST_ACK。
- 客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态。
- 当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为CLOSED状态。
- 而客户端则会等待一个2MSL(Maximum Segment Lifetime,报文最大生存时间)才会进入CLOSED状态。
至此四次挥手结束,通信双方成功断开连接。
套接字和四次挥手之间的关系
- 客户端发起断开连接请求,对应就是客户端主动调用close函数。
- 服务器发起断开连接请求,对应就是服务器主动调用close函数。
- 一个close对应的就是两次挥手,双方都要调用close,因此就是四次挥手。
CLOSE_WAIT
- 双方在进行四次挥手时,如果只有客户端调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态。
- 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于CLOSE_WAIT状态的连接(即服务器的 TCP 连接仍然占着一个文件描述符),而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少。
- 因此如果不及时关闭不用的文件描述符,除了会造成文件描述符泄漏以外,可能也会导致连接资源没有完全释放,这其实也是一种内存泄漏的问题。
- 因此在编写网络套接字代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时就可以检查一下是不是服务器没有及时调用close函数关闭对应的文件描述符。
TIME_WAIT
四次挥手中前三次挥手丢包时的解决方法:
- 第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
- 第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
如果客户端在发出第四次挥手后立即进入CLOSED状态,此时服务器虽然进行了超时重传,但已经得不到客户端的响应了,因为客户端已经将连接关闭了。
服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的。
导致的坏结果
-
服务器资源被占用
-
服务器的连接状态卡在 LAST_ACK,等待 ACK。
-
这个连接对应的 文件描述符 / PCB / 缓冲区 等资源都不能释放。
-
如果有很多这样的情况,服务器资源就可能被耗尽。
-
-
服务器不断重传 FIN,占用带宽和 CPU
-
每次超时重传都会消耗服务器的 CPU 和网络带宽。
-
如果积压太多未完成关闭的连接,可能影响正常业务。
-
-
连接一致性问题
-
从服务器角度看,这个连接还没完全关闭(它在等 ACK)。
-
从客户端角度看,连接已经关闭。
-
这种状态不一致会带来诊断上的麻烦。
-
为了避免这种情况,因此客户端在四次挥手后没有立即进入CLOSED状态,而是进入到了TIME_WAIT状态进行等待,此时要是第四次挥手的报文丢包了,客户端也能收到服务器重发的报文然后进行响应。
TIME_WAIT状态存在的必要性:
- 客户端在进行四次挥手后进入TIME_WAIT状态,如果第四次挥手的报文丢包了,客户端在一段时间内仍然能够接收服务器重发的FIN报文并对其进行响应,能够较大概率保证最后一个ACK被服务器收到。
- 客户端发出最后一次挥手时,双方历史通信的数据可能还没有发送到对方。因此客户端四次挥手后进入TIME_WAIT状态,还可以保证双方通信信道上的数据在网络中尽可能的消散。
实际第四次挥手丢包后,可能双方网络状态出现了问题,尽管客户端还没有关闭连接,但也收不到服务器重发的连接断开请求,此时客户端TIME_WAIT等若干时间最终会关闭连接,而服务器经过多次超时重传后也会关闭连接。
断开连接:四次挥手
四次挥手的目标是让双方都能明确“我这边没有数据了”,并且最终释放资源。
- 但如果第四次 ACK 丢失,服务器会不断重发 FIN,而客户端 TIME_WAIT 兜底。
- 如果网络出现异常,TIME_WAIT 期满后客户端仍然会进入 CLOSED,而服务器也会在多次重传超时后进入 CLOSED。
为什么 TCP 不能保证 建立/断开 的绝对可靠
-
三次握手、四次挥手都依赖报文交互;报文可能丢失、延迟、甚至被网络隔离。
-
在连接的 建立阶段,如果握手没能完成,双方会认为连接失败。
-
在连接的 断开阶段,如果最后的 ACK 丢失,双方状态可能一度不一致(客户端 CLOSED/服务器 LAST_ACK),只能依靠 TIME_WAIT + 重传超时 机制最终收敛。
所以:
-
TCP 并不能 绝对保证“连接一定能成功建立”或“一定能优雅断开”;
-
TCP 保证的是 一旦连接进入 ESTABLISHED,那么在此期间,数据传输是 可靠的、有序的、无丢失/无重复的。
总结一句
TCP 的可靠性是针对“数据传输阶段”的,而不是连接建立和释放阶段。
三次握手、四次挥手只是尽量做到健壮,最终依靠 超时和状态机机制 来保证系统能收敛,但并不能百分百保证“建立/断开”的可靠
TIME_WAIT的等待时长是多少?
TIME_WAIT的等待时长既不能太长也不能太短。
- 太长会让等待方维持一个较长的时间的TIME_WAIT状态,在这个时间内等待方也需要花费成本来维护这个连接,这也是一种浪费资源的现象。
- 太短可能没有达到我们最初目的,没有保证ACK被对方较大概率收到,也没有保证数据在网络中消散,此时TIME_WAIT的意义也就没有了。
TCP协议规定,主动关闭连接的一方在四次挥手后要处于TIME_WAIT状态,等待两个MSL(Maximum Segment Lifetime,报文最大生存时间)的时间才能进入CLOSED状态。
MSL在RFC1122中规定为两分钟,但是各个操作系统的实现不同,比如在ubuntu 20.04下默认配置的是60s
可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout来查看
TIME_WAIT的等待时长设置为两个MSL的原因:
- MSL是TCP报文的最大生存时间,因此TIME_WAIT状态持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。
- 同时也是在理论上保证最后一个报文可靠到达的时间。
滑动窗口
连续发送多个数据
需要注意的是,虽然双方在进行TCP通信时可以一次向对方发送大量的报文,但不能将自己发送缓冲区当中的数据全部打包发送给对端,在发送数据时还要考虑对方的接收能力。
滑动窗口
发送方可以一次发送多个报文给对方,此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的。
其实可以将发送缓冲区当中的数据分为三部分:
- 已经发送并且已经收到ACK的数据。
- 已经发送还但没有收到ACK的数据。
- 还没有发送的数据。
这里发送缓冲区的第二部分就叫做滑动窗口。(也有人把这三部分整体称之为滑动窗口,而将其中的第二部分称之为窗口大小)
而滑动窗口描述的就是,发送方不用等待ACK一次所能发送的数据最大量。
滑动窗口存在的最大意义就是可以提高发送数据的效率:
- 滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小的较小值,因为发送数据时不仅要考虑对方的接收能力,还要考虑当前网络的状况。
- 我们这里先不考虑拥塞窗口,并且假设对方的窗口大小一直固定为4000,此时发送方不用等待ACK一次所能发送的数据就是4000字节,因此滑动窗口的大小就是4000字节。(四个段)
- 现在连续发送1001-2000、2001-3000、3001-4000、4001-5000这四个段的时候,不需要等待任何ACK,可以直接进行发送。
- 当收到对方响应的确认序号为2001时,说明1001-2000这个数据段已经被对方收到了,此时该数据段应该被纳入发送缓冲区当中的第一部分,而由于我们假设对方的窗口大小一直是4000,因此滑动窗口现在可以向右移动,继续发送5001-6000的数据段,以此类推。
- 滑动窗口越大,则网络的吞吐率越高,同时也说明对方的接收能力很强。
在 TCP 中,滑动窗口不仅控制发送方能够发送的数据范围,同时也天然支持重传机制。窗口左边界之前的数据已被确认,可以安全释放;窗口中间的数据已发送但未确认,必须暂存在发送缓冲区以备重传;窗口右边界决定了还能发送多少新数据。当发送方收到 ACK 时,窗口左边界右移,释放已确认的数据,并可能推动右边界扩展,从而允许更多新数据进入窗口发送。
丢包问题
当发送端一次发送多个报文数据时,此时的丢包情况也可以分为两种
1、数据包已经抵达,ACK丢包。
在发送端连续发送多个报文数据时,部分ACK丢包并不要紧,此时可以通过后续的ACK进行确认。
毕竟ACK是表示多少序号前的数据是已应答,比如传递1~4000字节的数据时,应答的1001ACK丢失了或者1001和2001丢失,不要紧只要最后面的4001正常就已经表示了1~4000字节的数据已收到
2、数据包丢失
详情见重传机制中的快重传
滑动窗口中的数据一定都没有被对方收到吗?
滑动窗口当中的数据是可以暂时不用收到对方确认的数据,而不是说滑动窗口当中的数据一定都没有被对方收到,滑动窗口当中可能有一部分数据已经被对方收到了,但可能因为滑动窗口内靠近滑动窗口左侧的一部分数据,在传输过程中出现了丢包等情况,导致后面已经被对方收到的数据得不到响应。
例如图中的1001-2000的数据包如果在传输过程中丢包了,此时虽然2001-5000的数据都被对方收到了,此时对方发来的确认序号也只能是1001,当发送端补发了1001-2000的数据包后,对方发来的确认序号就会变为5001,此时发送缓冲区当中1001-5000的数据也会立马被归置到滑动窗口的左侧
流量控制
接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度.这个机制就叫做流量 控制(Flow Control);
- 接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK通知发送端。
- 窗口大小字段越大,说明网络的吞吐量越高。
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。
- 发送端接收到这个窗口之后,就会减慢自己发送的速度。
- 如果接收端缓冲区满了,就会将窗口值设置为0,这时发送方不再发送数据,但需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
当发送端得知接收端接收数据的能力为0时会停止发送数据,此时发送端会通过以下两种方式来得知何时可以继续发送数据。
- 等待告知。接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个TCP报文,主动将自己的窗口大小告知发送端,发送端得知接收端的接收缓冲区有空间后就可以继续发送数据了。
- 主动询问。发送端每隔一段时间向接收端发送报文,该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了。
16为数字最大表示65535,那TCP窗口最大就是65535吗?
理论上确实是这样的,但实际上TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位得到的。
第一次向对方发送数据时如何得知对方的窗口大小?
双方在进行TCP通信之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力,所以双方在发送数据时是不会出现缓冲区溢出的问题的。
拥塞控制
为什么会有拥塞控制?
两个主机在进行TCP通信的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发。但如果双方在通信时出现了大量丢包,此时就不能认为是正常现象了。
TCP不仅考虑了通信双端主机的问题,同时也考虑了网络的问题。
- 流量控制:考虑的是对端接收缓冲区的接收能力,进而控制发送方发送数据的速度,避免对端接收缓冲区溢出。
- 滑动窗口:考虑的是发送端不用等待ACK一次所能发送的数据最大量,进而提高发送端发送数据的效率。
- 拥塞窗口:考虑的是双方通信时网络的问题,如果发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。
双方网络通信时出现少量的丢包TCP是允许的,但一旦出现大量的丢包,此时量变引起质变,这件事情的性质就变了,此时TCP就不再推测是双方接收和发送数据的问题,而判断是双方通信信道网络出现了拥塞问题。
拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据.但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据
- TCP除了有窗口大小和滑动窗口的概念以外,还有一个窗口叫做拥塞窗口。拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。
- 刚开始发送数据的时候拥塞窗口大小定义以为1,每收到一个ACK应答拥塞窗口的值就加一。
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送数据的窗口大小,即滑动窗口的大小。
每收到一个ACK应答拥塞窗口的值就加一,此时拥塞窗口就是以指数级别进行增长的,如果先不考虑对方接收数据的能力,那么滑动窗口的大小就只取决于拥塞窗口的大小
- 但指数级增长是非常快的,因此“慢启动”实际只是初始时比较慢,但越往后增长的越快。如果拥塞窗口的值一直以指数的方式进行增长,此时就可能在短时间内再次导致网络出现拥塞。
- 为了避免短时间内再次导致网络拥塞,因此不能一直让拥塞窗口按指数级的方式进行增长。
- 此时就引入了慢启动的阈值,当拥塞窗口的大小超过这个阈值时,就不再按指数的方式增长,而按线性的方式增长。
- 当TCP刚开始启动的时候,慢启动阈值设置为对方窗口大小的最大值。
在每次超时重发的时候,慢启动阈值会变成当前拥塞窗口的一半,同时拥塞窗口的值被重新置为1,如此循环下去。
如下图:
图示说明:
- 指数增长。刚开始进行TCP通信时拥塞窗口的值为1,并不断按指数的方式进行增长。
- 线性增大。慢启动的阈值初始时为对方窗口大小的最大值,图中慢启动阈值的初始值为16,因此当拥塞窗口的值增大到16时就不再按指数形式增长了,而变成了的线性增长。
- 乘法减小。拥塞窗口在线性增长的过程中,在增大到24时如果发生了网络拥塞,此时慢启动的阈值将变为当前拥塞窗口的一半,也就是12,并且拥塞窗口的值被重新设置为1,所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是12。
主机在进行网络通信时,实际就是在不断进行指数增长、线性增大和乘法减小。
需要注意的是,不是所有的主机都是同时在进行指数增长、线性增大和乘法减小的。每台主机认为拥塞窗口的大小不一定是一样的,即便是同区域的两台主机在同一时刻认为拥塞窗口的大小也不一定是完全相同的。因此在同一时刻,可能一部分主机正在进行正常通信,而另一部分主机可能已经发生网络拥塞了。
延迟应答
如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。
- 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
- 但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
- 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的最大窗口大小就是1.5M。
需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。
此外,不是所有的数据包都可以延迟应答。
- 数量限制:每收到N个包就应答一次。
- 时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。
延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。
捎带应答
捎带应答其实是TCP通信时最常规的一种方式,就好比主机A给主机B发送了一条消息,当主机B收到这条消息后需要对其进行ACK应答,但如果主机B此时正好也要给主机A发送消息,此时这个ACK就可以搭顺风车,而不用单独发送一个ACK应答,此时主机B发送的这个报文既发送了数据,又完成了对收到数据的响应,这就叫做捎带应答。
捎带应答最直观的角度实际也是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了。
此外,由于捎带应答的报文携带了有效数据,因此对方收到该报文后会对其进行响应,当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了,同时也能确保捎带的ACK应答也被对方可靠的收到了
面向字节流
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区
- 调用write时,数据会先写入发送缓冲区中
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用read从接收缓冲区拿数据
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一 个连接,既可以读数据,也可以写数据.这个概念叫做全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
- 写100个字节数据时,可以调用一次write写100字节,也可以调用100次write,每次写一个字节。
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
实际对于TCP来说,它并不关心发送缓冲区当中的是什么数据,在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流。
粘包问题
什么是粘包?
- 首先要明确,粘包问题中的“包”,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段。
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 但站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界
- 对于定长的包,保证每次都按固定大小读取即可。
- 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含Content-Length属性,表示正文的长度。
- 对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可。
思考:对于UDP协议来说,是否也存在"粘包问题"呢?
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层的,有很明确的数据边界。
- 站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况。
因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
TCP异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN.和正常关闭没有什么区别.
机器重启:和进程终止的情况相同.
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset.即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在.如果对方不在,也会把连接释放
另外,应用层的某些协议,也有一些这样的检测机制.例如HTTP长连接中,也会定期检测对方的状态.例如QQ,在QQ断线之后,也会定期尝试重新连接.
TCP小结
TCP协议这么复杂就是因为TCP既要保证可靠性,同时又尽可能的提高性能。
可靠性:
- 检验和。
- 序列号。
- 确认应答。
- 超时重传。
- 连接管理。
- 流量控制。
- 拥塞控制。
提高性能:
- 滑动窗口。
- 快速重传。
- 延迟应答。
- 捎带应答。
需要注意的是,TCP的这些机制有些能够通过TCP报头体现出来的,但还有一些是通过代码逻辑体现出来的。
TCP/UDP对比
我们说了TCP是可靠连接,那么是不是TCP一定就优于UDP呢?TCP和UDP之间的 优点和缺点,不能简单,绝对的进行比较
- TCP用于可靠传输的情况,应用于文件传输,重要状态更新等场景;
- UDP用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等.另外UDP可以用于广播
归根结底,TCP和UDP都是程序员的工具,什么时机用,具体怎么用,还是要根据具体 的需求场景去判定.