注:本文基于模型知识整理,建议结合权威教材与 RFC 原文核对细节。
一句话定义
套接字(socket)是应用进程访问传输层服务的门:一个文件描述符,附加上(协议族、本地地址:端口、远端地址:端口)的连接状态,TCP 与 UDP 的收发都通过它完成。
为什么重要
所有网络库与中间件(requests、gRPC、Nginx、数据库驱动)的底层都是 socket。理解它之后,端口占用、连接被拒、程序「卡住」、粘包这些日常问题才看得见成因;亲手写一个 echo server,是把 kp-018 的握手与关闭、kp-021 的字节流语义落到代码里的最短路径,也是一切网络服务的最小模型。
前置知识
建议先读 kp-018《TCP 连接管理》,掌握三次握手、四次挥手与端口语义;UDP 侧的无连接收发对照 kp-017《UDP》;代码中会直接遇到字节流无消息边界导致的拆包问题,对应 kp-021《TCP 工程实践》。
核心概念
- 套接字抽象:对应用是一个 fd(file descriptor,文件描述符),对内核是(协议族 AF_INET、本地 IP:端口、远端 IP:端口)构成的状态;SOCK_STREAM 对应 TCP,SOCK_DGRAM 对应 UDP。
- TCP 服务端五步:socket(创建)→ bind(绑定本地 IP:端口)→ listen(进入监听,backlog 限定等待队列长度,对应半连接/全连接队列)→ accept(从已完成三次握手的全连接队列取出一条连接,返回新的连接 fd)→ read/write 循环 → close。关键认知:三次握手由内核在网络层自动完成,accept 只是取货,不参与握手本身。
- 客户端三步:socket → connect(向服务器发起并阻塞到握手完成)→ read/write;不显式 bind 时由内核自动分配临时端口。
- UDP 版:无 connect(也可以 connect 锁定默认对端,但不改变无连接语义),直接 sendto/recvfrom 一问一答,服务器无需为每个客户端维持连接状态。
图示
TCP 服务端与客户端的系统调用时序:
服务端 客户端
socket() socket()
bind(0.0.0.0:9000) 绑定端口
listen(5) 监听 + backlog 队列
accept() ◀── 取一条已完成三次握手的连接 ◀── connect() 触发三次握手
│
read() ◀──────────────── 数据 ─────────────── write()
write() ──────────────── 回显 ───────────▶ read()
│(对端关闭后 read 返回空,循环退出)
close() close()原理与机制
完整可运行的 Python TCP echo(服务端加客户端约 20 行,含注释):
# echo_server.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP 套接字
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许重用 TIME_WAIT 端口
s.bind(("", 9000)) # 绑定所有网卡的 9000 端口
s.listen(5) # backlog:等待 accept 的已完成连接队列长度
print("listening on :9000")
conn, addr = s.accept() # 阻塞,直到取出一条握手完成的连接
with conn:
while True:
data = conn.recv(1024) # 阻塞读,最多 1024 字节;对端关闭返回 b""
if not data:
break
conn.sendall(data) # 回显(sendall 保证全部写出)
s.close()
# echo_client.py
import socket
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(("127.0.0.1", 9000)) # 触发三次握手,阻塞到完成
c.sendall(b"hello")
print(c.recv(1024)) # 输出 b'hello'
c.close()UDP 对照:u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM);服务端 data, addr = u.recvfrom(1024) 返回数据与对端地址,回信用 u.sendto(data, addr)——没有连接,天然一问一答。
常见错误与排查。 EADDRINUSE(地址已被占用):服务重启时旧连接处于 TIME_WAIT 仍占用端口,服务端在 bind 前设置 SO_REUSEADDR 即可解决。程序「卡住」:默认套接字是阻塞模式,read 在对端不发数据时会一直挂在系统调用上——这不是死循环,是等待;需要对读设超时,或改用非阻塞加 IO 多路复用。粘包:TCP 是字节流、没有消息边界,recv(1024) 一次可能读到半条消息,也可能读到一条半消息拼在一起(衔接 kp-021 的拆包),必须按定长、分隔符或长度前缀在应用层拆分。
实例分析
开两个终端:先运行 echo_server.py,再用 nc 127.0.0.1 9000(或运行 client)连上,敲任意字符都会原样返回;Ctrl+C 断开客户端,服务端 recv 返回空字节串、循环退出——亲手走完连接、传输、关闭的全生命周期,并可配合 kp-034 的 tcpdump 观察 9000 端口上的握手与挥手报文。生产级服务还必须处理并发:为每个连接开线程,或用 select/poll/epoll 单线程管理大量连接(IO 多路复用),那是下一步的扩展方向。
常见误区
- 以为 accept 参与三次握手:握手在 connect 到达时由内核完成,accept 只从就绪队列取连接;backlog 满时新连接可能被丢弃。
- 把 recv 的返回值当「一条完整消息」:它只是「当前已到达的字节」,长度由参数上限与网络到达情况决定。
- 用 send 而非 sendall:send 可能只写出一部分数据,要用 sendall 或循环写直到写完。
- 混淆监听 fd 与连接 fd:accept 返回的新 fd 才用于与该客户端通信,监听 fd 只负责受理连接。
- 认为 UDP socket 一定不能 connect:可以 connect 锁定默认对端从而直接用 send/recv,但无连接、不保证可靠的语义不变。
自测题
- TCP 服务端的五个系统调用顺序及各自作用是什么?
答案要点: socket 创建套接字;bind 绑定本地地址与端口;listen 进入监听并设定 backlog 队列长度;accept 从已完成三次握手的全连接队列取出一条连接、返回新 fd;之后 read/write 收发数据,close 关闭。
- accept 与三次握手是什么关系?
答案要点: 握手由内核在客户端 connect 到达后自动完成,完成的连接进入全连接队列;accept 只是从队列取出一条连接返回新 fd,不参与握手过程本身。
- 服务重启时报 EADDRINUSE,原因与解法是什么?
答案要点: 旧连接处于 TIME_WAIT 状态仍占用该端口;服务端在 bind 前设置 SO_REUSEADDR,允许重用处于 TIME_WAIT 的地址与端口。
- echo 程序「卡在 recv 不动」是什么现象?
答案要点: 默认阻塞模式下,对端不发数据时 recv 挂起等待,属正常阻塞行为而非故障;应对读设置超时,或改用非阻塞 socket 配合 select/epoll 等IO多路复用。
- 发一条 100 字节消息,为什么对端 recv 可能只读到 37 字节?
答案要点: TCP 是无消息边界的字节流,recv 返回的是当前已到达的字节数;一条消息可能分多个报文段到达,应用层必须按定长、分隔符或长度前缀做拆包(粘包问题)。
延伸阅读
- Stevens《UNIX 网络编程 卷1:套接字联网 API》
- Python 官方标准库文档:socket 模块