三、确认应答机制
TCP保证可靠性的机制之一就是确认应答机制
确认应答机制就是由TCP报头中的32位序号和32位确认序号来保证的。确认应答机制不是保证双方通信的全部消息的可靠性,而是通过收到对方的应答消息,来保证曾经发送给对方的某些消息被对方可靠的收到了
TCP将每个字节的数据都进行编号
TCP是面向字节流的,可以将TCP的发送缓冲区和接收缓冲区都想象成一个字符数组
上层应用拷贝到TCP发送缓冲区中的每一个字节数据天然有一个序号,即字符数组的下标,只不过这个下标是从1开始向后递增的
双方在通信时,本质就是将发送缓冲区中的数据远程拷贝到对方的接收缓冲区中
发送方发送数据时报头中所填的序号,实际就是发送的若干字节数据中,首个字节数据在发送缓冲区当中对应的下标
接收方接收到数据进行响应时,响应报头中的确认序号实际就是,接收缓冲区中接收到的最后一个有效数据的下一个位置所对应的下标
当发送方收到接收方的响应后,就可以从下标为确认序号的位置继续进行发送了
四、超时重传机制
双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内若得不到对方的应答,此时发送方就会进行数据重发
TCP保证双方通信的可靠性,一部分是通过TCP的协议报头体现的,还有一部分是通过实现TCP的代码逻辑体现的。如超时重传机制就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传,这就是通过TCP的代码逻辑实现的,在TCP报头中是体现不出来的
丢包的多种情况
丢包有多种情况,一种是发送的数据报文丢失了,此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传
一种情况是对方发来的响应报文丢包了,此时发送端也会因为收不到对应的响应报文(以为发送失败),而进行超时重传
当出现丢包时,发送方是无法辨别是发送的数据报文丢失了,还是对方发来的响应报文丢失了,因为这两种情况下发送方都收不到对方发来的响应报文,此时发送方就只能进行超时重传。
若是对方的响应报文丢失而导致发送方进行超时重传,此时接收方就会再次收到一个重复的报文数据。但不用担心,接收方可以根据报头中的32位序号来判断曾经是否收到过这个报文,并且进行报文去重
当发送缓冲区中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区中删除或覆盖,而会让其保留在发送缓冲区中,以免需要进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖
超时重传的等待时间
超时重传的时间不能设置的太长也不能设置的太短
超时重传的时间设置的太长,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率
超时重传的时间设置的太短,会导致对方收到大量的重复报文,可能对方发送的响应报文还在网络中传输而并没有丢包,但此时发送方就开始进行数据重传了,并且发送大量重复报文会也是对网络资源的浪费
因此超时重传的时间一定要是合理的,最理想的情况就是找到一个最小的时间,保证"确认应答一定能在这个时间内返回"。但这个时间的长短,是与网络环境有关的。网好的时候重传的时间设置的短一点,网卡的时候重传的时间设置的长一点,即超时重传设置的等待时间一定是上下浮动的,不可能是固定的某个值
TCP为了保证无论在任何环境下都能有比较高性能的通信,动态计算这个最大超时时间
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍
若重发一次之后,仍然得不到应答,下一次重传的等待时间就是2×500ms
若仍然得不到应答,那么下一次重传的等待时间就是4×500ms。以此类推,以指数的形式递增
当累计到一定的重传次数后,TCP就会认为是网络或对端主机出现了异常,进而强转关闭连接
五、连接管理机制
TCP面向连接
TCP的各种可靠性机制都不是从主机到主机的,而是基于连接的,与连接是强相关的。如一台服务器启动后有多个客户端访问,若TCP不是基于连接的,也意味着服务器端只有一个接收缓冲区,此时各个客户端发来的数据都会拷贝到这个接收缓冲区中,此时这些数据就会互相干扰
在进行TCP通信之前需要先建立连接,就是因为TCP的各种可靠性保证都是基于连接的,要保证传输数据的可靠性的前提就是先建立好连接
操作系统对连接的管理
面向连接是TCP可靠性的一种,只有在通信建立好连接才会有各种可靠性的保证,而一台机器上可能会存在大量的连接,此时操作系统就得对这些连接进行管理。
操作系统在管理这些连接时需要"先描述,再组织",在操作系统中一定有一个描述连接的结构体,该结构体中包含了连接的各种属性字段,所有定义出来的连接结构体都会以某种数据结构组织起来,此时操作系统对连接的管理就变成了对该数据结构的增删查改
建立连接,就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构中即可;断开连接,就是将某个连接从管理连接的数据结构中删除,释放该连接曾经占用的各种资源。因此连接的管理也是有成本的,这个成本就是管理连接结构体的时间成本,以及存储连接结构体的空间成本
5.1 三次挥手
双方在进行TCP通信之前需先建立连接,建立连接的过程被称为三次握手
以服务器和客户端为例,当客户端想要与服务器进行通信时,需要先与服务器建立连接,此时客户端作为主动方会先向服务器发送连接建立请求,然后双方TCP在底层会自动进行三次握手
第一次握手:客户端向服务器发送的报文中的SYN位被设置为1,表示请求与服务器建立连接
第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文中的SYN位和ACK位均被设置为1
第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应
客户端向服务器发起的连接建立请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的连接建立请求后,服务器也需向客户端发起连接建立请求,请求建立从服务器到客户端方法的通信连接
为什么是三次握手?而不是一次、两次、四次?
连接建立并不是百分之百成功的,通信双方在进行三次握手时,其中前两次握手能够保证被对方收到,因为前两次握手都有对应的下一次握手对其进行响应(没有响应则触发重发机制),但第三次握手是没有对应的响应报文的,若第三次握手时客户端发送的ACK报文丢失了,那么连接建立就会失败
虽然客户端发起第三次握手后就完成了三次握手,但服务器却没有收到客户端发来的第三次握手,此时服务器端就不会建立对应的连接结构体。所以建立连接时不管采用几次握手,最后一次握手的可靠性都是不能保证的
既然连接的建立都不是百分之百成功的,因此建立连接时具体采用几次握手的依据,实际是看几次握手时的优点更多
三次握手是验证双方信道通畅的最小次数:
TCP是全双工通信的,因此连接建立的核心要务实际是:验证双方的通信信道是否是连通的
而三次握手恰好是验证双方通信信道的最小次数,通过三次握手后双方就都能知道自己和对方是否都能够正常发送和接收数据
在客户端看来,当它收到服务器发来第二次握手时,说明发出的第一次握手被对方可靠的收到了,证明自己能发以及服务器能收,同时当自己收到服务器发来的第二次握手时,也就证明服务器能发以及自己能收,此时就证明自己和服务器都是能发能收的
在服务器看来,当它收到客户端发来第一次握手时,证明客户端能发以及自己能收,而当它收到客户端发来的第三次握手时,说明自己发出的第二次握手被对方可靠的收到了,也就证明自己能发以及客户端能收,此时就证明自己和客户端都是能发能收的
既然三次握手已经能够验证双方通信信道是否正常了,那么三次以上的握手自然也是可以验证的,但既然三次已经能验证了就没有必要再进行更多次的握手了
三次握手能够保证连接建立时的异常连接挂在客户端(风险转移):
当客户端收到服务器发来的第二次握手时,客户端就已经知道双方信道连通了,因此当客户端发出第三次握手后,这个连接就已经在客户端建立了;服务器只有收到客户端发来的第三次握手后,服务器才知道双方信道是连通的,此时服务器端才会建立对应的连接
因此双方在进行三次握手建立连接时,双方建立连接的时间点是不一样的。若客户端最后发出的第三次握手丢包了,此时在服务器端就不需要建立对应的连接,而客户端则需要短暂的维护一个连接
维护连接是需要时间成本和空间成本的,三次握手能保证连接建立异常时,这个异常连接是挂在客户端的,而不会影响到服务器。
虽然客户端需要短暂维护这个异常连接,但不会特别多。不像服务器,一旦多个客户端建立连接时都建立失败了,此时服务器端就需要耗费大量资源来维护这些异常连接
建立连接失败时的异常连接不会一直维护下去。若服务器端长时间收不到客户端发来的第三次握手,就会将第二次握手进行超时重传,此时客户端就有机会重新发出第三次握手。或者当客户端认为连接建立好后向服务器发送数据时,此时服务器会发现没有和该客户端建立连接而要求客户端重新建立连接(RST)
三次握手时的状态变化
三次握手时的状态变化如下:
最开始时客户端和服务器都处于CLOSED状态
服务器为了能够接收客户端发来的连接请求,需要由CLOSED状态变为LISTEN状态
此时客户端就可以向服务器发起三次握手了,当客户端发起第一次握手后,状态变为SYN_SENT状态
处于LISTEN状态的服务器收到客户端的连接请求后,将该连接放入内核等待队列中,并向客户端发起第二次握手,此时服务器的状态变为SYN_RCVD
当客户端收到服务器发来的第二次握手后,紧接着向服务器发送最后一次握手,此时客户端的连接已经建立,状态变为ESTABLISHED
而服务器收到客户端发来的最后一次握手后,连接也建立成功,此时服务器的状态也变成ESTABLISHED
至此三次握手结束,双方可以开始进行正常数据通信了
套接字和三次握手之间的关系
在客户端发起连接建立请求前,服务器调用对应listen函数,进入LISTEN状态
当服务器进入LISTEN状态后,客户端就可以向服务器发起三次握手了,客户端对应调用的就是connect函数
connect函数不参与底层的三次握手,connect函数的作用只是发起三次握手。当connect函数返回时,三次挥手已经结束(成功或失败)
若服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列中,服务器端需要通过调用accept函数将这个建立好的连接获取上来
当服务器端将建立好的连接获取上来后,双方就可通过调用read/recv函数和write/send函数进行数据交互了
5.2 四次挥手
四次挥手的过程
双方维护连接都是需要成本的,因此当双方TCP通信结束之后就需要断开连接,断开连接的这个过程被称为四次挥手
以服务器和客户端为例,当客户端与服务器通信结束后,需断开连接,就需进行四次挥手
第一次挥手:客户端向服务器发送的报文中的FIN位被设置为1,表示请求与服务器断开连接
第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应
第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求
第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应
为什么是四次挥手?
由于TCP是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。在断开连接时不仅要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手
某些情况下可能是三次挥手,第二次挥手与第三次挥手合并为一次(有些文章说不行,其实是可以合并的,只不过有条件,看下面的链接)
四次挥手中的第二次和第三次挥手在某些情况下不能合并在一起,因为第三次握手是服务器端想要与客户端断开连接时发给客户端的请求,而当服务器收到客户端断开连接的请求并响应后,不一定会马上发起第三次挥手,因为服务器可能还有某些数据要发送给客户端,只有当服务器端将这些数据发送完后才会向客户端发起第三次挥手
(141条消息) 我问老大:TCP 四次挥手,可以变成三次吗?_四次握手可以变三次-CSDN博客
https://blog.csdn.net/chenxuyuana/article/details/126558893
四次挥手的状态变化
挥手前客户端和服务器都处于连接建立后的ESTABLISHED状态
客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为FIN_WAIT_1
服务器收到客户端发来的连接断开请求后对其进行响应ACK,此时服务器的状态变为CLOSE_WAIT
当服务器没有数据需要发送给客户端时,服务器会向客户端发起断开连接请求,等待最后一个ACK到来,此时服务器的状态变为LASE_ACK
客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态
当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为CLOSED状态
处于TIME_WAIT状态的客户端则会等待2MSL(Maximum Segment Lifetime,报文最大生存时间)才会进入CLOSED状态
套接字与四次挥手之间的关系
客户端发起断开连接请求,对应客户端主动调用close函数
服务器发起断开连接请求,对应服务器主动调用close函数
一个close对应的就是两次挥手(一次请求、一次响应),双方都调用close,因此是四次挥手
CLOSE_WAIT
双方在进行四次挥手时,若只有客户端调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态
若服务器没有主动关闭不需要的文件描述符,此时服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少。因此若不及时关闭不用的文件描述符,除了会造成文件描述符泄漏,可能导致连接资源没有完全释放,即内存泄漏问题
因此在编写网络套接字代码时,若发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时可以检查是不是服务器没有及时调用close函数关闭对应的文件描述符
TIME_WAIT
第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传
第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传
第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传
第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传
若客户端在发出第四次挥手后立即进入CLOSED状态,此时服务器虽然进行了超时重传,但已经得不到客户端的响应了,因为客户端已经将连接关闭了
服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器进行超时重传期间并且维护这条废弃的连接,这对服务器非常不友好
为了避免这种情况,因此客户端在四次挥手后没有立即进入CLOSED状态,而是进入到了TIME_WAIT状态进行等待,此时要是第四次挥手的报文丢包了,客户端也能收到服务器重发的报文然后进行响应
TIME_WAIT状态存在的必要性:
客户端在进行四次挥手后进入TIME_WAIT状态,若第四次挥手的报文丢包了,客户端在一段时间内仍然能够接收服务器重发的FIN报文并对其进行响应,能够较大概率保证最后一个ACK被服务器收到
客户端发出最后一次挥手时,双方历史通信的数据可能还没有发送到对方。因此客户端四次挥手后进入TIME_WAIT状态,还可以保证双方通信信道上的数据在网络中尽可能的消散
第四次挥手丢包后,若网络状态出现了问题,尽管客户端还没有关闭连接,也收不到服务器重发的连接断开请求,此时客户端TIME_WAIT等若干时间最终也会关闭连接,而服务器经过多次超时重传后也会关闭连接。这种情况虽然也让服务器维持了闲置的连接,但毕竟是少数,引入TIME_WAIT状态就是让主动发起四次挥手的客户端承担主要成本(服务器不必维持TIME_WAIT状态)
TIME_WAIT的等待时长是多少?
TIME_WAIT的等待时长既不能太长也不能太短
太长会让等待方维持一个较长的时间的TIME_WAIT状态,在这个时间内等待方也需要花费成本来维护这个连接,浪费资源。太短可能没有达到最初目的,没有保证ACK被对方较大概率收到,也没有保证数据在网络中消散,此时TIME_WAIT就没有意义了。TCP协议规定,主动关闭连接的一方在四次挥手后要处于TIME_WAIT状态,等待两个MSL(报文最大生存时间)的时间才能进入CLOSED状态
MSL在RFC1122中规定为两分钟,但是各个操作系统的实现不同,如在Centos7上默认配置的值是60s。可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout命令来查看MSL
TIME_WAIT的等待时长设置为两个MSL的原因:
MSL是TCP报文的最大生存时间,因此TIME_WAIT状态持续存在2MSL,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已消散
同时也是在理论上保证最后一个报文可靠到达的时间