TCP原理
我们还是先回顾一下TCP特点
-
TCP特点
- 有连接
哪怕知道通信IP和端口,也不能直接通信,必须先通过accpet()进行连接才能通信 - 可靠传输
接收方收到完整数据后会返回一个确认报文.发送方如果未收到确认报文则进行重传操作 - 面向字节流
可以任意拆分数据,不会有数据报的限制 - 全双工
可以同时进行收发数据 - 大小不限
- 有连接
-
协议格式

-
TCP报头
- 源端口
- 目的端口
- 32位序列号:发送数据中第一个字节的序号
- 32位确认序号:发送期望收到的序号
- 4位TCP报头长度:表示TCP头部有多少个32位bit(TCP头部的最大长度是4*15=60)
- 6位标志位:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST:对方要求重新建立连接(携带RST标识的称为复位报文段)
- SYN:请求建立连接(携带SYN标识的称为同步报文段)
- FIN:通知对方此端准备关闭(携带FIN标识的称为结束报文段)
- 16位窗口大小:接收方应该接收的窗口大小
- 16位效验和:接收端数据若CRC效验不同,则数据有问题.与UDP不同,这个效验不仅包含TCP数据,还包含TCP报头
- 16位紧急指针:标识哪部分数据是紧急数据
- 选项:可以填充一些公司信息和部分额外信息,非必须
-
TCP状态
在前面多线程的学习中,我们知道锁就是有各种各样的状态的,每个状态代表当前锁的情况
在TCP协议中,协议也是有状态的
| TCP状态 | 含义 |
|---|---|
| CLOSED | 初始状态,无连接 |
| LISTEN | 服务端监听,等待客户端连接 |
| SYN_SENT | 客服端发送了SYN报文,等待服务端确认 |
| SYN_RCVD | 服务端接收到了SYN报文并返回一条SYN+ACK报文,等待客户端确认 |
| ESTABLISHED | 连接已建立,可以进行数据传输 |
| FIN_WAIT_1 | 主动关闭方发FIN,开始断连 |
| FIN_WAIT_2 | 收到对方ACK,等待对方FIN |
| TIME_WAIT | 等待一段时间后自动转入CLOSED |
| CLOSING | 双方同时发FIN,同时关闭 |
| CLOSE_WAIT | 被动关闭方,收到对方FIN |
| LAST_ACK | 被动关闭方发FIN,等待对方确认 |
现在我们先了解,在下文介绍三次握手四次挥手中会更加详细的介绍每种状态出现的时机
TCP和UDP最大差异点就是TCP是可靠传输.那么TCP是如何实现可靠传输的呢?
TCP核心机制1:确认应答
TCP传输中,发送方每发送一段数据,接收方都会返回一个ACK报文.作用就是向发送方告知:确认应答,并且还会说下一个想要的数据序列号(确认序列号)
每一个TCP报头中都包含了应该32位序号.这个序号的作用是发送数据中的第一个字节的编号,接收方会通过第一个的字节编号来以此类推下一次发送的编号是多少
例如主机A向主机B发送一个hello数据.此时主机A的TCP报头中指定'h'这个字节序列号是100.那么当主机B接收到这个hallo数据时,会自动对每一个字节进行编号('h'编号是100,'e'编号是101...),到最后一个'o'的编号是104.此时接收方收完这个数据就会返回一个ACK报文.报文中的确认序列号就是105(104的下一位),向主机A告知下一个数据的第一个字节编号是105
主机B 主机A 主机B 主机A 发送 hello (序号100~104) 返回 ACK (确认号105) 发送 world (序号105~109) 返回 ACK (确认号110)
也正是由于每一条数据都具有特定的序列号,所以在网络通信中不会出现后发的数据先被解析,而是会依序列号进行解析

