之前排查接口连接慢的问题时,日志里只看到一句 connect timeout。如果只盯着 HTTP 层,很容易误判成接口服务慢;但很多时候,问题发生在更前面的 TCP 建连阶段。
这篇笔记用一个本地 Python Socket 示例,把 TCP 三次握手、listen 队列、accept、connect 和数据收发串起来。重点不是把 TCP 协议讲成教科书,而是给出一套可以在本机复现的观察方法。
一、三次握手到底在确认什么
TCP 是面向连接的协议。客户端调用 connect() 之后,并不是马上开始发送业务数据,而是先完成三次握手:
text
Client Server
| |
| SYN |
|------------------------------------------>|
| |
| SYN + ACK |
|<------------------------------------------|
| |
| ACK |
|------------------------------------------>|
| |
| connection established |
可以简单理解为:
- 客户端告诉服务端:我想建立连接;
- 服务端回复:我收到了,也准备好了;
- 客户端确认:我也收到你的回复了。
完成之后,双方才进入可传输数据的状态。
二、Socket 里的几个关键调用
Python 的 socket 模块是比较贴近系统 Socket API 的封装。一个最小 TCP 服务端通常会用到:
| 调用 | 作用 |
|---|---|
socket.socket() |
创建 socket |
bind() |
绑定 IP 和端口 |
listen() |
进入监听状态 |
accept() |
接收客户端连接 |
recv() |
读取数据 |
sendall() |
发送数据 |
close() |
关闭连接 |
客户端通常会用到:
| 调用 | 作用 |
|---|---|
create_connection() |
创建并连接 TCP socket |
sendall() |
发送完整字节数据 |
recv() |
接收响应 |
注意:connect() 或 create_connection() 成功返回,通常意味着 TCP 建连已经完成;但它不代表 HTTP 请求一定成功,也不代表服务端业务逻辑可用。
三、服务端代码:监听本地端口
先写一个最小服务端:
python
import socket
import threading
from datetime import datetime
HOST = "127.0.0.1"
PORT = 9000
def log(message: str) -> None:
now = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[{now}] {message}")
def handle_client(conn: socket.socket, addr: tuple[str, int]) -> None:
with conn:
log(f"accepted from {addr}")
data = conn.recv(1024)
log(f"received: {data!r}")
conn.sendall(b"pong\n")
log("response sent")
def main() -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen(5)
log(f"listening on {HOST}:{PORT}")
while True:
conn, addr = server.accept()
thread = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
thread.start()
if __name__ == "__main__":
main()
运行:
bash
python tcp_server.py
这里的 listen(5) 表示内核维护的连接等待队列大小提示。它不是业务并发上限,也不是线程数量。真实行为还会受操作系统参数影响。
四、客户端代码:主动建连并发送数据
再写客户端:
python
import socket
import time
from datetime import datetime
HOST = "127.0.0.1"
PORT = 9000
def log(message: str) -> None:
now = datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[{now}] {message}")
def main() -> None:
start = time.perf_counter()
with socket.create_connection((HOST, PORT), timeout=3) as client:
connect_ms = (time.perf_counter() - start) * 1000
log(f"connected in {connect_ms:.2f} ms")
client.sendall(b"ping\n")
log("request sent")
response = client.recv(1024)
log(f"response: {response!r}")
if __name__ == "__main__":
main()
运行:
bash
python tcp_client.py
你会看到客户端先打印 connected,然后发送 ping,服务端打印 accepted 和 received。
五、用 netstat 观察连接状态
服务端启动后,可以另开终端观察端口:
bash
netstat -an | grep 9000
macOS / Linux 上也可以用:
bash
lsof -iTCP:9000 -sTCP:LISTEN
Windows:
powershell
netstat -ano | findstr 9000
如果客户端连接后很快关闭,你可能会看到 TIME_WAIT。这不是异常,而是 TCP 连接关闭流程中的常见状态。
六、模拟 connect timeout
如果连接一个没有服务监听的端口,通常会很快报 ConnectionRefusedError:
python
import socket
try:
socket.create_connection(("127.0.0.1", 9999), timeout=3)
except OSError as exc:
print(type(exc).__name__, exc)
而连接一个无法到达的地址,可能会等待到 timeout:
python
import socket
try:
socket.create_connection(("203.0.113.10", 9000), timeout=3)
except OSError as exc:
print(type(exc).__name__, exc)
203.0.113.0/24 是文档示例地址段,不应该用于真实服务。它适合用来演示不可达场景。
这两个错误定位方向不同:
| 现象 | 可能原因 |
|---|---|
| Connection refused | 目标主机可达,但端口无人监听或被拒绝 |
| timed out | 路由、网络链路、防火墙或目标不可达 |
| reset by peer | 对端主动重置连接 |
七、三次握手和 HTTP 请求的关系
一次 HTTPS API 调用里,TCP 只是其中一层:
text
DNS 解析
-> TCP 三次握手
-> TLS 握手
-> HTTP 请求
-> 服务端业务处理
-> HTTP 响应
所以看到接口慢,需要先拆开:
- DNS 是否慢;
- TCP 建连是否慢;
- TLS 握手是否慢;
- HTTP 首字节是否慢;
- 响应体下载是否慢。
如果只记录一个总耗时,排查时会非常被动。
八、一个简单的 TCP 探测函数
实际项目里,我会把 TCP 建连耗时单独记录:
python
import socket
import time
def tcp_probe(host: str, port: int, timeout: float = 3.0) -> dict:
start = time.perf_counter()
try:
with socket.create_connection((host, port), timeout=timeout):
elapsed_ms = (time.perf_counter() - start) * 1000
return {
"host": host,
"port": port,
"ok": True,
"connect_ms": round(elapsed_ms, 2),
}
except OSError as exc:
elapsed_ms = (time.perf_counter() - start) * 1000
return {
"host": host,
"port": port,
"ok": False,
"elapsed_ms": round(elapsed_ms, 2),
"error_type": type(exc).__name__,
"error": str(exc),
}
if __name__ == "__main__":
for target in [("127.0.0.1", 9000), ("example.com", 80)]:
print(tcp_probe(*target))
把这个函数加到 API 巡检脚本里,就能区分"建连慢"和"业务响应慢"。
九、常见误区
1. connect 成功不代表接口可用
TCP 连接建立成功,只说明目标主机和端口可达。后面的 TLS、HTTP、鉴权、限流都可能失败。
2. Connection refused 不一定是网络坏了
它往往说明目标端口没有服务监听,或者服务端明确拒绝连接。
3. TIME_WAIT 不是错误
短连接较多时看到 TIME_WAIT 很正常。是否需要优化,要看连接数量、端口占用和业务场景。
4. listen 参数不是业务并发开关
listen(backlog) 影响的是等待队列,不是服务端同时处理请求的能力。业务并发还要看线程、进程、协程或事件循环模型。
十、复盘
TCP 三次握手不是抽象概念,它和我们每天遇到的 connect timeout、connection refused、reset by peer 都有关。
在排查接口慢、代理链路不稳定或 API 调用失败时,把 DNS、TCP、TLS、HTTP 分层记录,比只看一个总耗时可靠得多。Python Socket 虽然偏底层,但用来写最小复现实验非常合适:代码短、现象明确,也更容易把问题从"感觉很慢"变成"哪一层慢"。
参考资料:
- Python socket 标准库文档
- RFC 9293 Transmission Control Protocol