TCP三次握手图解与Python Socket代码验证

之前排查接口连接慢的问题时,日志里只看到一句 connect timeout。如果只盯着 HTTP 层,很容易误判成接口服务慢;但很多时候,问题发生在更前面的 TCP 建连阶段。

这篇笔记用一个本地 Python Socket 示例,把 TCP 三次握手、listen 队列、acceptconnect 和数据收发串起来。重点不是把 TCP 协议讲成教科书,而是给出一套可以在本机复现的观察方法。

一、三次握手到底在确认什么

TCP 是面向连接的协议。客户端调用 connect() 之后,并不是马上开始发送业务数据,而是先完成三次握手:

text 复制代码
Client                                      Server
  |                                           |
  |  SYN                                      |
  |------------------------------------------>|
  |                                           |
  |                         SYN + ACK         |
  |<------------------------------------------|
  |                                           |
  |  ACK                                      |
  |------------------------------------------>|
  |                                           |
  |              connection established       |

可以简单理解为:

  1. 客户端告诉服务端:我想建立连接;
  2. 服务端回复:我收到了,也准备好了;
  3. 客户端确认:我也收到你的回复了。

完成之后,双方才进入可传输数据的状态。

二、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,服务端打印 acceptedreceived

五、用 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 响应

所以看到接口慢,需要先拆开:

  1. DNS 是否慢;
  2. TCP 建连是否慢;
  3. TLS 握手是否慢;
  4. HTTP 首字节是否慢;
  5. 响应体下载是否慢。

如果只记录一个总耗时,排查时会非常被动。

八、一个简单的 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 timeoutconnection refusedreset by peer 都有关。

在排查接口慢、代理链路不稳定或 API 调用失败时,把 DNS、TCP、TLS、HTTP 分层记录,比只看一个总耗时可靠得多。Python Socket 虽然偏底层,但用来写最小复现实验非常合适:代码短、现象明确,也更容易把问题从"感觉很慢"变成"哪一层慢"。

参考资料:

  • Python socket 标准库文档
  • RFC 9293 Transmission Control Protocol
相关推荐
森G2 小时前
64、完善聊天室程序(TLV拓展)---------网络编程
网络·c++·tcp/ip
青瓦梦滋7 小时前
Linux:TCP协议的socket套接字
网络·网络协议·tcp/ip
KaMeidebaby7 小时前
卡梅德生物技术快报 | Fab 合成文库构建与抗体筛选实验流程及数据解析
人工智能·python·tcp/ip·算法·机器学习
IP老炮不瞎唠8 小时前
采集运行不稳定?分清住宅IP与数据中心代理的差异
网络·网络协议·tcp/ip
森G8 小时前
65、UDP协议(拓展选学)---------网络编程
网络·c++·qt·网络协议·tcp/ip·udp
liulilittle8 小时前
回归物理本质:对拥塞控制实验室依赖与公平性误置的反思
网络·tcp/ip·计算机网络·算法·tcp·通信·拥塞控制
cyforkk8 小时前
破除网络协议迷雾:TCP、TLS 与 HTTP 的“连环套”逻辑
网络协议·tcp/ip·http
@insist1238 小时前
系统架构设计师-TCP/IP 协议族核心协议详解
网络协议·tcp/ip·系统架构·软考·系统架构设计师·软件水平考试