TCP协议是工作中最常用到的协议。
TCP协议格式:
TCP头部共计20个字节,其中前16个字节为源端口号、目的端口号、序列号、确认号、数据偏移、保留和控制位字段,后4个字节为窗口大小和校验和字段。
说明:
URG:第一位:紧急指针(urgent pointer)的有效位。当这个标志位被设为1时,表示紧急数据在包里面,并且紧急指针字段有效。紧急数据是指比正常数据更重要的数据,需要优先处理。紧急指针指示了紧急数据的位置。
ACK:第二位:确认序号(acknowledgment number)的有效位。当这个标志位被设为1时,表明确认号字段中包含有效信息。
PSH:第三位:推送(push)标志位。当这个标志位被设为1时,表示接收方应该立即将数据交给应用层,而不需要等到缓冲区满了再交付。
RST:第四位:复位(reset)标志位。当这个标志位被设为1时,表示连接中出现严重错误,需要重新建立连接。
SYN:第五位:同步序号(synchronize sequence numbers)的有效位。在建立连接时用于同步序号。
FIN:第六位:结束(finish)标志位。当这个标志位被设为1时,表示发送方已经没有数据发送,并要求释放连接。
这些标志位组合起来可以描述TCP连接的状态和控制信息。通过设置不同的标志位,可以实现可靠的数据传输、连接的建立和释放等操作。
其他内容我们放在后续讲解。
网络通信是复杂的,无法确保发送出去的数据,一定可以到达。这里的可靠是指,发送方能够知道对方是否接收到了数据。
TCP确保可靠性的最核心机制称为"确认应答":
简单来说,就是当 发送方发送一个TCP数据报时,接收方接收到后会发送一个确认应答(ACK报文),表示已经收到数据。
但是在网络传输过程中,传输的速度是不太稳定的,发送方现在快速的发送了两条数据,就有可能出现后发送的数据先到达的情况,这个时候,接受方返回一个确认应答,发送方法要怎么知道这次应答的数据是哪个呢?所以TCP还引入了序号和确认序号,对数据进行编号,应答报文里就会说明这次应答的数据是哪个。应答报文里回复的序号就叫确认序号。
注意:
在网络传输中,如果传输的数据的传输线路负载过大,就有可能发送 "丢包" 的情况,数据就无法到达,所以,TCP有超时重传功能,如果发送方发出了数据,在一段时间内没有收到确认应答,那么发送发就会认为对方没有接收到数据,就会重新发一次数据。
注意:这里如果是ACK报文丢了,在发送方看来和发送的数据丢了没有区别,它是感知不到的,也会重新发送,所以有可能接收方会收到两次相同的数据,为了解决这个问题,TCP socket 在内核中存在一个接收缓冲区(一块内存空间),当放送方发来数据时,会先存储到缓冲区,然后程序调用 对应方法 才能读到数据,当数据到达缓冲区时,接受方会检查当前缓冲区是否有这个数据,或者是否曾经存在过这个数据,如果存在过,就会把这个数据丢弃,并且再次发送ACK报文,防止发送方再次发送。
接受方判断数据受否已经接收过的方法:
1. 如果两个数据都在缓冲区里
接收方会把新收到的数据一 一对比序号 即可发现重复数据
2.如果已经有一个数据被读走了
应用程序读取数据时,是按序号顺序读取的,以确保读到的数据连续,即从序号小的读到序号大的,应该读的下一个数据还没有到,则会阻塞等待,socket 的 api会 储存上次读的最后一个数据,所以只需要判断该数据的序号是否大于上次读的最后一个数据,大于则说明不是重复数据。
注意:
使用TCP传输数据之前要先建立连接。
连接的过程 被称为 "三次握手"
当服务器收到客户端的ACK报文段后,就完成了三次握手,建立了双方之间的连接。此时,数据传输的窗口已经确定,并且双方都知道对方的初始序列号和其他参数,可以开始进行可靠的数据传输。
通过三次握手,TCP协议确保了双方的状态同步和正确性。客户端和服务器都能确认对方的接收和发送能力,并建立起可信任的连接。
三次握手还能防止上次连接中发送的数据被下次连接的数据接收到:
假设一个客户端短时间内和服务器建立了两次连接,第一次连接时发送的某个数据还没到达服务器,就已经断开连接了,直到建立了第二次连接之后才到达,显然,这个数据包是一个 "错误的" 数据包 ,第一次握手中 “客户端选择一个初始序列号” 就可以解决这个问题,选择初始序号的策略会使每次连接的初始序号差异非常大,所以就能非常容易的识别出是否是本次连接的数据包。
四次挥手是指在TCP连接中断开连接时的一种过程。由于TCP是全双工的通信协议,所以在关闭连接时需要双方都发送确认消息。
具体的四次挥手过程如下:
第一次挥手(FIN):关闭方向对方发送一个带有FIN标志的数据包,表示自己已经没有数据要发送了,但仍可以接收数据。
第二次挥手(ACK):对方接收到第一次挥手后,会发送一个带有ACK标志的数据包作为确认,表示已经收到了关闭方的请求,并准备好关闭连接。
第三次挥手(FIN):对方发送一个带有FIN标志的数据包,表示自己也没有数据要发送了。
第四次挥手(ACK):关闭方接收到第三次挥手后,会发送一个带有ACK标志的数据包作为确认,表示已经收到了对方的请求,并准备好关闭连接。
通过这个四次挥手过程,双方可以完成连接的断开,并释放相关的资源,以确保连接的正常关闭。这样,双方都可以安全地结束通信。
注意:
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 端口号 查看对应的端口状态
TCP在确认应答机制下,每发送方接收到一个ACK才会发送下一个数据,导致大量的时间都消耗在等待ACK上了。
滑动窗口就是为了解决上述问题。
滑动窗口就是从 "一次发一个数据" 变为 "一次发多个数据"。
滑动窗口,会一次发送多个数据,当收到当前窗口中序号最小的数据的ACK,窗口就后移一位发送一个新的数据。这样就把一次等待一个ACK变为了同时等待多个ACK,提高了效率。
1. 数据已经到了,ACK丢了:
这里我们先重新说一下ACK的含义是:表示确认序号之前的数据已经收到了。
如图,假设2001 和3001 丢失了,1001 和 4001正常到达,由于4001正常到达,所以发送方就会知道4001以前的数据都已经到达了,也就不会重新发送 。
如果 4001也没有到达,则发送方会从2000开始重新发送数据。
2. 数据丢了
如图假设数据 1001 - 2000 丢包了,所以就不会有它的ack,而后面 1001 到 4000 的数据 返回的确认应答都会变成 1001,表示下一个发的数据应该是1001开头的。当返回1001的ACK数量达到某个阈值,发送方就会重传数据1001 - 2000,这里的重传叫做快速重传此时返回的序号是下一个应该发送的数据,即4001。
通过滑动窗口可以提高传输效率,窗口越大,即传输多少数据不等ACK,数据传输速度越快,但是如果数据发送的太快,导致接收方处理不过来,把接收缓冲区填满了,后续的数据就会丢包,所以就需要控制好发送速度与接收方处理速度能够达到平衡。这就叫做流量控制。
在TCP报头中有个16位的字段叫窗口大小,ACK报文中会在这个字段中设置接下来要发送的窗口设置为多少合适,发送方接收到这个ACK就会根据这个值来调整自己的窗口大小。由于储存窗口的字节数只有两个字节,有时候可能会不够用,所以在TCP报头中的 选项 中,还包含了一个参数叫做 窗口扩展因子,实际上真正要设置的窗口大小是16位窗口大小 * 2 ^ 窗口扩展因子。
接收方会按自己的接收缓冲区剩余空间来设置窗口大小,如果窗口大小为0了,发送方将不会发送数据,如果过了重发超时的时间,发送方还没有接收到窗口更新的ACK,就会发送一个窗口探测的包,让接收方再发一个ACK查看窗口大小,如果已经是非0了,则会继续发送数据,考略到ACK可能丢包的情况,所以发送方,时不时就会发送窗口探测包。
如果接收方处理数据很快,如果发送方和接收方中间的通信路径出现了问题,如果发送速度过快,会导致通信路径负载更大,导致数据丢包。
如果按照某个窗口大小发送数据后,出现了丢包,这时就认为通信路径存在拥堵,于是就减小窗口大小,如果没出现丢包则认为中间路径不拥堵,就增加窗口大小。
注意:最后实际的窗口大小是取流量控制的窗口大小和拥塞控制的窗口大小的较小值。
拥塞控制的控制过程:
由此我们发现,拥塞控制的过程是一个动态平衡的过程,这是因为网络的情况是多变的
同样是基于滑动窗口的优化。
通过延时应答机制,接收方接收到数据后,不会立刻返回ACK,而是会延时一会再返回ACK,在这段时间内,接收方就能处理掉更多的数据,于是返回的窗口大小就会更大,同时如果这期间有其他数据到达,这时则只需要返回最后到的数据的ACK即可,于是又省去了一部分开销。
尽可能的把能合并的数据包合并发送,从而提高效率。
世家开发中,客户端和服务器之间通常是一问一答的情况,当客户端发送一个请求时,服务器接收到请求后,因为存在延时应答不马上发送ACK,如果在这个延时时间 内响应已经被准备好了,此时就会把响应数据包和ACK合并,此时客户端接收到响应也不会马上返回ACK,如果后续客户端还有请求则也可能合并在一起发送......
由于TCP是字节流传输,当发送方发送了多个数据,而接收方没来的及处理,可能会出现,两个无关的数据紧密连接在一起,分不清数据的边界的情况就叫粘包。
如图,我们读数据的时候怎么读才是正确的呢。
解决方案:
1. 通过特殊的符号作为分隔符,看到分隔符就视为读取到了一个完整的数据了。
2. 指定数据的长度,在数据开始的位置,使用一段空间储存整个数据的长度。读取的时候按数据的长度来读。
注意:
考虑丢包更严重的情况,甚至网络出现故障等情况如何处理。