🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:マリンブルーの庭園—ずっと真夜中でいいのに。
0:34━━━━━━️💟──────── 3:34
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
连接管理机制
连接建立—三次握手
报文丢失的问题
为什么需要三次握手?
连接终止—四次挥手
为什么要等待 2MSL 呢?
谁先终止通信的问题
整体的理解三次握手和四次挥手
细节处理
accept的问题
listen的问题
处理服务器关闭后不能立即重启的问题
连接管理机制
TCP(Transmission Control Protocol)是一种可靠的、面向连接的传输层协议,它提供了一种可靠的、字节流形式的数据传输服务。TCP连接管理机制包括连接建立、连接维护和连接终止等过程,主要通过三次握手和四次挥手来实现。
- 连接建立(Three-Way Handshake):
- 第一步:客户端向服务器发送一个SYN(同步)包,表明客户端请求建立连接,并选择一个初始的序列号(这个序列号是随机的)。
- 第二步:服务器收到客户端的SYN包后,会发送一个ACK(确认)包作为应答,并将确认号设置为客户端的序列号加1,同时也会发送一个SYN包给客户端,表明服务器也愿意建立连接,并选择一个初始的序列号。
- 第三步:客户端收到服务器的SYN和ACK包后,会发送一个ACK包作为应答,确认收到了服务器的确认,并将确认号设置为服务器的序列号加1,此时连接建立完成。
- 连接维护:
- 一旦连接建立成功,TCP会维护连接的状态信息,包括序列号、确认号、窗口大小、拥塞窗口大小等。TCP使用序列号和确认号来保证数据的可靠传输,通过超时重传和选择性重传等机制来处理丢失的数据包,同时通过拥塞控制机制来避免网络拥塞。
- 连接终止(Four-Way Handshake):
- 第一步:一方(通常是客户端)发送一个FIN包,表明它已经完成了所有数据的发送,并请求关闭连接。
- 第二步:另一方(通常是服务器)收到FIN包后,会发送一个ACK包作为应答,确认收到了FIN包。
- 第三步:服务器发送一个FIN包给客户端,表明服务器也已经完成了所有数据的发送,并请求关闭连接。
- 第四步:客户端收到服务器的FIN包后,发送一个ACK包作为应答,确认收到了服务器的FIN包,此时连接关闭完成。
TCP连接管理机制通过三次握手和四次挥手确保了连接的建立、维护和终止,保证了数据的可靠传输和网络的稳定性。下面对每一步进行详细的介绍:
连接建立—三次握手
三次握手,我们以客户端向服务器请求建立连接为例:
通过前面的学习我们知道SYN是一个请求连接的标志位,而ACK为响应连接,确认的标志位。
客户端在一开始是没有任何状态的,也就是关闭状态的。当他要进行三次握手,同服务器建立连接时。只要把SYN发出去了(这个报文中包含了客户端的初始序列号(seq)),那么他的状态就会被置为SYN_SENT,也被称为同步发送。接着,只要服务器收到了连接请求报文,那么服务器的状态就会置为SYN_RCVD。接着他会回一个SYN+ACK的响应请求连接。
后续客户端收到了服务器的报文后会返回一个ACK报文(期间他们的状态是没变的,保持SYN_SENT和SYN_RCVD),服务器在接收到客户端发送的ACK包后,服务器会进入到连接建立成功的状态,即ESTABLISHED状态,双方可以开始进行数据传输。大致的图示如下:
报文丢失的问题
case1:若在客户端第一次向服务器请求连接时的SYN报文丢失,那么服务器不会受到任何影响,因为服务器并没有收到任何报文,而客户端则会超时重传。
case2:若服务器向客户端响应的SYN+ACK报文丢失,那么可能会导致三次握手失败。而此之间,客户端等带服务器响应如果在一定时间内(通常是操作系统设定的默认超时时间)没有收到服务器的响应,则客户端会重新发送SYN包并重新开始计时等待。由于服务器已经将响应报文发出,那么服务器无法收到客户端的ACK确认,因此无法确定连接是否成功建立,也无法进入到下一个状态。这样的情况下,服务器会保持在SYN_RCVD状态,直到超时或者收到客户端的重传SYN包。这样最终可能就会引起客户端的重传行为。
case3:若前面的报文都正常接受,但是客户端向服务器应答的ACK报文丢失,那么对于客户端来说,由于ACK已经发出,那么就认为三次握手成功了。但是对于服务器来说,只有收到了ACK才能认为成功建立连接。此时,
- 服务器处于半开放连接状态(半连接状态):服务器在接收到客户端发送的SYN包后,会向客户端发送SYN+ACK报文,并进入SYN_RCVD状态等待客户端的ACK确认。如果客户端发送的ACK报文丢失了,服务器无法收到客户端的确认,此时服务器处于半开放连接状态。在这种情况下,服务器等待一段时间后可能会超时,关闭连接。
- 服务器可能重传SYN+ACK报文:如果服务器在等待超时后仍未收到客户端的ACK确认,可能会重传SYN+ACK报文。服务器会继续重传,直到收到客户端的ACK确认或达到最大重传次数。
- 客户端认为建立好连接了,那么就会直接向服务器发信息,而服务器没建立好连接,当收到信息报文后,服务器就会立即重置连接,响应RST给客户端,双方会重新建立连接。大致的图示如下:
为什么需要三次握手?
先明确一个目标:三次握手是为了建立连接。前两次握手都是验证客户端到服务端的通路是否通畅。这两次握手只是保证了一次收和发是成功的。也就说保证了客户端的收发是正常的,而服务器的收是正常的。那么只要客户端给服务器应答,服务器成功收到,也就说明服务器的发也是正常的。这样就验证了双方的通路是通畅的!验证了全双工的特效。
如果只进行一次握手,那么客户端每发起一个建立连接请求,那么服务器都认为是建立成功的,服务器不需要响应应答。但是,客户端是有很多的,而服务器只有一个,很容易导致服务器连接资源被很快被占满的问题,影响后续的连接建立。这也被称为SYN洪水。
如果只进行两次握手,也就是说明只要服务器给出了ACK应答那么就说明建立成功了。但是,如果这个应答客户端没有收到呢?那么客户端就会认为没建立成功,可是服务器认为建立成功了啊!(这与三次握手是相反的情况)这就导致服务器一直挂着大量的异常连接,导致连接异常的成本佳节给了服务器!
所以,总结一下,三次握手是为了:1. 设计的优越性,一旦建立连接出现异常,成本嫁接到客户端,而不是由服务器承担,这样服务器建立失败的成本就会降低。2、减少SYN洪水的攻击可能。3. 能够验证双方的通信信道的通畅情况,验证双方全双工的特性。
连接终止—四次挥手
为什么要进行四次挥手呢?这是因为断开的本质是双方都没有数据给对方发了,而双方都是有可能会给对方发数据的,所以要断开连接,就需要征得双方的同意,不能只征得一方,双方的地位是对等的,四次挥手可以是双方以最小成本断开。还是以客户端和服务器为例:
通过前面的学习我们知道FIN表明此报文段的发送端的数据已发送完毕,并要求释放运输连接的标志位。
如果要结束连接,那么就要发送FIN报文。客户端要结束连接,那么客户端先第一次挥手,发送FIN后,他的状态变为FIN_WAIT1(在这个状态下,客户端仍然可以接收来自服务器的数据,但不再发送数据),而服务器收到FIN报文后服务器状态变为CLOSE_WAIT(CLOSE_WAIT状态表示本地端已经收到了远程端发送的FIN包,并等待本地应用程序处理完毕数据,准备关闭连接)然后服务器第二次挥手向客户端回复ACK。客户端收到ACK后状态变为FIN_WAIT2(这个状态表明客户端已经发送了关闭连接的请求,并且收到了服务器的确认,但还没有收到服务器发送的FIN包)。接着如果服务器也想断开连接了就会第三次挥手向客户端发送FIN,接着服务器状态变为LAST_ACK(表示本地端不再发送数据)。客户端收到服务器的FIN后会第四次挥手向服务器回复ACK,然后进入TIME_WAIT状态(在这个状态下,表示连接已经被正常关闭,但为了确保数据传输的可靠性,本地端会等待一段时间后才能完全关闭连接)。而服务器收到ACK后直接进入CLOSE状态(CLOSE状态通常指的是连接已经完全关闭,不再接收或发送任何数据,且不再占用任何系统资源的状态)。客户端在TIME_WAIT后会等待2MSL(2MSL表示一个TCP报文在网络中的最长存活时间)才进入CLOSE状态。大致的图示如下:
而在发送往最后一次ACK后客户端才认为完成四次挥手,而服务端在收到ACK后才会认为完成四次挥手。意味着:主动断开的一方一定是发送最后一次ACK的一方!
为什么要等待 2MSL 呢?
- 确保网络中所有数据包被丢弃:在网络中,数据包可能会因为延迟、重复或乱序而导致连接关闭过程中的混乱。等待 2MSL 的时间可以确保所有相关的数据包都已经被丢弃,从而避免在新的连接中出现问题。
- 防止旧连接数据干扰新连接:如果不等待足够长的时间,旧连接的数据包可能会混入到新连接中,导致数据错乱或错误。通过等待 2MSL,可以确保任何旧连接的残留数据都已经在网络中消失。
- 保证连接状态同步:在等待 2MSL 的过程中,连接的两端可以保持连接状态同步,确保双方都能正确地处理关闭连接的过程。
谁先终止通信的问题
1.如果客户端是主动终止通信的一方,那么服务器在接收到FIN会变为CLOSE_WAIT状态,而客户端在接受应答后会变为FIN_WAIT2,如果不做下一步的动作,那么服务器为长时间保存CLOSE_WAIT。
2.如果服务器是主动终止通信的一方,那么如果完成了所有的四次挥手动作,那么服务器将会处于TIME_WAIT状态,需要等待一段时间才会变为CLOSE状态,由于服务器IP+PORT正在被使用,这也就导致了我们如果在关闭服务器后想要马上重启会发送绑定端口失败的原因。那为什么客户端不会出现呢?因为客户端使用的是随机的端口号!
整体的理解三次握手和四次挥手
理解完成完上面的知识,那么结合系统提供的接口,下面我们全面的理解三次握手和四次挥手的全过程:
细节处理
accept的问题
在TCP/IP协议中,三次握手用于建立客户端和服务器之间的连接,而四次挥手则用于断开这个连接。accept函数在这个过程中对于上层起到了关键的作用。但是,实际上连接成功的与否与accept是没关系的。三次握手是双方在系统层自动完成的!accept是作为在双方建立成功连接后,对于上层的显示。
listen的问题
前面的文章中我们提到listen函数原型通常如下:
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
其中
backlog
:这是等待连接队列的最大长度。换句话说,这是可以等待处理的未完成连接请求的最大数量。当队列满时,新的连接请求可能会被拒绝。这个值通常设置为5或更大,但具体的值取决于你的应用需求和系统限制。
现在再来理解一下backlog这个参数:这是可以等待处理的未完成连接请求的最大数量。当队列满时,新的连接请求可能会被拒绝。backlog参数告诉操作系统在内核中创建的连接队列的大小。连接队列用于存储已经到达但尚未被服务器accept调用接受的连接请求。当服务器正在处理连接时,新的连接请求会被放在这个队列中等待。默认的,我设置的允许建立连接的数量backlog+1。那么如果允许建立连接的数量满了,又来了新的建立连接怎么办呢?这里就在三次握手这里做文章了,服务端会在最后面也就是当服务端处于SYN_RECV的状态将本该接受的ACK报文给丢弃,这样这个连接就会处于SYN_RECV。但是客户端却认为是建立成功了处于上面提到的丢失报文的case3问题。但是服务端不会长时间的维持SYN_RECV状态(半连接状态),过一段时间会被消除。
那么这个backlog
是越大越好吗?为什么是不能设置过大也不能过小呢?这是因为
- 过大的backlog:
- 过大的backlog可能会占用过多的系统资源,导致服务器性能下降。每个挂起连接都需要一定的内存资源来维护,如果连接队列过大,会消耗大量的内存资源,影响服务器的正常运行。
- 另一方面,即使设置了很大的backlog值,实际上服务器也可能无法处理大量的挂起连接,这取决于服务器的处理能力。如果服务器无法及时处理连接队列中的请求,那么增加backlog的大小也不会有太大的帮助。
- 过小的backlog:
- 过小的backlog可能会导致新的连接请求被拒绝,因为连接队列已经满了。如果服务器繁忙或者连接负载较高,过小的backlog会导致部分连接请求无法被及时处理,客户端可能会收到连接被拒绝的错误。
处理服务器关闭后不能立即重启的问题
前面我们也提到过setsockopt这个函数:
setsockopt
是一个用于设置套接字选项的系统调用,它允许程序员设置各种与套接字相关的参数,以控制套接字的行为和特性。下面是对 setsockopt
的详细解释:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd
:表示要设置选项的套接字文件描述符。level
:表示选项的级别,通常为 SOL_SOCKET,也可以是其他协议特定的值。optname
:表示要设置的选项名称。optval
:指向包含新选项值的缓冲区。optlen
:表示optval
缓冲区的大小。
- 常用的选项:
SO_REUSEADDR
:允许在同一端口上启动多个套接字,用于解决 TIME_WAIT 状态下不能立即重用相同端口的问题。- SO_REUSEPORT :允许多个套接字在同一台主机上监听相同的 IP 地址和端口号,而不会因为地址和端口已经被占用而失败。
SO_KEEPALIVE
:启用 TCP 的 keepalive 机制,检测连接是否仍然有效。SO_LINGER
:控制关闭连接时套接字的行为,可设置等待时间。SO_SNDBUF
和SO_RCVBUF
:设置发送和接收缓冲区的大小。TCP_NODELAY
:禁用 Nagle 算法,立即发送数据。
- 设置选项的值:
- 对于大多数选项,
optval
是一个指向包含新选项值的缓冲区的指针。缓冲区中存储的是选项值的数据结构,其类型和格式取决于具体的选项。optlen
表示optval
缓冲区的大小,即选项值的长度。
- 返回值:
- 如果设置成功,
setsockopt
返回 0。- 如果设置失败,返回 -1,并设置全局变量
errno
指示错误类型。
我们可以设置如上的SO_REUSEADDR
选项SO_REUSEPORT选项来解决这个问题。
如下是一个例子:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt);
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~