既然通过ACK可以确保数据是否被收到.那么如果发送方一直没收到ACK呢?此时,就有了TCP核心机制2
TCP核心机制2 超时重传
网络通信中经常会由于不可抗力因素导致的丢包.在上部分UDP原理中,UDP协议并不会在意是否丢包,而TCP中由于有ACK机制,所以当丢包发生时,发送方可能迟迟未收到ACK报文,于是就会重新发一个相同的数据
主机B 数据丢失 主机A 主机B 数据丢失 主机A 发送 hello (丢包) 发送第二条 hello(触发超时重传) 返回ACK
但丢包不只是存在发送的数据丢包,还有接收方返回的数据丢包.若返回数据丢包,则会发生下面的内容
主机B 数据丢失 主机A 主机B 数据丢失 主机A 发送 hello(序号100~104) 返回ACK(确认号105)(丢包) 发送 hello(序号100~104)(触发超时重传) 返回ACK(确认号105)
可见由于第一次接收到的数据返回ACK丢包了,主机A没收到ACK,于是重新发送了一遍相同的数据,这时主机B还是返回了一个相同的ACK(确认号105)
虽然看起来主机B收到了两次相同的数据,但实际上真正被处理的还是第一次发送的数据.因为当主机B收到第二次相同的数据时,会发现序列号是已经被处理过的了,直接当作过期数据,返回一个ACK后就不做任何额外处理了.因此在应用层实际收到的还是一个hello,而不是hellohello
TCP协议本质上还是一个生产者消费者模型.内部有一个缓冲区.当发生重复接收同一条数据时,就会处理掉重复的数据(不添加到缓冲区);当发生后发先至时,就会对数据进行排序处理(在未排序好数据时会阻塞缓冲区)
通过上面两个机制,便已经具有基本可靠传输的传输的特点.TCP还和UDP有一个很不同的特点,即TCP是有连接的,在网络通信前先尝试连接,如果网络连接不可靠,那么自然连接不上,因此通过连接机制,也能提高可靠传输的能力
TCP核心机制3 连接管理
- 三次握手
我们经常说的三次握手就是在这发生.大概的流程如下
服务端 客户端 服务端 客户端 SYN(我要和你建立连接了) ACK(好的,我知道了) SYN(我也要和你建立连接了) ACK(好的,我也知道了)
不是说三次握手吗?为什么这里有四条通信.其实事实上确实是三条通信,只是为了方便理解所以才拆分成了四条.真正的情况下是这样:
服务端 客户端 服务端 客户端 SYN(我要和你建立连接了) ACK+SYN(好的,我知道了,我也要和你建立连接了) ACK(好的,我也知道了)
通过这样的三次通信,客户端和服务端就都能确定对方的通信正常了
- 那能不能用两次握手呢?我们可以看看如果使用两次握手是怎样的
服务端 客户端 服务端 客户端 SYN(我要和你建立连接了) ACK+SYN(好的,我知道了,我也要和你建立连接了)
在这中情况下,只有客户端知道能够和服务端建立连接,但在服务端眼里,因为没有接收到确认报文,所应并不清楚客户端有没有真正建立连接
- 那四次握手可以吗?
当然可以,但是很明显没必要.能三步完成的事情为什么要使用四步 - 三次握手中的具体内容:
握手这个过程中,不传递具体的数据,只传递没有载荷,只有报头的数据报
- 第一次握手:客户端向服务端发起建立连接请求,此时SNY = 1 ,ACK = 0
谁先发起请求,谁就是客户端.在客户端向服务端发出的请求中,报头里包含一个 SNY(同步) 数据
相当于客户端在告诉服务端:我要和你建立连接(需要保存你的信息)
待服务端收到连接请求后,服务端也要告诉客户端:我也要和你建立连接(也需要保存信息)
于是,第二次握手开始了 - 第二次握手:服务端回应客户端发送的请求并尝试于客户端建立连接,此时SNY = 1,ACK = 1
这时服务端不仅回应客户端:我知道了你的连接请求.还同时告知服务端:我也要和你建立连接了
当客户端收到ACK信息后,客户端就确定了自己已经和服务端连接上了.当解析到SNY信息时,客户端也知道了服务端要进行连接.于是返回一个确认报文. - 第三次握手:客户端向服务端发送一个ACK报文
服务端接收到这个确认报文后知道自己也跟客户端连接上了.这时候客户端和服务端双方就真正的连接成功了
-
三次握手的意义
- 初步验证网络通信路径是畅通的
客户端和服务端都能正常收到请求说明网络能够正常通信 - 验证通信双方发送能力和接收能力是否正常
如果服务端能收到客户端的连接请求,那么说明服务端的接收能力和客户端的发送能力是正常的
如果客户端能收到服务端的连接请求,那么说明服务端的发送能力和客户端的接收能力是正常的 - 完成参数协商
三次握手中客户端和服务端都会各自生成一个初始序号并告知对方
这样双方就会知道应该从哪一个序号开始
- 初步验证网络通信路径是畅通的
-
状态变化
- 握手前:
客户端:CLOSED
服务端:LISTEN
客户端在连接前服务端已经启动,因此服务端在监听状态,随时准备进行连接 - 第一次握手:
客户端:CLOSED --> SYN_SENT
服务端:LISTEN --> SYN_RCVD
客户端发送连接报文SYN,状态变为SYN_SEND.服务端收到SYN后,发送SYN和ACK报文,状态变为SYN_RCVD - 第二次握手:
客户端:SYN_SENT --> ESTABUSHED
服务端:SYN_RCVD
客户端接收到服务端的SYN和ACK报文后,返回一个ACK报文,状态变为ESTABUSHED - 第三次握手:
客户端:ESTABUSHED
服务端:SYN_RCVD --> ESTABUSHED
服务端接收到ACK后,状态变为ESTABUSHED
- 握手前:
-
四次挥手
建立连接需要三次握手,那么断开连接肯定也不是说断就断的.断开连接就需要通过四次挥手才能真正断开.大概流程如下
主机B 主机A 主机B 主机A FIN(我要和你断开连接了) ACK(好的,我知道了) FIN(那我也要和你断开连接了) ACK(好的,我也知道了)
实际上我们可以发现四次挥手的流程和三次握手几乎相同,仅仅只是把最开始建立连接的SYN报文替换成了断开连接FIN报文
那么我们也可以把四次通信给优化成下面这样的三次吧?
主机B 主机A 主机B 主机A FIN(我要和你断开连接了) ACK+FIN(好的,我知道了,那我也要和你断开连接了) ACK(好的,我也知道了)
这个可以说是不行的,但在部分情况下确实可以实现(通过后文讲解的捎带应答机制)
前面三次握手能优化成三次通信是由于在建立连接时不会传输数据,接收方接到连接请求可以立即进行连接操作(SYN)和返回ACK.但在断开连接这个过程中,接收方收到了FIN报文可以立即返回ACK,但只能在处理完传输逻辑后返回FIN.这就导致在通常情况下FIN报文和ACK报文就不会在同一时间发出,故而这两条报文通常不能合并成一条
-
状态变化
在前面的三次握手中,是由客户端主动向服务器发起的.但在四次挥手中,客户端和服务器都有可能是主动发起的一方
下面以客户端要断开连接为例
- 挥手前:
客户端:ESTABUSHED
服务端:ESTABUSHED
客户端和服务端在互相传输数据 - 第一次挥手:
客户端:ESTABUSHED --> FIN_WAIT_1
服务端:ESTABUSHED
客户端发送FIN报文,状态由ESTABUSHED变更为FIN_WAIT_1 - 第二次挥手:
客户端:FIN_WAIT_1
服务端:ESTABUSHED --> CLOSE_WAIT
服务端收到FIN报文后立即返回ACK报文并等待剩余数据传输完毕,状态由ESTABUSHED变更为CLOSE_WAIT - 第三次挥手:
客户端:FIN_WAIT_1 --> FIN_WAIT_2
服务端:CLOSE_WAIT --> LAST_ACK
客户端接收到第一个ACK,状态变更为FIN_WAIT_2,进入第二阶段的等待,等待服务端发送的FIN报文
服务端处理完剩余数据后再发送FIN报文,状态由CLOSE_WAIT变更为LAST_ACK - 第四次挥手:
客户端:FIN_WAIT_2 --> TIME_WAIT --> CLOSED
服务端:LAST_ACK --> CLOSED
客户端收到FIN报文后由FIN_WAIT_2状态变为TIME_WAIT,在此状态下,客户端会在2MSL(两倍的通信最长时间)后自动变成CLOSED
服务端在接收到ACK后直接变为CLOSED状态
到此四次挥手结束
- 挥手前:
-
为什么客户端在收到FIN报文后不是直接进入关闭状态而是非要等待一段时间才真正关闭呢?
这个就是预防丢包导致客户端发送的ACK没有被服务端接收到.如果客户端是直接进入CLOUSED状态,服务端由于ACK丢包导致重发一遍FIN报文时客户端无法解析,更无法响应.而特意留一段时间关闭就可以在收到第二次FIN报文时重新返回新的ACK,让服务端能够正常关闭
TCP核心机制4 滑动窗口
-
窗口是什么?
引入一个概念,对批量发送的"度"进行限制(定量的衡量)
这个概念就是窗口
能够不阻塞的,发送数据的最大量就是所谓的滑动窗口
就好比一个人一顿最多吃三碗饭,如果强制给他吃4顿,那也会吐一顿出来.无论吃再多,真正能吃进去的就只有3顿
-
什么是"滑动"窗口?
事实上滑动窗口只是一个形象的比喻.这个就和算法中的滑动窗口所表示的意思是差不多的,具体如下

