欢迎光临
我们一直在努力

TCP 专章:三次握手之后,可靠管道里发生了什么?

专栏:《计算机网络基础》· 第三篇·传输层 承接:《UDP 专章:不握手的快件》 讲解维度: 概念 → 原理 → 应用 → 问题定位 + 可运行示例 读完你能: 讲清握手/挥手与常见状态;写出 TCP 服务端/客户端;理解粘包;用 ss/tcpdump 排连接问题

导读:从「明信片」到「水管」

UDP 是一封封明信片;TCP 更像两端之间的可靠水管:

你 write("你好世界") ──► [ TCP 管道 ] ──► 对端 read() 按序得到字节
│ │
│ 丢了会重传、乱了会排好
│ 对方太忙会让你慢点发

本篇先建立正确直觉,再动手写代码,最后用命令看「连接状态」。

一、概念:TCP 是什么?

1.1 一句话

TCP(Transmission Control Protocol):面向连接、可靠、基于字节流的传输层协议。

关键词

人话

面向连接

传数据前先握手,传完大致要挥手

可靠

尽量做到:不丢、不乱、不重复(在可能范围内)

字节流

不替你保留「消息边界」(和 UDP 数据报不同)

全双工

同一连接上双方都能同时发

1.2 和 UDP 对照(复习)

UDP

TCP

连接

无

有

可靠

否

是(重传+确认等)

保序

否

是

消息边界

有(数据报)

无(流)

速度/开销

更轻

更重

典型

DNS、直播

Web、SSH、数据库

1.3 在栈里的位置

HTTP / SSH / 数据库协议 …
│
▼
TCP ← 本专章
│
▼
IP


 

二、原理:握手、可靠、窗口、状态

2.1 三次握手:为什么是三步?

客户端 服务器
│──────── SYN ────────────►│ 「我想连,序号从 x 起」
│◄────── SYN+ACK ──────────│ 「好,我的从 y 起;确认收到 x」
│──────── ACK ────────────►│ 「确认收到 y;开始传数据」
│ ESTABLISHED │

