资讯动态

从 POSIX API 到网络协议栈:一条 TCP 连接的一生

发布时间:2026/8/10 9:05:59 来源:尧图企业网站定制
这篇文章想解决的问题——写代码的人只看到socket/send/recv但一个 connect 背后是内核协议栈在干活。用一条连接从创建到关闭的视角把 API 与内核实现对应起来。一、总览网络编程调用的 POSIX API——socket、bind、connect、listen、accept、send、recv、close——只是用户态与内核之间的接口真正干活的是内核协议栈。其中 select/poll 是标准 POSIX API而 epoll 不是epoll 是 Linux 内核提供的接口。每个 socket 背后是一个内核态的 socket 对象内含 TCP 控制块 TCB用户拿到的只是一个文件描述符 fd通过 fd 找到 socket 对象再操作对应的 TCB。socket 创建完成后系统只分配了 fd 和一个空的 TCB——此时还没有分配收发缓冲区。一条连接的一生客户端socket → connect发起三次握手→ send/recv → close。服务器socket → bind绑定 IP 与端口→ listen把 TCB 状态置为 LISTEN分配半连接/SYN 队列与全连接/accept 队列此后三次握手由内核协议栈完成、用户不可控→ accept取出已建立的连接分配新的 fd 并与对应 TCB 建立映射→ recv/send → close。贯穿全文的六个关键认知bind 可绑可不绑不绑时由内核自动分配端口connect 对 UDP 同样适用只是告诉内核只与这个对端通信不等于建立连接TCP 的 connect 才发起三次握手。TCP 是字节流协议一个 TCB 对应一个 stream是连续的字节流、没有消息边界边界需由应用层自行划分。send/recv 只是用户态与内核缓冲区之间的拷贝与真实网络传输是异步的TCP 协议描述的是两个协议栈之间的数据通信。close 是文件系统函数而非网络函数它减少 fd 的引用计数当 socket 无引用时才进入 TCP 关闭流程——先回收 fd再发送 FIN。断开连接不分客户端/服务器只分主动方/被动方。连接的生命周期从第一次握手就开始了协议栈收到 SYN 即创建 TCB 副本放入半连接队列第三次握手的数据包靠五元组从半连接队列查找匹配节点ACK 后节点移入全连接队列。攻击与防护SYN 泛洪通过大量 SYN 耗尽半连接资源用 listen 的 backlog 参数防护它控制 SYN 队列长度、SYNaccept 队列总长度即未分配 fd 的 TCB 数量与 accept 队列长度CC 攻击是应用层的 HTTP 泛洪SYN、UDP、ICMP、HTTP 泛洪同属 DDoS。以上六点会在后文逐一展开。二、客户端视角一条连接如何发起客户端创建连接的五步逐一对应内核里的动作socket创建 fdsocket()在内核态创建一个 socket 对象内含 TCB返回一个整数 fd。用户态后续所有操作都以 fd 为凭据内核通过 fd 找到对应的 socket 对象。此时 TCB 是空的——没有绑定地址、没有分配收发缓冲区。bind可绑可不绑bind()的作用是把 IP 与端口绑定到这个 socket 上。但它是可选的不调用 bind 时内核会在 connect 时自动分配一个临时端口。客户端通常不绑让内核挑端口服务器必须绑客户端需要知道连哪里。connect发起三次握手TCP 的connect()发起三次握手这一步是同步阻塞的握手完成后才返回。特别要澄清一点UDP 也可以调 connect它只是告诉内核我只跟这个对端通信后续 sendto/recvfrom 可简化为 send/recv但不等于建立连接——UDP 本来就没有连接。send / recv用户态与内核之间的拷贝send()把数据从应用程序复制到内核发送缓冲区recv()把数据从内核接收缓冲区复制到应用程序。它们只保证进出内核不保证到达对端与网络上真正的传输是异步的。close关闭close()减少 fd 引用计数引用归零时进入 TCP 关闭流程先回收 fd再发 FIN。细节在第七节展开。一句话串起来客户端流程是 socket → connect → send/recv → close其中只有 connect 涉及三次握手其余都是对 fd 和内核缓冲区的操作。三、服务器视角socket 的完整生命周期socket创建内核对象socket()在内核态创建 socket 对象内含 TCB返回 fd。注意创建完成后系统只分配了 fd 和一个空的 TCP 控制块没有分配接收/发送缓冲区——缓冲区要到连接建立后才有实际意义。bind绑定地址通过 fd 找到 socket 对象把 IP 与端口绑定上去。服务器必须 bind否则客户端不知道该连哪里。listen置状态分配队列listen()把 TCB 中的状态置为 TCL_STATUS_LISTEN监听态同时为 TCB 分配两条队列半连接队列SYN 队列和全连接队列accept 队列。从这一刻起三次握手可以发生——但它是内核协议栈实现的用户态不可控。握手的具体过程收到 SYN 后内核创建一个 TCB 副本放入半连接队列收到第三次 ACK 后半连接队列中的 TCB 节点移动到全连接队列。第三次握手的数据包如何从半连接队列中找到匹配的节点靠五元组源 IP、源端口、目的 IP、目的端口、协议匹配。accept取出连接分配新 fd三次握手完成后accept()从全连接队列中取出一个已建立的连接为对应的客户端分配一个新的 fd并把该 fd 与对应的 TCB 建立映射。此后这个连接就用这个新 fd 通信与监听 fd 无关。recv / send与内核缓冲区的拷贝recv 把数据从内核协议栈复制到应用程序send 把数据从应用程序复制到内核协议栈。close见第七节。一句话总结服务器流程socket → bind → listen置状态 分配两队列→ 内核自动完成三次握手 → accept取连接、分配新 fd→ recv/send → close。四、深入内核三次握手与两条队列三次握手的本质是内核协议栈的事accept 只是取结果这也是它听起来简单但面试总考的原因。时序客户端 SYN 到达 → 内核创建 TCB 副本放入半连接队列。此时连接已经开始存在。服务器回复 SYNACK。客户端回复 ACK → 内核从半连接队列中取出对应节点靠五元组匹配移入全连接队列。之后accept()从全连接队列取走连接完成。关键点半连接队列存的是握手没完成的连接全连接队列存的是握手已完成、等待 accept的连接。从半连接到全连接的移动发生在第三次握手 ACK 到达时这一步也是内核完成的用户态不可控。五元组是查找的依据第三次 ACK 到达时内核拿它的五元组去半连接队列里找匹配的 TCB 节点。所谓listen 之后才可以握手本质是只有置为 LISTEN 的 TCB 才分配了两条队列才有地方暂存这些连接。五、字节流模型TCP 没有消息边界一个 TCB 对应一个 stream也就是一条 TCP 字节流。TCP 提供的是连续的字节流它只保证字节的顺序和到达不负责消息边界。发送方调两次 send 发了helloworld接收方可能一次 recv 就收到helloworld也可能分成三次收到。边界怎么划是应用层自己的事——常见方案定长消息、分隔符如换行、或长度前缀包头带长度字段。这一点与 UDP 截然不同UDP 是数据报每次 sendto 对应一次 recvfrom天然有边界TCP 没有。六、send/recv 与网络传输的异步性send()把数据从应用程序复制到内核发送缓冲区就返回了不等于数据已经发到网络recv()把数据从内核接收缓冲区复制给应用程序也不代表缓冲区里一直有货。两者都只是用户态 ↔ 内核态的拷贝。与网络上的实际传输是异步的内核协议栈按自己的节奏把发送缓冲区里的数据切成报文段、发出、等待确认、超时重传、接收对端数据存入接收缓冲区。用户程序感知不到这个过程。理解这一点的价值TCP 协议描述的其实是两个协议栈之间的数据通信双方的 TCP 状态机、序列号、确认、窗口而不是两个应用程序之间。应用程序只跟自己的内核打交道。七、close 是文件系统函数close()不是网络函数是文件系统函数——因为 fd 本来就是文件系统的概念。它的原理减少 fd 的引用计数。一个 fd 可能被 fork 后父子进程共享引用计数大于 1 时close 只是减一不真正关闭。当引用计数归零、socket 无引用时才进入 TCP 关闭流程先把 fd 回收从文件系统层面注销再发送 FIN 给对方。关于断开连接还有一个重要认知断开连接不分客户端/服务器只分主动方/被动方。谁先调用 close谁就是主动方负责发 FIN 进入四次挥手流程另一方是被动方收到 FIN 后回复 ACK。所以客户端主动断开只是常见情形服务器同样可以主动 close。八、连接的生命周期从第一次握手开始TCP 连接的生命周期从第一次握手就开始了协议栈收到或发出SYN 的那一刻就已经分配了对应的 TCP 连接TCB 副本进半连接队列。这意味着在accept()返回之前连接其实已经存在了——只不过它在全连接队列里排队等着被取走。三次握手还没完成时半连接队列里已经有连接对象存在。所以连接何时建立的答案不是accept 返回时而是第一个 SYN 到达时。这个认知也解释了 SYN 泛洪为什么可怕——攻击者只需要发 SYN不完成握手就能让内核不断创建 TCB 副本占满半连接队列。九、网络攻击与防护SYN 泛洪原理攻击者发送大量 SYN 包不完成三次握手让服务器在半连接队列里堆积大量半截连接耗尽半连接资源导致正常用户无法完成握手。防护核心listen()的 backlog 参数。它的含义在不同内核里有细微差别但核心是控制队列容量涉及三个层面SYN 队列半连接队列长度SYN accept 队列的总长度——即未分配 fd 的 TCB 的数量。注意全连接队列里的连接虽然握手完成但还没被 accept所以也没有 fd同样受 backlog 约束accept 队列全连接队列长度。backlog 调大能容纳更多连接但也要权衡队列越长被攻击时资源消耗越大。CC 攻击HTTP 泛洪这是应用层的攻击攻击者模拟大量真实用户不断发送 HTTP 请求消耗 Web 服务器资源CPU、内存、连接数、数据库查询让服务器无法服务正常用户。它不像 SYN 泛洪那样攻击协议栈而是攻击应用逻辑所以更隐蔽。共同归属DDoSSYN 泛洪、UDP 泛洪、ICMP 泛洪、HTTP 泛洪本质都是分布式拒绝服务DDoS的不同实现通过耗尽目标资源让服务不可用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价