4. 连接建立:connect() 与 accept()、三次握手过程、连接队列管理
连接建立,说白了就是客户端和服务端之间「握手」的过程。很多人觉得这步就是调个函数,没什么好讲的。但我在生产环境里踩过好几次坑,都是因为对握手细节理解不够深。今天咱们就把这块彻底掰开揉碎。
4.1 三次握手:谁在什么时候做了什么
TCP 的三次握手,你肯定听过。但我要问的是:每一步分别对应哪个系统调用? 这才是关键。
| 步骤 | 方向 | 报文 | 对应的系统调用 | 状态变化 |
|---|---|---|---|---|
| 1 | 客户端 → 服务端 | SYN | connect() 发起 |
CLOSED → SYN_SENT |
| 2 | 服务端 → 客户端 | SYN + ACK | 内核自动回复(在 listen() 之后) |
LISTEN → SYN_RCVD |
| 3 | 客户端 → 服务端 | ACK | connect() 返回 |
SYN_SENT → ESTABLISHED |
注意看第二步——服务端收到 SYN 后,内核会自动回复 SYN+ACK,根本不需要你写代码。这时候服务端连接还没完全建立,处于半连接状态。直到第三步 ACK 到达,服务端内核才把连接从半连接队列移到全连接队列,accept() 才能把它取出来。
核心结论:connect() 在第三次握手完成后返回;accept() 在第三次握手完成之后才能取出连接。两者不是同时发生的。
4.2 connect():客户端的敲门砖
我个人习惯把 connect() 看作「发起一次交易请求」。它的阻塞行为经常让人困惑。
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(5.0) # 我建议永远不要省略这行
try:
client.connect(('192.168.1.100', 8080))
print("连接成功")
except socket.timeout:
print("连接超时——服务端可能没启动,或者防火墙拦截了")
except ConnectionRefusedError:
print("连接被拒绝——端口没在监听")
except OSError as e:
print(f"网络不可达或其他错误: {e}")
这里有个坑:默认的 connect() 是阻塞的。如果服务端没响应,你的线程会一直卡着。我曾经在生产环境里遇到过,某个上游服务挂了,结果所有工作线程都卡在 connect() 上,整个服务雪崩。从那以后,我写任何客户端代码都会先设 settimeout。
避坑指南:我曾经在压测时发现,connect() 返回成功不代表连接真的可用。如果服务端在第三次握手后立即发送了 RST(比如 backlog 满了),客户端 connect() 已经返回了,但第一次读写就会报错。所以,连接建立后最好做一次心跳探测。
4.3 accept():服务端的取件窗口
accept() 做的事情很简单:从全连接队列里取出一个已经完成三次握手的连接。但它有几个细节值得注意。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 8080))
server.listen(5) # backlog = 5
while True:
# accept 会阻塞,直到有连接可取
conn, addr = server.accept()
print(f"收到来自 {addr} 的连接")
# 注意:这里 conn 已经完成了三次握手
你想想看,accept() 返回的 conn 是一个新的 socket 对象,它和监听 socket 是两回事。监听 socket 继续监听,新 socket 负责通信。这个设计很巧妙——一个服务员负责招呼客人,多个服务员分别服务不同的客人。
4.4 连接队列管理:backlog 的秘密
这是面试常考、生产常坑的点。内核维护了两个队列:
- 半连接队列(SYN Queue):存放收到 SYN 但还没完成握手的连接。大小由内核参数
tcp_max_syn_backlog控制。 - 全连接队列(Accept Queue):存放已完成三次握手、等待
accept()取走的连接。大小由listen()的 backlog 参数决定。
很多人以为 backlog 就是最大连接数,其实不对。它只是全连接队列的长度上限。而且不同系统对这个值的解释还不一样:
| 系统 | backlog 实际含义 | 典型默认值 |
|---|---|---|
| Linux | 全连接队列长度(但会做向上取整和上限限制) | 128(可调) |
| macOS | 全连接队列长度 | 128 |
| Windows | 全连接队列长度(上限由系统决定) | 200(约) |
经验之谈:我在 Linux 上做高并发服务时,习惯把 backlog 设成 1024 甚至更大,同时配合 somaxconn 内核参数一起调。但要注意——backlog 不是越大越好。如果 accept() 处理不过来,队列里的连接会一直占用内存,而且客户端会超时重试,反而加重负载。
4.5 队列满了会发生什么?
这个问题很实际。当全连接队列满了,内核的行为取决于 tcp_abort_on_overflow 参数:
- 值为 0(默认):内核直接丢弃第三次握手的 ACK。客户端以为连接已经建立了,但服务端根本不认。客户端发数据时会收到 RST,或者重试 SYN。
- 值为 1:内核直接发 RST 给客户端。客户端
connect()会收到ECONNRESET错误。
我曾经在压测时遇到过全连接队列溢出的问题。现象很诡异:客户端 connect() 成功,但第一次 send() 就报错。查了半天才发现是 accept() 处理太慢,队列满了。解决办法有两个:
- 加快
accept()速度:用多线程或异步 IO 及时取走连接。 - 增大 backlog:给队列更多缓冲空间。
4.6 完整流程:一张图看懂
下面这张图把整个连接建立过程串起来了。我建议你仔细看看每个箭头对应的系统调用和队列变化。
4.7 实战建议:如何安全地管理连接建立
最后,我总结几条实战经验,希望能帮你少走弯路:
- 永远给
connect()设超时。默认阻塞太危险,一个挂掉的服务能拖垮整个客户端。 - 监控全连接队列溢出。Linux 上可以用
ss -lnt查看Recv-Q和Send-Q。如果Recv-Q长期接近 backlog 值,说明accept()处理不过来。 - 不要在生产环境用默认 backlog。128 对于高并发场景太小了,至少设到 1024,同时调大
net.core.somaxconn。 - 连接建立后做一次心跳。防止出现「假连接」——三次握手成功了,但服务端其实已经挂了。
小技巧:我写服务端代码时,习惯在 accept() 之后立即设置 TCP_NODELAY 和 SO_KEEPALIVE。前者减少小包延迟,后者帮我及时发现死连接。两行代码,省去很多麻烦。