直觉目的:

  • 确认双方 收发能力 正常;

  • 同步 初始序号,后面才能发现丢包/重复;

  • 防止「旧的迟到 SYN」莫名其妙建连(历史原因之一)。

  • 2.2 四次挥手:为什么分手也麻烦?

    TCP 全双工:一边说完了,另一边可能还有数据要发完。

    主动关闭方 被动关闭方
    │──────── FIN ────────────►│ 「我发完了」
    │◄─────── ACK ─────────────│ 「知道了」
    │ │ (对方可能继续发剩余数据)
    │◄─────── FIN ─────────────│ 「我也发完了」
    │──────── ACK ────────────►│ 「好」

    之后主动方通常进入 TIME-WAIT(等一会儿再彻底释放),避免旧包干扰新连接。

    2.3 可靠传输三件套(核心)

    ① 序号 seq + 确认 ack
    「你发的第 N 号字节我收到了」

    ② 超时重传 / 快速重传
    「等不到确认,再发一遍」

    ③ 滑动窗口
    「一次允许多少数据在路上还没确认」

    简化示意:

    已确认 已发送未确认 窗口内还可发 不能发
    ████████░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░│░░░░░░░░
    ▲ ▲
    左沿(已确认) 右沿(窗口限制)

    2.4 流量控制 vs 拥塞控制(再强调)

    流量控制

    拥塞控制

    怕谁

    接收方来不及

    网络路径堵了

    信号

    接收窗口 rwnd

    丢包/延迟 → 调 cwnd

    类比

    货架满了别送

    高速堵车大家减速

    发送方实际可发 ≈ min(rwnd, cwnd)(概念层)。

    2.5 常见连接状态(看 ss 会用到)

    状态

    人话

    LISTEN

    服务端在窗口等客

    SYN-SENT

    客户端已发 SYN,在等回应

    SYN-RECV

    服务端收到 SYN,回了 SYN-ACK,在等最终 ACK

    ESTABLISHED

    已接通,可传数据

    FIN-WAIT-* / CLOSE-WAIT / LAST-ACK

    正在关闭的不同阶段

    TIME-WAIT

    主动关闭方「冷静期」

    CLOSED

    没了

    高并发短连接 → TIME-WAIT 变多:常见,不一定是故障
    半开很多 SYN-RECV → 可能被打或积压
    大量 CLOSE-WAIT → 应用没及时 close,值得查代码

    2.6 字节流与「粘包 / 拆包」

    TCP 不保证「一次 write = 一次 read」。

    发送方连续 write:
    write("HELLO")
    write("WORLD")

    接收方可能:
    一次读到 "HELLOWORLD" ← 粘在一起
    或先 "HEL" 再 "LOWORLD" ← 拆开了

    所以应用层必须自己定边界:

    • 固定长度

    • 分隔符(如 \\n)

    • 长度前缀(先发 4 字节长度再发内容)——最常用

    下文示例会演示长度前缀。


     

    三、应用:谁在用 TCP?

    场景

    说明

    HTTP/1.1、HTTP/2

    经典网页与 API(HTTP/3 改走 QUIC)

    SSH / 数据库 / SMTP

    要完整可靠

    多数 RPC 框架默认

    gRPC 也可基于 HTTP/2→TCP

    文件传输

    完整性优先

    开发者日常:你写的业务代码很少手搓 TCP,但 超时、连接池、TIME-WAIT、探活 都踩在 TCP 语义上。


    四、示例程序:可运行的 TCP 收发

    Python 3;两个终端。先启动 server,再跑 client。

    4.1 最小 Echo 服务端

    tcp_server.py:

    #!/usr/bin/env python3
    """TCP Echo:一行进,一行出(以 \\\\n 为边界,避免粘包教学混在一起)"""
    import socket

    HOST, PORT = "0.0.0.0", 8888

    def handle(conn: socket.socket, addr):
    print(f"[server] client {addr} connected")
    with conn:
    f = conn.makefile("rwb")
    for line in f: # 按行读,边界=换行
    if not line:
    break
    print(f"[server] recv {line!r}")
    f.write(b"ECHO: " + line)
    f.flush()
    print(f"[server] client {addr} closed")

    def main():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((HOST, PORT))
    s.listen(128)
    print(f"[server] listening TCP {HOST}:{PORT}")
    while True:
    conn, addr = s.accept()
    handle(conn, addr) # 演示用同步;生产用线程/异步

    if __name__ == "__main__":
    main()

    4.2 最小客户端

    tcp_client.py:

    #!/usr/bin/env python3
    import socket
    import sys

    SERVER = ("127.0.0.1", 8888)

    def main():
    msg = (" ".join(sys.argv[1:]) if len(sys.argv) > 1 else "hello TCP") + "\\n"
    with socket.create_connection(SERVER, timeout=3) as s:
    print(f"[client] connected, send {msg!r}")
    s.sendall(msg.encode())
    data = s.recv(4096)
    print(f"[client] reply: {data!r}")

    if __name__ == "__main__":
    main()

    运行:

    # 终端1
    python3 tcp_server.py

    # 终端2
    python3 tcp_client.py
    python3 tcp_client.py 你好挂号件

    同时另开终端观察连接:

    ss -antp | grep 8888
    # 应能看到 ESTAB 或短连接后的 TIME-WAIT

    4.3 粘包实验:故意不按行协议

    tcp_sticky_server.py(一次性读大缓冲,打印原始字节):

    #!/usr/bin/env python3
    import socket

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", 8889))
    s.listen(1)
    print("sticky server :8889")
    conn, addr = s.accept()
    print("from", addr)
    # 故意等一会儿,让客户端多次 send 先到达
    import time; time.sleep(1)
    data = conn.recv(4096)
    print("one recv got:", data)
    conn.close()
    s.close()

    tcp_sticky_client.py:

    #!/usr/bin/env python3
    import socket
    import time

    s = socket.create_connection(("127.0.0.1", 8889))
    s.send(b"HELLO")
    s.send(b"WORLD")
    time.sleep(0.1)
    s.close()
    python3 tcp_sticky_server.py # 终端1
    python3 tcp_sticky_client.py # 终端2

    你很可能看到 一次 recv 得到 b'HELLOWORLD' —— 这就是粘包体感。

    4.4 长度前缀协议(推荐写法)

    约定:先发 4 字节大端长度,再发 payload。

    tcp_framed.py:

    #!/usr/bin/env python3
    """长度前缀帧:解决粘包/拆包"""
    import socket
    import struct
    import sys

    HDR = struct.Struct("!I") # 4字节无符号长度

    def send_frame(sock: socket.socket, payload: bytes) -> None:
    sock.sendall(HDR.pack(len(payload)) + payload)

    def recv_exact(sock: socket.socket, n: int) -> bytes:
    buf = b""
    while len(buf) < n:
    chunk = sock.recv(n – len(buf))
    if not chunk:
    raise ConnectionError("peer closed")
    buf += chunk
    return buf

    def recv_frame(sock: socket.socket) -> bytes:
    (length,) = HDR.unpack(recv_exact(sock, 4))
    if length > 10 * 1024 * 1024:
    raise ValueError("frame too large")
    return recv_exact(sock, length)

    def run_server(port=8890):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", port))
    s.listen(1)
    print(f"framed server :{port}")
    conn, addr = s.accept()
    with conn:
    print("client", addr)
    while True:
    try:
    payload = recv_frame(conn)
    except ConnectionError:
    break
    print("got", payload)
    send_frame(conn, b"ACK:" + payload)

    def run_client(port=8890):
    with socket.create_connection(("127.0.0.1", port)) as s:
    for msg in [b"alpha", b"beta", b"gamma"]:
    send_frame(s, msg)
    print("reply", recv_frame(s))

    if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "server":
    run_server()
    else:
    run_client()
    python3 tcp_framed.py server # 终端1
    python3 tcp_framed.py # 终端2

    结论: 业务消息边界要自己定;TCP 只保证字节可靠有序。

    4.5 观察握手(可选)

    # 终端1:抓 8888
    sudo tcpdump -ni lo -c 20 'tcp port 8888'

    # 终端2:起 server + client
    python3 tcp_server.py &
    python3 tcp_client.py

    在回环口上应能看到 SYN / SYN-ACK / ACK 交换。


     

    五、问题定位:TCP 翻车怎么查?

    5.1 现象速查

    现象

    可能原因

    先做什么

    Connection timed out

    路径丢弃、安全组、黑洞

    ping;抓包有无 SYN-ACK

    Connection refused

    到达主机但未 LISTEN

    ss -lnt

    建连很慢

    丢包重传、路由绕路、策略

    curl -v 看 Connect;mtr

    传大数据中途卡住

    拥塞、窗口、对端不读

    ss -ti 看重传/窗口(版本支持时)

    大量 TIME-WAIT

    短连接高频

    评估连接池/长连接;未必调参

    大量 CLOSE-WAIT

    应用未 close

    查服务端代码与 fd 泄漏

    5.2 命令清单

    # 谁在听
    ss -lntp

    # 连接状态分布
    ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn

    # 看某端口
    ss -antp | grep :8888

    # 客户端侧快速测
    curl -v –connect-timeout 3 telnet://127.0.0.1:8888
    # 或
    nc -vz 127.0.0.1 8888

    5.3 排障流程图

    5.4 和 UDP 排障差异

    UDP:常靠超时猜
    TCP:常有 refused / reset / 重传计数 / 状态机可观察


     

    六、实验清单(建议全做)

    编号

    实验

    收获

    A

    Echo 收发

    TCP 最小闭环

    B

    ss 观察 ESTAB/TIME-WAIT

    状态不是玄学

    C

    粘包实验

    理解字节流

    D

    长度前缀帧

    正确应用协议写法

    E

    tcpdump 看握手

    SYN 三步长什么样


     

    七、附录:C 语言最小 Echo(Linux)

    tcp_server.c

    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>

    int main(void) {
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(lfd, 128);
    printf("TCP server :8888\\n");

    for (;;) {
    struct sockaddr_in peer;
    socklen_t plen = sizeof(peer);
    int cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
    char buf[4096];
    ssize_t n = read(cfd, buf, sizeof(buf));
    if (n > 0) {
    write(cfd, "ECHO:", 5);
    write(cfd, buf, n);
    }
    close(cfd);
    }
    }

    tcp_client.c

    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>

    int main(int argc, char **argv) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server = {0};
    server.sin_family = AF_INET;
    server.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &server.sin_addr);
    connect(fd, (struct sockaddr *)&server, sizeof(server));

    const char *msg = argc > 1 ? argv[1] : "hello";
    write(fd, msg, strlen(msg));
    char buf[4096];
    ssize_t n = read(fd, buf, sizeof(buf)-1);
    if (n > 0) { buf[n] = 0; printf("%s\\n", buf); }
    close(fd);
    return 0;
    }
    gcc -o tcp_server tcp_server.c && gcc -o tcp_client tcp_client.c
    ./tcp_server &
    ./tcp_client world


    本章小结

    TCP = 面向连接的可靠字节流
    握手同步,挥手告别,TIME-WAIT 防旧包
    序号/ACK/重传/窗口构成可靠与节奏
    流量控制护接收方,拥塞控制护整网
    字节流无消息边界 → 应用层自己定帧
    示例:socket STREAM + accept/connect + 长度前缀
    排障:ss 看状态,tcpdump 看 SYN,区分 refused/timeout

    赞(0)
    未经允许不得转载:171主机测评 » TCP 专章:三次握手之后,可靠管道里发生了什么?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址