【Linux网络】Linux 网络编程:传输层TCP(四) 个人主页艾莉丝努力练剑❄专栏传送门《C语言》《数据结构与算法》《C/C干货分享学习过程记录》《Linux操作系统编程详解》《笔试/面试常见算法从基础到进阶》《Python干货分享》⭐️为天地立心为生民立命为往圣继绝学为万世开太平 艾莉丝的简介文章目录前言一、思维导图二、导入语1 ~ TCP连接管理机制1.1 TCP协议基础1.1.1 TCP首部格式1.1.2 数据传输缓冲区机制1.2 三次握手原理与状态流转标准三次握手流程双方完整状态转换1.3 四次挥手原理与完整状态流转标准四次挥手流程特殊状态说明1.4 CLOSE_WAIT状态详解产生原因与危害实际案例与排查1.5 TIME_WAIT状态详解存在的两个核心意义危害1.6 地址复用技术解决bind失败SO_REUSEADDR通用解决方案SO_REUSEPORTLinux 3.9新增1.7 连接管理机制知识图谱2 ~ 内核层面TCP连接的本质2.1 连接的内核数据结构嵌套关系2.2 内核多态机制的实现2.3 核心结构体详解struct file文件抽象层struct socketBSD套接字层struct sock通用套接字层struct tcp_sockTCP连接核心2.4 方法表机制struct proto_opsBSD层方法表拥塞控制算法表2.5 “如何理解连接”知识图谱3 ~ TCP可靠性保障机制3.1 确认应答(ACK)机制3.2 超时重传机制超时时间动态计算3.3 快速重传机制两种丢包场景处理4 ~ TCP流量控制机制4.1 基本原理4.2 零窗口处理4.3 窗口扩大因子代码设置4.4 流量控制优化机制延迟应答捎带应答4.5 流量控制知识图谱5 ~ 初识TCP滑动窗口机制5.1 引入背景5.2 滑动窗口的本质与实现5.3 标准工作流程5.4 异常场景处理ACK丢失数据包丢失5.5 滑动窗口初识知识图谱6 ~全文总结1. 连接管理2. 内核连接本质3. 可靠性保障4. 流量控制5. 滑动窗口结尾前言一、思维导图二、导入语TCP作为传输层最核心的可靠协议其连接管理与数据传输机制是网络编程的基石。我们在实际开发中遇到的服务器端口占用无法重启、“大量CLOSE_WAIT导致服务器卡死”、“大文件传输效率低下”、粘包问题导致数据解析错误等问题本质上都是对TCP连接状态、缓冲区管理、可靠性机制、流量控制与滑动窗口理解不透彻导致的。本文将从TCP协议首部格式出发系统梳理三次握手与四次挥手的完整状态流转、超时重传与快速重传的实现原理、流量控制的优化机制以及滑动窗口如何在保证可靠性的同时最大化传输效率。所有内容均基于Linux内核源码与实际调试案例帮助你建立从协议格式到内核实现的完整知识链条。1 ~ TCP连接管理机制1.1 TCP协议基础1.1.1 TCP首部格式TCP首部是TCP协议所有功能的载体标准长度为20字节最多可扩展至60字节包含40字节选项字段长度含义源端口号16位标识发送端进程目的端口号16位标识接收端进程32位序号32位本报文段第一个数据字节的编号32位确认序号32位期望接收的下一个字节的编号确认号之前的所有数据已正确接收4位首部长度4位以4字节为单位最大15×460字节保留位6位预留未使用必须为06位标志位6位控制报文的类型和行为 • URG紧急指针有效 • ACK确认序号有效 • PSH提示接收端立即将数据上交应用层 • RST强制重置连接 • SYN同步序号请求建立连接 • FIN发送端数据发送完毕请求关闭连接16位窗口大小16位接收端当前可接收的最大字节数流量控制的核心16位校验和16位覆盖TCP首部和数据部分用于检测传输错误16位紧急指针16位标识紧急数据的最后一个字节的序号选项0~40字节支持窗口扩大因子、时间戳、MSS协商等扩展功能1.1.2 数据传输缓冲区机制当应用层调用read()/write()/send()/recv()等I/O系统调用时数据并没有直接发送到网络中只是在应用进程地址空间和内核TCP缓冲区之间进行拷贝。TCP协议栈完全自主控制以下所有传输细节数据发送时机受Nagle算法、拥塞控制、滑动窗口影响每次发送的数据量根据对方接收窗口和当前网络状况数据出错处理超时重传、快速重传、重复数据丢弃数据的确认与重排序这意味着应用层无法干预TCP的传输细节只能通过系统调用与内核缓冲区交互。1.2 三次握手原理与状态流转TCP是面向连接的协议任何数据传输之前必须先通过三次握手建立连接。三次握手的本质是双方确认彼此的发送和接收能力正常并协商初始序列号。标准三次握手流程客户端调用connect()发送SYN报文SYN1序号ISN_c进入SYN_SENT状态服务器收到SYN后回复SYNACK报文SYN1ACK1确认号ISN_c1序号ISN_s进入SYN_RCVD状态客户端收到SYNACK后回复ACK报文ACK1确认号ISN_s1进入ESTABLISHED状态服务器收到ACK后进入ESTABLISHED状态连接建立完成双方完整状态转换服务器端CLOSED → LISTEN调用listen后→ SYN_RCVD收到SYN→ ESTABLISHED收到ACK客户端CLOSED → SYN_SENT调用connect发送SYN→ ESTABLISHED收到SYNACK1.3 四次挥手原理与完整状态流转TCP是全双工协议连接的两个方向可以独立关闭。四次挥手的本质是双方分别确认对方的关闭请求确保两个方向的数据都传输完毕。标准四次挥手流程主动关闭方调用close()发送FIN报文进入FIN_WAIT_1状态被动关闭方收到FIN后回复ACK报文进入CLOSE_WAIT状态主动关闭方收到ACK后进入FIN_WAIT_2状态被动关闭方数据传输完毕后调用close()发送FIN报文进入LAST_ACK状态主动关闭方收到FIN后回复ACK报文进入TIME_WAIT状态被动关闭方收到ACK后进入CLOSED状态主动关闭方等待2MSL时间后进入CLOSED状态特殊状态说明三次挥手当主动关闭方发送FIN时被动关闭方恰好也没有数据要发送会将ACK和FIN合并在一个报文中回复此时跳过FIN_WAIT_2状态直接进入TIME_WAITCLOSING状态双方同时发起关闭请求主动关闭方在收到对方的FIN之前先收到了对方的ACK此时进入CLOSING状态收到FIN后再进入TIME_WAIT1.4 CLOSE_WAIT状态详解CLOSE_WAIT是被动关闭方在收到FIN并回复ACK后进入的状态此时被动关闭方的TCP层已经知道对方要关闭连接但**应用层还没有调用****close()**释放文件描述符。产生原因与危害产生原因服务器代码中只调用了accept()获取连接但在客户端断开后没有调用close()关闭对应的文件描述符危害每个CLOSE_WAIT连接都会占用一个文件描述符和内核资源当文件描述符耗尽后服务器将无法接受新的连接最终卡死实际案例与排查// 典型错误代码只accept不closebool TcpServer::Start(Handler handler){CHECK_RET(listen_sock_.Socket());CHECK_RET(listen_sock_.Bind(ip_,port_));CHECK_RET(listen_sock_.Listen(5));for(;;){TcpSocket new_sock;std::string ip;uint16_tport0;if(!listen_sock_.Accept(new_sock,ip,port)){continue;}printf([client %s:%d] connect!\n,ip.c_str(),port);for(;;){std::string req;bool retnew_sock.Recv(req);if(!ret){printf([client %s:%d] disconnect!\n,ip.c_str(),port);// 缺少new_sock.Close()导致CLOSE_WAITbreak;}std::string resp;handler(req,resp);new_sock.Send(resp);}}returntrue;}使用netstat -natp | grep 端口号可以看到大量处于CLOSE_WAIT状态的连接且PID都指向同一个服务器进程。1.5 TIME_WAIT状态详解TIME_WAIT是主动关闭方在完成四次挥手后进入的状态会持续2MSLMaximum Segment Lifetime报文最大生存时间。RFC1122规定MSL为2分钟Linux系统默认配置为60秒因此TIME_WAIT时长为120秒。存在的两个核心意义保证网络中迟到的报文自然消散TCP报文在网络中可能存在延迟如果主动关闭方立即释放端口并重启服务器可能会收到上一个连接的迟到报文将其误认为是新连接的数据保证最后一个ACK可靠到达如果最后一个ACK丢失被动关闭方会重发FIN报文。此时主动关闭方的TCP连接仍然存在可以重发ACK如果没有TIME_WAIT主动关闭方已经进入CLOSED状态会回复RST报文导致被动关闭方异常危害主动关闭方在TIME_WAIT期间会占用完整的通信五元组源IP、源端口、目的IP、目的端口、协议。当服务器主动关闭大量短连接时会产生大量TIME_WAIT连接导致新的客户端连接因端口耗尽而失败。1.6 地址复用技术解决bind失败为了解决TIME_WAIT导致的端口占用问题Linux提供了两个套接字选项SO_REUSEADDR通用解决方案允许创建端口号相同但IP地址不同的多个socket描述符是服务器开发的标准最佳实践。// 在socket创建后、bind前设置intopt1;setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt));SO_REUSEPORTLinux 3.9新增允许多个进程绑定到完全相同的IP地址和端口内核会自动在多个进程之间进行负载均衡。intopt1;setsockopt(listenfd,SOL_SOCKET,SO_REUSEPORT,opt,sizeof(opt));最佳实践所有TCP服务器都应该在创建socket后立即设置SO_REUSEADDR选项。1.7 连接管理机制知识图谱2 ~ 内核层面TCP连接的本质从内核角度看TCP连接并不是一个抽象的概念而是一组嵌套的数据结构。内核通过面向对象的多态思想统一管理TCP、UDP、UNIX域套接字等不同类型的套接字。2.1 连接的内核数据结构嵌套关系一个TCP连接在内核中通过以下层级的结构体表示task_struct进程控制块 ↓ files_struct文件描述符表 ↓ file打开的文件抽象 ↓ private_data → socketBSD套接字层 ↓ sk → sock通用套接字层 ↓ tcp_sockTCP连接核心结构体 ↓ inet_connection_sock面向连接套接字 ↓ inet_sockIPv4套接字 ↓ sock通用套接字这种嵌套结构的特点是每个子类结构体的第一个成员都是父类结构体因此可以通过强制类型转换实现父类指针指向子类对象这是C语言实现多态的核心技巧。2.2 内核多态机制的实现内核中所有类型的套接字TCP、UDP、UNIX等都继承自struct sock因此内核可以用统一的struct sock*指针管理不同类型的套接字。当需要调用协议特有的方法时通过方法表指针找到对应的实现。例如TCP套接字struct sock*实际指向struct tcp_sockUDP套接字struct sock*实际指向struct udp_sockUNIX域套接字struct sock*实际指向struct unix_sock2.3 核心结构体详解struct file文件抽象层// include/linux/fs.hstructfile{structpathf_path;conststructfile_operations*f_op;// 文件操作方法表atomic_long_tf_count;// 引用计数unsignedintf_flags;fmode_tf_mode;loff_tf_pos;// 文件偏移量void*private_data;// 指向socket结构体// ... 其他字段};所有套接字都被抽象为文件应用层通过文件描述符操作套接字本质上就是操作struct file结构体。struct socketBSD套接字层// include/linux/net.hstructsocket{socket_state state;shorttype;unsignedlongflags;structfile*file;// 指向对应的file结构体structsock*sk;// 指向通用套接字层conststructproto_ops*ops;// BSD层方法表// ... 其他字段};实现了BSD套接字接口的通用逻辑屏蔽了不同协议的差异。struct sock通用套接字层// include/net/sock.hstructsock{structsock_common_sk_common;structproto*sk_prot;// 协议层方法表structproto_ops*sk_ops;// BSD层方法表// 数据队列structsk_buff_headsk_receive_queue;// 接收缓冲区structsk_buff_headsk_write_queue;// 发送缓冲区structsk_buff_headsk_error_queue;// 错误队列// 回调函数void(*sk_state_change)(structsock*sk);void(*sk_data_ready)(structsock*sk,intbytes);void(*sk_write_space)(structsock*sk);// ... 其他字段};所有套接字的公共基类包含了套接字的通用属性和方法。struct tcp_sockTCP连接核心// include/net/tcp.hstructtcp_sock{structinet_connection_sockinet_conn;// 继承面向连接套接字u16 tcp_header_len;u32 snd_wnd;// 发送窗口大小u32 rcv_wnd;// 接收窗口大小u32 rcv_nxt;// 期望接收的下一个序号u32 snd_nxt;// 下一个要发送的序号u32 srtt;// 平滑往返时间u32 rttvar;// 往返时间偏差u32 packets_out;// 已发送未确认的数据包数量// ... 其他TCP特有字段};包含了TCP连接的所有状态信息是TCP协议栈的核心数据结构。2.4 方法表机制内核通过方法表实现不同协议的差异化行为每个协议都有自己的方法表实现。struct proto_opsBSD层方法表// include/linux/net.hstructproto_ops{intfamily;int(*release)(structsocket*sock);int(*bind)(structsocket*sock,structsockaddr*myaddr,intsockaddr_len);int(*connect)(structsocket*sock,structsockaddr*vaddr,intsockaddr_len,intflags);int(*accept)(structsocket*sock,structsocket*newsock,intflags,bool kern);int(*sendmsg)(structsocket*sock,structmsghdr*m,size_ttotal_len);int(*recvmsg)(structsocket*sock,structmsghdr*m,size_ttotal_len,intflags);int(*listen)(structsocket*sock,intlen);int(*shutdown)(structsocket*sock,intflags);// ... 其他方法};// TCP的BSD层方法表实现net/ipv4/tcp_ipv4.cconststructproto_opsinet_stream_ops{.familyPF_INET,.bindinet_bind,.connectinet_stream_connect,.acceptinet_accept,.sendmsginet_sendmsg,.recvmsginet_recvmsg,.listeninet_listen,.shutdowninet_shutdown,// ... 其他方法};拥塞控制算法表TCP的拥塞控制算法也是通过方法表实现的可以动态切换staticstructtcp_congestion_opsbbr_ops{.namebbr,.initbbr_init,.cong_avoidbbr_cong_avoid,.ssthreshbbr_ssthresh,.undo_cwndbbr_undo_cwnd,// ... 其他方法};2.5 “如何理解连接”知识图谱3 ~ TCP可靠性保障机制TCP的可靠性是通过确认应答、超时重传、快速重传、校验和、序号等多种机制共同实现的。3.1 确认应答(ACK)机制TCP将每个字节的数据都进行了编号称为序列号。每一个ACK报文都带有对应的确认序列号表示我已经收到了确认号之前的所有数据下一次请从确认号开始发送。累计确认特性如果发送方发送了11000、10012000、20013000三个报文段接收方只回复了确认号为3001的ACK就表示13000的所有数据都已经正确接收。即使前两个ACK丢失了也不需要重传。3.2 超时重传机制如果发送方在一个特定时间间隔内没有收到对应的ACK就会认为数据丢失进行重传。超时时间动态计算TCP会动态计算超时重传时间(RTO)以适应不同的网络环境Linux系统以500ms为基本单位每次重传后超时时间以指数形式递增500ms → 1s → 2s → 4s → …累计到一定重传次数默认15次后TCP认为网络或对端异常强制关闭连接3.3 快速重传机制超时重传需要等待超时时间效率较低。TCP引入了快速重传机制当发送方连续收到3个相同的确认号时就会立即重传对应的数据而不需要等待超时时间。两种丢包场景处理ACK丢失部分ACK丢失不影响后续的ACK会通过累计确认覆盖前面的ACK数据包丢失接收方会一直回复相同的确认号发送方收到3次重复ACK后立即重传丢失的数据包快速重传机制大幅提高了丢包恢复的效率是TCP高性能传输的重要保障。4 ~ TCP流量控制机制流量控制的目的是让发送方的发送速率匹配接收方的接收能力防止发送方发送过快导致接收方缓冲区溢出。4.1 基本原理TCP首部有一个16位的窗口字段接收方在回复ACK时会将自己当前的接收缓冲区剩余空间填入该字段发送方根据这个值调整自己的发送速率。窗口大小 接收缓冲区剩余空间发送方已发送未确认的数据量 ≤ min(对方窗口大小, 拥塞窗口大小)4.2 零窗口处理当接收方的缓冲区满时会将窗口字段设置为0此时发送方会停止发送数据。为了防止窗口更新通知丢失导致双方死锁TCP采用了双机制窗口探测发送方定期发送一个字节的窗口探测包接收方必须回复ACK并携带当前窗口大小窗口更新通知当接收方缓冲区有空间时主动向发送方发送窗口更新通知两种机制同时工作谁先到达就按谁的结果处理。4.3 窗口扩大因子16位窗口字段最大只能表示65535字节这在高速网络中远远不够。TCP通过窗口扩大因子选项解决这个问题实际窗口大小 窗口字段的值 MM为窗口扩大因子窗口扩大因子在三次握手的SYN包中协商最大为14此时最大窗口可达65535×2^141GB代码设置#includesys/socket.h#includenetinet/tcp.hintsocksocket(AF_INET,SOCK_STREAM,0);intrcvbuf1024*1024;// 设置接收缓冲区为1MBif(setsockopt(sock,SOL_SOCKET,SO_RCVBUF,rcvbuf,sizeof(rcvbuf))0){perror(setsockopt SO_RCVBUF failed);}// 必须在connect()或listen()之前设置connect(sock,(structsockaddr*)addr,sizeof(addr));内核会根据设置的接收缓冲区大小自动选择合适的窗口扩大因子。4.4 流量控制优化机制延迟应答如果接收数据的主机立刻返回ACK应答此时返回的窗口可能比较小。如果接收端稍微等一会再应答最多200ms应用层可能已经消费了部分数据此时可以返回更大的窗口提高传输效率。数量限制每隔2个包就应答一次时间限制超过最大延迟时间200ms就应答一次捎带应答在延迟应答的基础上如果接收端恰好有数据要发送给发送端就可以将ACK和数据合并在一个报文中发送减少网络报文数量。4.5 流量控制知识图谱完整的思维导图5 ~ 初识TCP滑动窗口机制滑动窗口是TCP实现批量数据传输和流量控制的核心机制它在保证可靠性的同时大幅提升了网络传输效率。5.1 引入背景传统的串行确认应答机制发送一个段等待一个ACK再发送下一个段性能非常低下尤其是在网络往返时间较长的情况下。滑动窗口允许发送方一次性发送多个数据段将多个段的等待时间重叠在一起显著提升吞吐率。5.2 滑动窗口的本质与实现滑动窗口本质上是发送缓冲区的一段连续区间区间内的数据可以无需等待ACK直接发送。内核通过两个指针管理滑动窗口win_start窗口左边界指向已发送未确认数据的第一个字节win_end窗口右边界指向可以发送的最后一个字节的下一个位置滑动窗口的大小 win_end - win_start这个值由接收方通告的窗口大小和拥塞窗口大小中的较小值决定。5.3 标准工作流程发送方将滑动窗口内的所有数据一次性发送出去接收方收到数据后回复ACK并携带当前窗口大小发送方收到ACK后将win_start移动到ACK确认的序号位置同时根据新的窗口大小调整win_end重复上述过程直到所有数据发送完毕核心特点只有确认应答过的数据才能从发送缓冲区中删除窗口越大网络吞吐率越高滑动窗口只能向右滑动不能向左滑动5.4 异常场景处理ACK丢失TCP采用累计确认机制部分ACK丢失不会影响传输。只要收到后续的ACK就可以确认前面所有的数据都已经正确接收。数据包丢失当某个数据包丢失时接收方会一直回复相同的确认号。发送方连续收到3次相同的确认号后会立即重传丢失的数据包而不需要等待超时时间。5.5 滑动窗口初识知识图谱6 ~全文总结本文系统梳理了TCP协议最核心的连接管理、可靠性保障、流量控制与滑动窗口机制核心知识点总结如下1. 连接管理TCP首部包含6个关键标志位是所有控制功能的载体三次握手的本质是确认双方的发送和接收能力协商初始序列号四次挥手的本质是双向连接的独立关闭三次挥手是ACK与FIN合并的特殊情况CLOSE_WAIT由被动关闭方未调用close()导致会造成文件描述符泄漏TIME_WAIT由主动关闭方产生持续2MSL目的是保证迟到报文消散和最后一个ACK可靠到达通过SO_REUSEADDR和SO_REUSEPORT解决TIME_WAIT导致的bind失败问题2. 内核连接本质TCP连接在内核中是一组嵌套的数据结构通过task_struct→files_struct→file→socket→sock→tcp_sock层级关联内核通过C语言的结构体嵌套和方法表实现多态统一管理不同类型的套接字struct tcp_sock是TCP连接的核心包含了所有连接状态和参数3. 可靠性保障确认应答采用字节编号和累计确认机制超时重传采用指数退避算法动态调整超时时间快速重传通过三次重复ACK触发大幅提高丢包恢复效率4. 流量控制接收方通过TCP首部的窗口字段通告接收能力零窗口时通过窗口探测和窗口更新通知双机制避免死锁窗口扩大因子将最大窗口从65535字节扩展到1GB延迟应答和捎带应答进一步优化了传输效率5. 滑动窗口滑动窗口是发送缓冲区的一段可发送区间通过指针移动实现滑动允许批量发送数据重叠等待时间大幅提升传输效率支持累计确认和快速重传在异常情况下仍能保证可靠性是TCP可靠性与性能平衡的核心机制这些知识点相互关联共同构成了TCP协议的核心框架。深入理解这些内容不仅能帮助你解决网络编程中的常见问题还能为后续学习HTTP、HTTPS等应用层协议打下坚实的基础。结尾uu们本文的内容到这里就全部结束了艾莉丝在这里再次感谢您的阅读艾莉丝努力练剑C/C Linux 底层探索者 | 一个正在努力练剑的技术博主【关注】跟随我一起深耕技术领域见证每一次成长。❤️【点赞】让优质内容被更多人看见让知识传递更有力量。⭐【收藏】把核心知识点存好在需要时随时查、随时用。【评论】分享你的经验或疑问评论区一起交流避坑不要忘记给博主“一键四连”哦“今日练剑达成”“技术之路难免有困惑但同行的人会让前进更有方向。”结语希望对学习Linux相关内容的uu有所帮助不要忘记给博主“一键四连”哦往期回顾【Linux网络】Linux 网络编程传输层协议TCP三博主在这里放了一只小狗大家看完了摸摸小狗放松一下吧૮₍ ˶ ˊ ᴥ ˋ˶₎ა