-
那么为什么要引入这个机制呢?
现在我们知道,TCP和UDP最大的区别是可靠传输.为了做到可靠传输这一点,TCP每发一条数据,对方都必须返回一条ACK.这样确实可靠性保证了,但效率很明显就比不过UDP.人家UDP是不管数据能不能送达,反正我发了就行.因此,使用TCP会不可避免的降低传输效率
而滑动窗口正是 TCP 中用来提高传输效率的机制滑动窗口机制说起来其实很简单,就是一口气发送一组数据
-
下面是非滑动窗口的传输情况
主机B 主机A 主机B 主机A 数据(1~1000) 确认应答(下一个是1001) 数据(1001~2000) 确认应答(下一个是2001) 数据(2001~3000) 确认应答(下一个是3001) 数据(3001~4000) 确认应答(下一个是4001)
- 下面是滑动窗口的传输情况
主机B 主机A 主机B 主机A 数据(1~1000) 数据(1001~2000) 数据(2001~3000) 数据(3001~4000) 确认应答(下一个是1001) 确认应答(下一个是2001) 确认应答(下一个是3001) 确认应答(下一个是4001)
由上可见:
滑动窗口核心特点 :一次处理多条数据;批量发送和批量等待ACK
通过一次性发送大量数据和同时等待大量ACK,相比一次发送就等待一个ACK能明显提高TCP数据的传输效率
-
TCP协议是如何得知滑动窗口的大小?
毕竟不同的主机性能和网络都不同,TCP是如何确定滑动窗口大小的呢?
答案就在前面TCP协议格式中的16位窗口大小 里.建立连接过程中,不是有一点就是参数协商么.这个窗口大小就是协商的参数之一(具体的协商流程就是在下一个核心机制流量控制中了)
当然,毕竟是网络通信.所以也得考虑丢包情况
-
丢包情况
丢包情况分为两种.一种是返回的ACK丢包了,另外一种是发送的数据丢了
- ACK 丢了
主机B 数据丢失 主机A 主机B 数据丢失 主机A 数据(1~1000) 数据(1001~2000) 数据(2001~3000) 数据(3001~4000) 确认应答(下一个是1001) 确认应答(下一个是2001) 确认应答(下一个是3001) 确认应答(下一个是4001)
这种情况下其实不必太担心.即使部分ACK丢包了,但只要后面的ACK确认了,主机A就已经知道该序列号前面的数据都已经被主机B收到并解析了
类似于别人问你上高中了吗?然后你回别人一句我已经上大学了.既然这样那肯定是高中都上完了才能上的大学
- 那如果是最后一个ACK丢了呢?
这个在下一个要讲解的核心机制"流量控制"中会有详细说明.在此可以先说结果:会重发
- 数据丢了
主机B 数据丢失 主机 A 主机B 数据丢失 主机 A 收到三个同样的ACK进行重发 收到丢失数据后会自动补齐已收到的2001~5000数据 数据(1~1000) 数据(1001~2000) 数据(2001~3000) 数据(3001~4000) 数据(4001~5000) 确认应答(下一个是1001) 确认应答(下一个是2001) 确认应答(下一个是2001) 确认应答(下一个是2001) 数据(1001~2000) 确认应答(下一个是5001)
由此可见当发送端的数据丢包时,接收方每收到一个新数据,都会返回缺失的序列号.
而当发送端收到三个相同的ACK时就知道这个ACK对应的数据丢了,于是重发
重发丢包的数据不会让已经到达数据再重发一次.已经到达的数据会进入缓冲区,直到缺少的数据补齐才会进入解析流程,因此只要收到重发数据,返回ACK就是未到达的下一个数据序列号(这个机制被称为"高速重发控制",也叫"快重传")
-
滑动窗口的效率
很明显,滑动窗口的效率高低取决于窗口的大小:窗口越大,效率越高,反之亦然
只有当窗口无穷大时,此时的效率才等同于UDP.因此TCP的效率无论如何优化都不会高于UDP
TCP核心机制5 流量控制
通过上个滑动窗口机制,我们知道滑动窗口的大小决定了传输效率,而滑动窗口也不能无限大,过大的窗口可能会导致接收方处理不过来,造成丢包.也就是前面举例的三碗饭量,无论你如何让他吃四碗或五碗,他都会吐出3碗以上的量.而这吐出来的部分由于没有真正吃下去,还得重新给做一份,即丢包还要重传.这会无故多大量网络带宽
因此便有了流量控制这个核心机制
要知道TCP协议的本质还是生产者消费者模型.在此模型中,消费者都有一个缓冲队列.我们可以把这个缓冲队列类比成一个仓库.每天都有不同的货物进出.当生产速度大于消费速度时,货物就会堆积,仓库不是无限大,当堆积到一定数量时仓库就会爆满.满了后多的货物就只能丢弃了.想要再放新的货物只能等待消费一部分货物.
流量控制就是为了预防获取过度堆积造成的货物丢失
- 流量控制的大抵实现逻辑
- 接收方收到数据
- 计算剩余缓冲区大小
- 将剩余缓冲区大小放入ACK中返回给对方
这个缓冲区大小也是存储在16位窗口大小中的
可能你会疑惑:不是前面滑动窗口的窗口大小就是通过16位窗口大小反映的吗?现在再加上剩余缓冲区大小会不会不太够用?
事实上此时的16位并不代表16bit.通过选项中的"窗口扩展因子",理论上可以反映一个非常大的数字
当发送方接收到"16位窗口大小"后,就会作为下一次滑动窗口的大小
- 具体实现原理
- 发送方给接收方发送数据
- 接收方收到数据后把数据存放在接收方的缓冲区内
- 接收方的应用程序通过socket api(inputStream)从缓冲区中读取数据,读取一份缓冲区的数据就少一份数据
- 接收方在做ACK应答时会把缓冲区剩余空间的大小放入"16位窗口大小"中一并发送给发送方
- 若接收方发现自己的缓冲区快满了,就会把窗口大小设置成一个更小的值发送给对方
- 发送方接收到一个较小的窗口大小后会调整自己的发送速度
- 当接收方缓冲区满了就会把窗口大小设置为0
- 发送方收到后不会再发送数据,但是会定期发送一个窗口探测数据段,让接收方把当前窗口大小告诉发送端
下面是一个简单的流量控制示例
主机B 主机A 主机B 主机A 知道可用窗口大小后会通过滑动窗口机制一口气发送完该窗口大小的数据 当接收端处理出一定缓冲区空间时会发送一个窗口更新通知 当过了超时重发时间还未收到窗口更新通知时会主动发送一个窗口探测包 此时处理出一定缓冲区后 数据(1~1000) 确认应答(下一个是1001,窗口大小2000) 数据(1001~2000) 数据(2001~3000) 确认应答(下一个是2001,窗口大小1000) 确认应答(下一个是3001,窗口大小0) 窗口探测包 窗口更新通知(下一个是3001,窗口大小0) 窗口更新通知(下一个是3001,窗口大小1000) 数据(3001~4000)
- 假如窗口大小从0到了1000,发出去的窗口更新通知丢包了怎么办?
事实上没有什么很大影响.因为在发送端在等待期间会周期性不断发送窗口探测包
TCP核心机制6 拥塞控制
上一个核心机制流量控制是根据接收方的处理能力进行制约的
而这个核心机制拥塞控制是根据双发通信链路好坏进行制约的
每个设备之间的通信链路好坏都是不一样的.就像你在卧室的wifi信号总是要比厕所的wifi信号好一样.所以即使双方的处理能力都很强,但也会被通信链路给制约
那么,在TCP协议中,通信链路的好坏又是如何制约通信速度的呢?
- 拥塞控制的思路
- 首先按照非常慢的速度发送数据
- 如果没有出现丢包,就代表链路通畅,放大窗口,加快速度
增长速度为指数级 - 如果出现丢包,就代表链路拥堵,缩小窗口,放慢速度
在旧版本中:速度直接降回刚开始的非常慢的速度.重新以指数级递增到原丢包速度的一半后变成线性缓慢递增
在新版本中:速度直接降回到原丢包速度的一半后线性缓慢递增
下面是拥塞控制的大概示例:
主机B 主机A 主机B 主机A 由于处理不过来造成了最后一条数据丢包 减小了传输速度 数据(1~100) 确认应答(下一个是101) 数据(101~1000) 数据(1001~2000) 确认应答(1001) 确认应答(2001) 数据(2001~3000) 数据(3001~4000) 数据(4001~5000) 确认应答(3001) 确认应答(4001) 数据(4001~5000) 数据(5001~6000) 确认应答(5001) 确认应答(6001)
- 具体的阻塞原理可以看下图

