计算机网络传输层-TCP三次握手底层详情

张开发
2026/4/16 1:32:29 15 分钟阅读

分享文章

计算机网络传输层-TCP三次握手底层详情
TCP连接过程1. TCP的头部字段2. TCP三次握手过程3. TCP为什么需要三次握手4. TCP三次握手客户端第三次发送的确认包丢失了发生什么5. 三次握手和accept是什么关系accept做了哪些事情6. 客户端发送第一个SYN报文服务器没有接收到怎么办7. 假设客户端重传了SYN报文后服务端这边又收到重复的SYN报文怎么办8. 第一次握手客户端发送SYN服务端回复ACK报文这个过程服务端内部做了什么9. 大量SYN包发送给服务端会发生什么事情1. TCP的头部字段序列号在建立连接时由计算机生成数计数作为初始值通过YSN包传递给接收主机后面每发送一次数据就累加一次该数据字节数的大小。用来解决网络包乱序问题。确认应答号接收端接收到信号后下一次期待收到的数据的序列号发送端收到该确认应答号就知道接收端该号之前的数据已经接收到了。用来解决丢包问题。控制位ACK-确认位该为为1时确认应答的字段变为有效TCP规定除了最初建立连接时的SYN包之外的包该位必须设为1。SYN-同步位用于发起一个连接并同步序列号。该位只有在连接3次握手前两步出现当SYN1ACK0时表明这是一个连接请求报文。RST-重置位当该位为1时表示TCP连接出现了严重错误必须强制释放连接重写连接。FIN-终止位用于释放连接。当该位为1时表示发送方数据已经发送完毕没有数据要发送请求断开TCP连接。2. TCP三次握手过程TCP是面向连接的协议因此使用TCP前必须建立连接连接是通过三次握手完成的。步骤1客户端发起SYN请求1.SYN同步客户端行动标志位SYN1ACK0序列化初始化序列号x状态变化客户端发送报文后状态从CLOSED变为SYN-SENT同步已发送。服务器接收服务器接收到该报文后会创建一个包含以一下关键信息的TCP报文段作为响应。步骤2服务器响应SYNACK服务器行动服务器接收到客户端SYN请求会创建并发送一个包含以下关键信息的TCP报文段标志位YSN1ACK1序列号初始序列号y确认号x1确认收到客户端seqx并请求下一个字节状态变化服务器发送报文后状态变为SYN-RECEIVED同步已接收。客户端接收客户端接收后会再次发送一个ACK报文。步骤3客户端最后确认3. ACK确认客户端行动客户端确认服务端的SYNACK报文后发送以下关键信息的报文段标志位YSN0ACK1序列号x1自己的序列号加1确认号y1 确认收到了服务器序列号y的数据请求下一个字节状态变化客户端发送报文后其状态变为ESTABLISHED已建立连接服务器接收到ACK报文后状态也变为SETABLISHED。注意三次握手前两次不可以携带数据最后一次可以携带数据。3. TCP为什么需要三次握手确认双方的收发能力第一次握手服务器知道客户端具备发送能力。第二次握手客户端知道服务器收发能力正常知道服务器具备发送接收能力。第三次握手服务器知道自己收发能力正常客户端收发能力正常。如果不进行三次握手服务端无法知道自己发送的数据是否被客户端接收到也无法确认客户端是否已经准备好接收数据。防止旧的已经失效的连接突然传到连接端这是很经典的原因客户端发送一个连接请求A因为网络原因A在网络中滞留了。客户端很长时间没有收到服务器的的第二次握手响应于是以为丢包了于是重写发送连接请求B顺利完成了两次握手连接通信结束后关闭了连接。这是服务器突然收到了连接请求A。如果是两次握手连接服务器接收到了A的请求后会立即创建连接等待客户端发送数据。但客户端已经不使用这个连接了这会导致服务器的资源浪费。如果是三次握手连接服务端接收到A后会回一个ACK客户端发现这并不是自己当前的请求序号于是拒绝响应连接建立失败。三次握手客户端拒绝响应的过程客户端判断出这是一个无效响应。会给服务器发送一个RST复位标志位为1的TCP报文。客户端收到RST报文后立刻明白刚才连接是无效的。它会终止连接过程并释放半连接的内存等资源并重新恢复到LISTEN监听状态。同步序列号TCP是可靠传输靠的是序列号。三次握手的过程其实是双方交换初始序列号的过程。客户端告诉服务器我的初始序列号是X服务器回复收到X我的初始序列号是Y最后客户端再回一句收到Y。总结为什么不是两次只能保证单向通畅容易产生死锁或浪费资源。为什么不是四次理论上可以但没有必要。因为第二次握手服务端把对客户端的确认ACK和自己的同步请求YSN合并成同一个包发送从而提高了效率。4. TCP三次握手客户端第三次发送的确认包丢失了发生什么双方当前的状态客户端在发出第三个ACK包后客户端状态为ESTABLISHED已建立连接。在客户端视角里它可以发送应用层数据了。服务端由于没收到ACK它的状态停留在SYN_RCVD半连接状态这个连接会暂时存放在服务器的半连接队列SYN Queue中。服务器的自救超时重连服务器有个定时器如果超时没接收到ACK服务器会以为自己的第二次SYN和ACK丢失或者客户端第三次ACK丢失于是重写发送SYN和ACK。如果重传后还是没有收到ACK服务器会采取指数退避的方式例如等待1s2s4s8s…继续重传。在Linux内核中有个参数net.ip4.tcp_synack-retries控制最大重传次数通常默认是5次。接下来3种核心场景场景1客户端没有发送数据空闲等待1. 客户端处于SETABLISHED状态但没有立即发送数据。2. 服务器多次重传SYNACK。3. 客户端每次接收到重传的SYNACK都会重新回复一个ACK。4. 如果其中某次ACK重新到达服务端服务端状态转为ESTABLISHED握手最终成功。场景2客户端立刻发送数据网络层自动携带ACK1. 客户端发送第三次ACK后立即发送数据。2. TCP协议在封装数据报文时会自动把ACK置1并且携带确认号。3. 如果携带数据和ACK报文到达处于SYN_RCVD状态的服务端服务端会提取其中的ACK信息顺利完成三次握手状态转为SETABLISHED然后正常接收这批数据。4. 这个机制极大的提高了网络通信的容错率。场景3服务端已经放弃连接客户端才发送数据1. 服务端重传次数超过上限会丢弃该半连接恢复到LISTEN状态。2. 过了很久客户端才发送应用层数据。3. 服务器接收到数据包后发现这不是一个正常连接的数据包。4. 服务端立即回复一个RST包。5. 客户端收到RST包知道该连接崩溃套接字API会向应用层抛出Connection reset by peer的错误客户端随即关闭连接。5. 三次握手和accept是什么关系accept做了哪些事情它们是完全解耦的关系。握手是内核全自动完成的 TCP 的三次握手完全由底层的 Linux 内核网络协议栈负责根本不需要应用程序用户态代码的参与。独立存在 只要服务端调用了 listen()哪怕代码里一直不调用 accept()只要内核的队列没满客户端依然可以成功完成三次握手建立连接。accept 只是负责“消费”已经建立好的连接。accept 具体做了哪些事情当应用程序调用 accept() 时它本质上是一个“消费者”主要做了以下三件事消费全连接队列 它会去检查内核维护的全连接队列Accept Queue。这个队列里存放的都是已经完成三次握手、状态为 ESTABLISHED 的连接。分配新的文件描述符核心 如果队列里有连接accept 会把这个连接摘取出来并在内核中为它分配一个新的“已连接套接字文件描述符connfd”返回给用户态。这个新的 FD 专门用来和当前客户端进行读写而原来的监听 FD 继续去接待新连接。提取客户端信息 它会将连进来的客户端的 IP 地址和端口号等元信息从内核态拷贝到我们传入的 sockaddr 结构体内存中。#includeiostream#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.hintmain(){// 1. 初始化监听 Socket (为了简洁省略了前置的错误检查)intlistenfdsocket(AF_INET,SOCK_STREAM,0);structsockaddr_inserv_addr{};serv_addr.sin_familyAF_INET;serv_addr.sin_porthtons(8080);serv_addr.sin_addr.s_addrINADDR_ANY;bind(listenfd,(structsockaddr*)serv_addr,sizeof(serv_addr));listen(listenfd,128);// 开始监听维护全连接队列std::cout服务器已启动正在监听 8080 端口...std::endl;// 核心 accept 逻辑 // 2. 准备传出参数用于接收客户端的 IP 和端口信息structsockaddr_inclient_addr{};socklen_t client_lensizeof(client_addr);// 3. 调用 accept从全连接队列中取出一个已完成三次握手的连接// 注意默认情况下如果队列为空这里会阻塞等待intconnfdaccept(listenfd,(structsockaddr*)client_addr,client_len);if(connfd0){std::cerr获取连接失败std::endl;return-1;}// 4. 成功获取连接通过传出参数解析客户端信息std::cout\n[成功] 获取到新连接std::endl;std::cout客户端 IP : inet_ntoa(client_addr.sin_addr)std::endl;std::cout客户端 Port : ntohs(client_addr.sin_port)std::endl;std::cout分配的通信FD: connfdstd::endl;// // 5. 释放资源close(connfd);// 关闭与该客户端的通信连接close(listenfd);// 关闭监听套接字return0;}6. 客户端发送第一个SYN报文服务器没有接收到怎么办客户端的”自救“超时重传与指数退避当客户端调用 connect() 发出SYN后内核TCP协议会启动一个定时器。如果在这个时间内没有接收到 SYNACK 响应客户端会认为SYN包丢了并重新发送SYN报文。为了无脑发包重传采用的是指数避让即每次等待时间翻倍1s2s4s…。Linux内核中的重传细节Linux中控制重传报文次数是有限的由内核参数 net.ip4.tcp_syn_retries 控制默认一般是5次。假如第一次失败后续重传等待时间为1s2s4s8s16s。重传次数耗尽不会立即确认连接失败会等待最后一次时间的两倍32s这个时间结束还没有收到 SYNACK 响应就可以确认彻底失败。C Socket API 的表现阻塞模式默认你调用connect()会一直卡住 挂起当前线程。直到成功或彻底失败失败后才会返回-1并且全局错误码erron被设置为ETIMEDOUT连接超时。#includeiostream#includesys/socket.h#includenetinet/in.h#includearpa/inet.h#includeunistd.hintmain(){// 1. 初始化监听 Socket (为了简洁省略了前置的错误检查)intlistenfdsocket(AF_INET,SOCK_STREAM,0);structsockaddr_inserv_addr{};serv_addr.sin_familyAF_INET;serv_addr.sin_porthtons(8080);serv_addr.sin_addr.s_addrINADDR_ANY;bind(listenfd,(structsockaddr*)serv_addr,sizeof(serv_addr));listen(listenfd,128);// 开始监听维护全连接队列std::cout服务器已启动正在监听 8080 端口...std::endl;// 核心 accept 逻辑 // 2. 准备传出参数用于接收客户端的 IP 和端口信息structsockaddr_inclient_addr{};socklen_t client_lensizeof(client_addr);// 3. 调用 accept从全连接队列中取出一个已完成三次握手的连接// 注意默认情况下如果队列为空这里会阻塞等待intconnfdaccept(listenfd,(structsockaddr*)client_addr,client_len);if(connfd0){std::cerr获取连接失败std::endl;return-1;}// 4. 成功获取连接通过传出参数解析客户端信息std::cout\n[成功] 获取到新连接std::endl;std::cout客户端 IP : inet_ntoa(client_addr.sin_addr)std::endl;std::cout客户端 Port : ntohs(client_addr.sin_port)std::endl;std::cout分配的通信FD: connfdstd::endl;// // 5. 释放资源close(connfd);// 关闭与该客户端的通信连接close(listenfd);// 关闭监听套接字return0;}非阻塞模式配合 epoll connect() 会立刻返回 -1 且 errno 为 EINPROGRESS表示正在建立连接。你可以把这个 Socket 加入 epoll 监听可写事件。如果底层 SYN 重传最终失败epoll 会触发 EPOLLERR 严重错误或 EPOLLHUP连接挂断事件此时通过 getsockopt 检查套接字错误就能拿到 ETIMEDOUT。7. 假设客户端重传了SYN报文后服务端这边又收到重复的SYN报文怎么办继续发送第二次握手报文因为不知道第一次给客户端的SYNACK是不是丢包了。8. 第一次握手客户端发送SYN服务端回复ACK报文这个过程服务端内部做了什么服务端收到客户端SYN后会把连接放到半连接队列中然后向客户端发送SYN和ACK包服务端收到第三次握手ACK后将连接从半连接队列移入全连接队列等待进程调用accept函数把连接取出来。注意不管是半连接还是全连接都有最大连接限度超过队列限度的连接直接丢弃或者返回RST。9. 大量SYN包发送给服务端会发生什么事情有可能导致TCP半连接队列满了这样后续的SYN报文就会丢弃导致客户端和服务器无法连接。开启 SYN Cookies 终极杀招开启 nrt.ipv4.tcp_syncookiesecho1/proc/sys/net/ipv4/tcp_syncookies当半连接队列被打满时服务端不再为新的 SYN 请求分配队列内存而是根据源 IP、端口等信息通过密码学哈希计算出一个 Cookie并把它作为 SYNACK 的初始序列号ISN发回去。如果对方是正常的客户端它会在第三次握手的 ACK 中把这个 Cookie 加 1 发回来。服务端通过校验这个序列号就能确认合法性并直接建立连接从而绕过了半连接队列的限制。减小 SYNACK 重传次数修改 net.ipv4.tcp_synack_retries将其从默认的 5 降低到 1 或 2让服务器更快地放弃那些不回复的“僵尸”连接加速队列内存的回收。扩大半连接队列容量适当调大 net.ipv4.tcp_max_syn_backlog但这只是治标不治本通常配合 SYN Cookies 一起使用。防火墙与限流在系统层使用 iptables 限制单个 IP 发送 SYN 包的速率或者在云服务商层面开启 DDoS 高防 IP 清洗流量。

更多文章