1.TCP协议
TCP协议是工作中最常用到的协议。
TCP协议格式:
- 源端口号(16位):源端口标识发送方的应用程序。
- 目的端口号(16位):目的端口标识接收方的应用程序。
- 序列号(32位):用于标识每个TCP报文段的数据部分的字节流,序列号表示这个字节流中第一个字节的序号。
- 确认序号(32位):表示期望收到的下一个字节的序号,即上次已成功接收的字节序号加1。
- 首部长度(4位):指定TCP头部长度,以4字节为单位。
- 保留(6位):保留字段,未使用。
- 标志位(6位):包括URG、ACK、PSH、RST、SYN和FIN六种控制位,用于描述TCP连接的状态和控制数据传输。
- 窗口大小(16位):表示接收方的缓冲区大小,用于告知发送方可以发送的数据量。
- 校验和(16位):用于检验TCP报文段是否有误。如果校验和为0,说明报文段没有发生错误。
- 紧急指针(16位):用于指示数据流中的紧急数据。
- 选项(可变长度):TCP头部中可以包含一些可选的字段,如最大报文段长度、时间戳等。
- 数据区:包含要传输的数据。
TCP头部共计20个字节,其中前16个字节为源端口号、目的端口号、序列号、确认号、数据偏移、保留和控制位字段,后4个字节为窗口大小和校验和字段。
说明:
- 首部长度:1个字节大小范围为,0 - 15,单位是4个字节,也就是说明TCP报头最大长度是 15 * 4 = 60个字节,也就是 选项 最多可以占40个字节。
- 保留位:为将来可能的功能扩展备用。
- 标志位:
URG:第一位:紧急指针(urgent pointer)的有效位。当这个标志位被设为1时,表示紧急数据在包里面,并且紧急指针字段有效。紧急数据是指比正常数据更重要的数据,需要优先处理。紧急指针指示了紧急数据的位置。
ACK:第二位:确认序号(acknowledgment number)的有效位。当这个标志位被设为1时,表明确认号字段中包含有效信息。
PSH:第三位:推送(push)标志位。当这个标志位被设为1时,表示接收方应该立即将数据交给应用层,而不需要等到缓冲区满了再交付。
RST:第四位:复位(reset)标志位。当这个标志位被设为1时,表示连接中出现严重错误,需要重新建立连接。
SYN:第五位:同步序号(synchronize sequence numbers)的有效位。在建立连接时用于同步序号。
FIN:第六位:结束(finish)标志位。当这个标志位被设为1时,表示发送方已经没有数据发送,并要求释放连接。
这些标志位组合起来可以描述TCP连接的状态和控制信息。通过设置不同的标志位,可以实现可靠的数据传输、连接的建立和释放等操作。
其他内容我们放在后续讲解。
1.1 TCP的可靠传输
网络通信是复杂的,无法确保发送出去的数据,一定可以到达。这里的可靠是指,发送方能够知道对方是否接收到了数据。
TCP确保可靠性的最核心机制称为"确认应答":
简单来说,就是当 发送方发送一个TCP数据报时,接收方接收到后会发送一个确认应答(ACK报文),表示已经收到数据。
但是在网络传输过程中,传输的速度是不太稳定的,发送方现在快速的发送了两条数据,就有可能出现后发送的数据先到达的情况,这个时候,接受方返回一个确认应答,发送方法要怎么知道这次应答的数据是哪个呢?所以TCP还引入了序号和确认序号,对数据进行编号,应答报文里就会说明这次应答的数据是哪个。应答报文里回复的序号就叫确认序号。
注意:
- TCP是字节流传输,所以每个字节都会有一个序号并且是连续的,但由于我们传输的数据是连续的,所以TCP报头中只需要只需要存第一个字节的序号,后续的序号可以通过计算获得。
- 应答报文中的确认序号,是按照接收到的最后一个字节的序号再加1,可以理解为,表示确认序号前面的数据已经收到,或者接下来该发送的数据的开始序号
1.2 超时重传
在网络传输中,如果传输的数据的传输线路负载过大,就有可能发送 "丢包" 的情况,数据就无法到达,所以,TCP有超时重传功能,如果发送方发出了数据,在一段时间内没有收到确认应答,那么发送发就会认为对方没有接收到数据,就会重新发一次数据。
注意:这里如果是ACK报文丢了,在发送方看来和发送的数据丢了没有区别,它是感知不到的,也会重新发送,所以有可能接收方会收到两次相同的数据,为了解决这个问题,TCP socket 在内核中存在一个接收缓冲区(一块内存空间),当放送方发来数据时,会先存储到缓冲区,然后程序调用 对应方法 才能读到数据,当数据到达缓冲区时,接受方会检查当前缓冲区是否有这个数据,或者是否曾经存在过这个数据,如果存在过,就会把这个数据丢弃,并且再次发送ACK报文,防止发送方再次发送。
接受方判断数据受否已经接收过的方法:
1. 如果两个数据都在缓冲区里
接收方会把新收到的数据一 一对比序号 即可发现重复数据
2.如果已经有一个数据被读走了
应用程序读取数据时,是按序号顺序读取的,以确保读到的数据连续,即从序号小的读到序号大的,应该读的下一个数据还没有到,则会阻塞等待,socket 的 api会 储存上次读的最后一个数据,所以只需要判断该数据的序号是否大于上次读的最后一个数据,大于则说明不是重复数据。
注意:
- 重传次数是有限的,重传次数到某个阈值,还没有ACK就会认为连接存在问题,就会重置连接,,如果重置也失败,就直接放弃连接。
- 重传的超时时间也不是固定不变的,随着重传次数增加,超时时间也会增加
1.3 连接管理
1.3.1 建立连接(三次握手)
使用TCP传输数据之前要先建立连接。
连接的过程 被称为 "三次握手"
- 第一次握手:首先客户端会给服务器发送一个不带载荷的数据报: SYN(同步报文段),请求建立连接。这个报文段中,客户端选择一个初始序列号(sequence number)并设置SYN标志位为1,同时指定自己的接收窗口大小和其他选项。这个报文段称为SYN报文段。
- 第二次握手:服务器收到客户端的SYN报文段后,会确认请求并回复一个带有SYN和ACK(确认序号)标志位的报文段。服务器选择一个初始序列号并设置SYN和ACK标志位为1,同时指定自己的接收窗口大小和其他选项。这个报文段称为SYN-ACK报文段。
- 第三次握手:客户端收到服务器的SYN-ACK报文段后,会向服务器发送一个带有ACK标志位的报文段作为确认。客户端将确认序号设置为服务器发来的序列号加1,并设置ACK标志位为1。这个报文段称为ACK报文段。
当服务器收到客户端的ACK报文段后,就完成了三次握手,建立了双方之间的连接。此时,数据传输的窗口已经确定,并且双方都知道对方的初始序列号和其他参数,可以开始进行可靠的数据传输。
通过三次握手,TCP协议确保了双方的状态同步和正确性。客户端和服务器都能确认对方的接收和发送能力,并建立起可信任的连接。
三次握手还能防止上次连接中发送的数据被下次连接的数据接收到:
假设一个客户端短时间内和服务器建立了两次连接,第一次连接时发送的某个数据还没到达服务器,就已经断开连接了,直到建立了第二次连接之后才到达,显然,这个数据包是一个 "错误的" 数据包 ,第一次握手中 “客户端选择一个初始序列号” 就可以解决这个问题,选择初始序号的策略会使每次连接的初始序号差异非常大,所以就能非常容易的识别出是否是本次连接的数据包。
1.3.2 断开连接(四次挥手)
四次挥手是指在TCP连接中断开连接时的一种过程。由于TCP是全双工的通信协议,所以在关闭连接时需要双方都发送确认消息。
具体的四次挥手过程如下:
-
第一次挥手(FIN):关闭方向对方发送一个带有FIN标志的数据包,表示自己已经没有数据要发送了,但仍可以接收数据。
-
第二次挥手(ACK):对方接收到第一次挥手后,会发送一个带有ACK标志的数据包作为确认,表示已经收到了关闭方的请求,并准备好关闭连接。
-
第三次挥手(FIN):对方发送一个带有FIN标志的数据包,表示自己也没有数据要发送了。
-
第四次挥手(ACK):关闭方接收到第三次挥手后,会发送一个带有ACK标志的数据包作为确认,表示已经收到了对方的请求,并准备好关闭连接。
通过这个四次挥手过程,双方可以完成连接的断开,并释放相关的资源,以确保连接的正常关闭。这样,双方都可以安全地结束通信。
注意:
- 四次挥手一般不能像三次握手一样,把2,3合二为一,因为四次挥手中的2,3的发送时机不同,FIN是通过socket.close()触发的,当服务器或客户端收到FIN时,会立刻返回ACK,而返回FIN之前还需要执行一些代码逻辑,如果让ACK等待FIN一起发送的话,可能会让发送端认为数据没有到达而触发超时重传。
- 三次握手,必须是客户端主动发起,四次挥手,客户端/服务器都可以发起。
1.4 TCP 连接/断开 中的状态转换
TCP状态和线程的状态类似
TCP连接过程中涉及的状态转换主要包括以下几个状态:
-
CLOSED:表示初始状态,表示连接未建立或已经关闭。
-
LISTEN:表示服务器端正在监听来自客户端的连接请求(服务器已经创建好serverSocket)。
-
SYN_SENT:表示客户端发送了连接请求,并等待服务器端的确认。
-
SYN_RECEIVED:表示服务器端接收到客户端的连接请求,并发送确认。
-
ESTABLISHED:表示连接已经建立,双方可以进行数据传输。
-
FIN_WAIT_1:表示连接关闭的第一阶段,在这个状态下,一方已经发送了连接关闭请求。
-
CLOSE_WAIT:表示另一方已经发送了连接关闭请求,当前方还在进行数据传输。
-
FIN_WAIT_2:表示连接关闭的第二阶段,在这个状态下,另一方已经确认了连接关闭请求。
-
LAST_ACK:表示连接关闭的最后阶段,在这个状态下,一方发送了最后的确认。
-
TIME_WAIT:表示连接关闭后的等待状态,用于确保网络中所有延迟的数据都被接收完毕,即确认最后一个ACK是否到达,如果超过某个时间对方没有重传则视为到达。
-
CLOSED(2nd):表示连接已经完全关闭。
可以在控制台输入:netstat -ano | findstr 端口号 查看对应的端口状态
1.5 滑动窗口
TCP在确认应答机制下,每发送方接收到一个ACK才会发送下一个数据,导致大量的时间都消耗在等待ACK上了。
滑动窗口就是为了解决上述问题。
滑动窗口就是从 "一次发一个数据" 变为 "一次发多个数据"。
滑动窗口,会一次发送多个数据,当收到当前窗口中序号最小的数据的ACK,窗口就后移一位发送一个新的数据。这样就把一次等待一个ACK变为了同时等待多个ACK,提高了效率。
1.5.1 滑动窗口出现丢包的情况
1. 数据已经到了,ACK丢了:
这里我们先重新说一下ACK的含义是:表示确认序号之前的数据已经收到了。
如图,假设2001 和3001 丢失了,1001 和 4001正常到达,由于4001正常到达,所以发送方就会知道4001以前的数据都已经到达了,也就不会重新发送 。
如果 4001也没有到达,则发送方会从2000开始重新发送数据。
2. 数据丢了
如图假设数据 1001 - 2000 丢包了,所以就不会有它的ack,而后面 1001 到 4000 的数据 返回的确认应答都会变成 1001,表示下一个发的数据应该是1001开头的。当返回1001的ACK数量达到某个阈值,发送方就会重传数据1001 - 2000,这里的重传叫做快速重传此时返回的序号是下一个应该发送的数据,即4001。
1.5.2 流量控制
通过滑动窗口可以提高传输效率,窗口越大,即传输多少数据不等ACK,数据传输速度越快,但是如果数据发送的太快,导致接收方处理不过来,把接收缓冲区填满了,后续的数据就会丢包,所以就需要控制好发送速度与接收方处理速度能够达到平衡。这就叫做流量控制。
在TCP报头中有个16位的字段叫窗口大小,ACK报文中会在这个字段中设置接下来要发送的窗口设置为多少合适,发送方接收到这个ACK就会根据这个值来调整自己的窗口大小。由于储存窗口的字节数只有两个字节,有时候可能会不够用,所以在TCP报头中的 选项 中,还包含了一个参数叫做 窗口扩展因子,实际上真正要设置的窗口大小是16位窗口大小 * 2 ^ 窗口扩展因子。
接收方会按自己的接收缓冲区剩余空间来设置窗口大小,如果窗口大小为0了,发送方将不会发送数据,如果过了重发超时的时间,发送方还没有接收到窗口更新的ACK,就会发送一个窗口探测的包,让接收方再发一个ACK查看窗口大小,如果已经是非0了,则会继续发送数据,考略到ACK可能丢包的情况,所以发送方,时不时就会发送窗口探测包。
1.5.3 拥塞控制
如果接收方处理数据很快,如果发送方和接收方中间的通信路径出现了问题,如果发送速度过快,会导致通信路径负载更大,导致数据丢包。
如果按照某个窗口大小发送数据后,出现了丢包,这时就认为通信路径存在拥堵,于是就减小窗口大小,如果没出现丢包则认为中间路径不拥堵,就增加窗口大小。
注意:最后实际的窗口大小是取流量控制的窗口大小和拥塞控制的窗口大小的较小值。
拥塞控制的控制过程:
- 慢启动:刚开始传输数据时速度是比较小的,采用的窗口大小也比较小。
- 如果传输的数据没有丢包,此时就按照当前窗口的二倍来增大窗口
- 如果一直按当前的二倍来增大窗口,后续会增加的太开而突然导致网络拥塞, 所以这里引入了一个阈值,当当前窗口达到阈值后,还要继续增大窗口时,会变为线性增长。
- 线性增长一段时间后,仍然会导致数据传输太快引起丢包,一旦出现丢包,就把拥塞窗口减半,然后重新增长,并且会更具丢包时的窗口大小重新设置指数增长到线性增长的阈值。
由此我们发现,拥塞控制的过程是一个动态平衡的过程,这是因为网络的情况是多变的
1.5.4 延时应答
同样是基于滑动窗口的优化。
通过延时应答机制,接收方接收到数据后,不会立刻返回ACK,而是会延时一会再返回ACK,在这段时间内,接收方就能处理掉更多的数据,于是返回的窗口大小就会更大,同时如果这期间有其他数据到达,这时则只需要返回最后到的数据的ACK即可,于是又省去了一部分开销。
1.5.5 捎带应答
尽可能的把能合并的数据包合并发送,从而提高效率。
世家开发中,客户端和服务器之间通常是一问一答的情况,当客户端发送一个请求时,服务器接收到请求后,因为存在延时应答不马上发送ACK,如果在这个延时时间 内响应已经被准备好了,此时就会把响应数据包和ACK合并,此时客户端接收到响应也不会马上返回ACK,如果后续客户端还有请求则也可能合并在一起发送......
1.6 粘包问题
由于TCP是字节流传输,当发送方发送了多个数据,而接收方没来的及处理,可能会出现,两个无关的数据紧密连接在一起,分不清数据的边界的情况就叫粘包。
如图,我们读数据的时候怎么读才是正确的呢。
解决方案:
1. 通过特殊的符号作为分隔符,看到分隔符就视为读取到了一个完整的数据了。
2. 指定数据的长度,在数据开始的位置,使用一段空间储存整个数据的长度。读取的时候按数据的长度来读。
注意:
- 上述问题需要在应用层考虑。
- 由于UDP每次传输的都是一个完整的数据,并且UDP的接收缓冲区是类似一个i链表的结构,每个数据都是分开的,所以不存在粘包问题。
- 之前介绍的各种常用的应用层协议的格式 xml json protobuffer 都能处理好粘包问题。
1.7 异常情况
考虑丢包更严重的情况,甚至网络出现故障等情况如何处理。
- 通信的其中一方进程崩溃了
进程无论是正常结束还是异常崩溃,系统都会完成回收文件资源,关闭文件等操作,就会触发四次挥手,TCP连接的生命周期可以比进程更长一些,虽然进程已经退出了,但是TCP连接还在,仍然可以进行四次挥手,即进程不在了但是系统中仍然持有连接信息。
- 如果一方出现了关机(正常流程关机)
当主机关机时,会先强制终止所有进程,终止进程时,就会触发四次挥手,但是关闭进程后,系统就会关闭,所以四次挥手的流程不一定能够全部完成,但至少也能把第一个FIN发给对端,对端收到FIN之后也会进入释放连接的流程,返回ACK以及发送FIN,这个FIN收不到ACK就会进行重传,当重传达到一定次数就会重置连接,显然重置连接也会失败,于是就会单方面释放连接信息。
- 如果一方出现了断电
如果直接断电,相当于瞬间关机,此时一定是来不及四次挥手。
这里还要分两种情况:
1)断电的是接收方,发送方就会发现,突然没有ACK了,就会重传,重传一定次数后就会重置连接,即发送复位报文段RST,此时重置也不行,就会单方面放弃连接。
2)断电的是发送方,接收方一段时间没有接收到数据就会周期性地发送"心跳包"来询问对方的情况,即探查连接的情况,如果多次没有回应就会单方面释放连接。
- 网线断开
这个情况即情况3中 1 2 情况的结合