由此我们知道
流量控制:控制的是发送方的窗口大小
阻塞控制:控制的也是发送方的窗口大小
在实际传输过程中,上面哪个小就以哪个为传输最大速度
TCP核心机制7 延迟应答
延迟应答也是TCP用来提高传输效率 的机制之一
具体实现原理很简单:接收收到数据时不会立即返回ACK,而是等一会再返回
- 为什么延迟应答能提高效率?
拿前面仓库存放快递想举例.仓库并不是只能放入完货物后才开始拿出货物的,而是在放入货物这个过程中也能拿出货物.因此只要稍等一会儿,缓冲区就会有更大的空闲空间.也就能够接受更大的窗口 - 延时应答的时间是多久?
实际上延迟应答的时间取决于数量和时间- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,不同的操作系统可能会有一定差异.一般N=2,超时时间为200ms
主机B 主机A 主机B 主机A 数据(1~1000) 数据(1001~2000) 确认应答(下一个是2001) 数据(2001~3000) 数据(3001~4000) 确认应答(下一个是4001)
TCP核心机制8 捎带应答
捎带应答其实就是基于延迟应答的基础上做了部分优化,让TCP效率变得更高一点
- 具体作用过程如下:
- 正常接受到一个请求时,系统内核会马上返回一个ACK
- 真正的响应内容是由程序制作的,于ACK制作的时机不同
- 由于延迟应答存在.ACK可能就会和应用响应合并在一起
虽然有捎带应答机制,但注意其并不是100%会发生的.该机制由系统内核进行处理
这就是为什么前面在讲解四次挥手时说的是通常不可以 而非完全不可行的原因了
-
下面是正常情况
服务端 客户端 服务端 客户端 request ACK response ACK
-
下面是触发捎带应答情况
服务端 客户端 服务端 客户端 request ACK + reponse ACK
TCP核心机制9 面向字节流
前面我们知道TCP是通过数据流来进行传输的.既然是通过数据流,那么就意味着通信的数据没有被分割,即粘包问题
-
什么是粘包问题?
接受方 发送方 接受方 发送方 数据(aaa) 数据(bbb) 数据(ccc)
对于接收方.它处理给应用的不是aaa和bbb和ccc三条数据,而是aaabbbccc这一条数据
这种发送的数据不经过处理会自动粘在一起的现象就是粘包问题
-
如何解决粘包问题?
上面我们知道造成粘包问题的原因是通信的数据无法被分割
既然流读写无法自动分割,那么我们自己定义一个分隔符就行了(类似于\n这样)
- 引入分隔符
接受方 发送方 接受方 发送方 数据(aaa\n) 数据(bbb\n) 数据(ccc\n)
虽然对于接收方收到的原始数据仍然是aaa\nbbb\nccc\n这样的粘包数据.但由于我们引入了分割符,这样的数据在应用层可以解析为aaa和bbb和ccc三条数据(前提是应用层提前设置好分隔符)
不仅可以通过引入分割符来解决.还有一个很好理解的方法,就是在每一段数据前记录一下发送的数据长度,这个长度以内对于的就是一条数据,长度以外就是别的数据
2. 引入数据长度
接受方 发送方 接受方 发送方 数据(3aaa) 数据(2bb) 数据(4cccc)此时接收方收到的数据是3aa2bb4cccc.这条数据只需要在应用层简单解析一下就能分清除这是三条数据(先解析第一个数字3,说明接下来3个字符都是第一条数据,即aaa;再解析aaa后面的数字2...)
粘包问题只需要在定义应用层协议稍微考虑一下就能解决.对于流传输的操作(文件IO等)也会有这样的粘包问题
- 引入分隔符
TCP核心机制10 异常情况
两台主机在通信过程中除了上面所说的丢包问题.还会有以下几种常见的异常情况
-
进程崩溃
-
主机关机
-
主机断电
-
网线断开
下面我们解释一下TCP协议在遇到各类异常情况的处理方法
-
进程崩溃
一个程序如果在通信到一半时出于不可抗力原因,这个程序崩溃了.那么此时正在通信的TCP协议会怎么样呢?
实际上在应用程序中这个和正常的断开连接(四次挥手)没有区别
对于触发FIN报文.不是仅可通过程序正常调用close触发的.在进程退出,崩溃时也会自动触发FIN.甚至即使程序崩溃了,程序还会等待四次握手完毕
主机B 主机A 主机B 主机A 程序错误崩溃 TCP连接仍然正常 等待一段时间后关闭 FIN ACK FIN ACK
-
主机关机
我们在电脑关机的时候,有时电脑会提示正在等待某些应用程序关闭.这个等待的过程就是操作系统给主机上的所有运行中程序通知要关机了,赶快停止运行.应用程序收到关机提醒后,会强制关闭程序,而这个强制关闭程序的过程本质上和上文进程崩溃是一样的
主机B 主机A 主机B 主机A 程序收到关机提示 如果四次挥手快,能运行完下面的通信流程 如果四次挥手慢,在运行到这之前主机A就下线了 FIN ACK FIN ACK
当关机较快导致主机A提前下线时,主机B会反复重传FIN报文.如果都没有收到ACK那么主机B最后会给主机A发送一个RST报文,即向主机A通知我已强制退出该连接.之后主机B不会管主机A有没有返回ACK,单方面断开连接了
-
主机断电
对于这一场景,应用程序来不及反应,可能在传输数据传到一半就突然下线了.而另一方如果长时间没收到回应,便也会发送一个RST报文来单方面断开连接
- 发送方断电
-
主机B 主机A 主机B 主机A 发生断电 长时间未收到新数据不断发送"心跳包" 多次未收到ACK,强制退出连接 删除连接 数据(1001~2000) ACK(20001) 心跳包(不带载荷的数据包,用于判断对方是否正常工作) 心跳包 心跳包 心跳包 RST
- 接收方断电
-
主机B 主机A 主机B 主机A 发生断电 长时间未收到ACK不断重发数据包 多次未收到ACK,强制退出连接 删除连接 数据(1001~2000) ACK(20001) 数据(2001~3000) 数据(2001~3000) 数据(2001~3000) 数据(2001~3000) 数据(2001~3000) 数据(2001~3000) RST
-
网线断开
这个场景和上面的主机掉电非常类似
虽然网络断开主机并未关机,但在通信的另一方眼里和断电没有区别,都是突然下线.故此处不再赘述
至此,我们终于整理完了TCP的绝大部分核心机制.
不过聪明的你一定注意到怕还有部分内容未提及(URG和PSH报文).现在我再做最后的补充内容(选项内容多且杂乱,不做整理)
- 补充内容:
- URG
当一个报文中URG标志位为1时,证明16位紧急指针有效
紧急指针就和它的名字一样,具有紧急属性
前面我们说过,TCP处理数据是按照16位序列号来进行处理的:在第1000个序列号数据处理完之前,绝不对处理第1001个数据.
而这个URG的作用便是绕过这个机制.假设URG的紧急指针是序列号为2000的数据,那么在另一方,即使第1000个序列号数据还没处理完,也会优先处理这个2000的数据 - PSH
PSH的作用是刷新缓冲区.通常TCP协议都会等发送的多个小数据包攒积在缓冲区,等缓冲区满了再发送.但当发送一个报文中含有PSH的数据时i,会直接刷新缓冲区,不管缓冲区满没满都会直接发送.通过在这个报文可以使通信的延迟稍微降低
- URG
最后我们来说一个TCP和UDP不同的点
广播支持性
- 什么是广播?
此时我们要知道一个相对的概念:单播
单播:一对一交流
就像两个人互相交流,一个人说的话只给另一个人听
与之相对,
广播:一对多交流
就像上课,老师一个人说的话给多个学生听
我们知道TCP更侧重于可靠性,而UDP更侧重于传输效率
在网络中,TCP是不支持广播的,而UDP支持广播