Socket(套接字)
1. 定义
Socket(套接字)是操作系统提供的一套网络通信编程接口,是两台机器上的两个程序之间通信的端点 (endpoint)。你想让程序收发网络数据,不用自己去操心 TCP/IP 协议的一堆细节,只要通过 socket 这组 API(socket / bind / listen / accept / connect / send / recv / close)就能完成。
一句话概括:Socket 是应用程序和网络协议栈之间的一扇门——程序把数据丢进门,操作系统负责按 TCP/IP 送出去;对方从门里把数据取出来。
类比:Socket 像电话插座 + 电话机。IP 地址是电话号码(找到哪台机器),端口是分机号(找到机器里的哪个程序),建立连接就是拨通电话,之后双方就能对着话筒收发声音(数据)。挂电话就是
close。“Socket” 原意就是”插座/插口”——把两个程序”插”在一起通信。
2. Socket 在哪一层(它不是协议,是接口)
很多人误以为 socket 是一种协议,其实不是。它是夹在应用程序和传输层协议之间的一层抽象:
┌─────────────────────┐
│ 你的程序(应用层) │ ← Nginx / 浏览器 / 你写的 server.py
├─────────────────────┤
│ Socket API │ ← 操作系统提供的"门":socket()/bind()/send()...
├─────────────────────┤
│ 传输层 TCP / UDP │ ← 真正的协议,负责可靠传输/分包
├─────────────────────┤
│ 网络层 IP │ ← 负责寻址、路由到目标机器
├─────────────────────┤
│ 链路层 / 网卡 │ ← 电信号/网线/WiFi
└─────────────────────┘
关键认知:socket 是”接口”不是”协议”。 同一套 socket API,既能创建走 TCP 的 socket,也能创建走 UDP 的 socket——只是创建时参数不同(见第 8 节)。它屏蔽了底层协议差异,让你用统一的方式编程。
3. 一个 Socket 是怎么被唯一确定的
一条 TCP 连接由四元组 (4-tuple) 唯一标识:
(源 IP, 源端口, 目标 IP, 目标端口)
| 要素 | 作用 |
|---|---|
| 源 IP + 源端口 | 标识连接的本机这一端 |
| 目标 IP + 目标端口 | 标识连接的对方那一端 |
由此推出一个常被问的问题:一个服务器的 80 端口,怎么同时和成千上万个客户端通信? 因为区分连接靠的是整个四元组,不是只看服务器端口。服务器端口都是 80,但每个客户端的 (IP, 端口) 不同,所以四元组各不相同,操作系统能把它们区分成一条条独立连接。
补充概念:服务器上负责”监听”的那个 socket(监听 socket)和每次
accept出来负责”和某个具体客户端通信”的 socket(连接 socket)是不同的 socket。一个监听 socket 能孵化出无数个连接 socket。
4. TCP Socket 的生命周期(服务端 vs 客户端)
这是 socket 编程的骨架,也解释了 server.md 第 5 节那个”监听循环”底下每一步到底调了什么:
服务端 (Server) 客户端 (Client)
┌────────────────────┐
│ socket() 创建套接字 │ ┌────────────────────┐
│ bind() 绑定IP:端口│ │ socket() 创建套接字 │
│ listen() 开始监听 │ └─────────┬──────────┘
└─────────┬──────────┘ │
│ 阻塞等待 connect() │ 发起连接
accept() ◀├───────────── 三次握手 ───────────────┤
│ 返回一个"连接socket" │
▼ ▼
┌────────────────────┐ ◀── 数据 ──▶ ┌────────────────────┐
│ recv() / send() │ │ send() / recv() │
│ 收发数据(可循环多次) │ │ 收发数据 │
└─────────┬──────────┘ └─────────┬──────────┘
│ close() ◀── 四次挥手 ──▶ │ close()
▼ ▼
关闭连接 关闭连接
| 系统调用 | 谁用 | 干什么 |
|---|---|---|
socket() | 双方 | 创建一个套接字,指定用 TCP 还是 UDP |
bind() | 服务端 | 把 socket 绑到某个 IP:端口(“我在这个门牌等”) |
listen() | 服务端 | 把 socket 转为监听状态,设置等待队列长度 |
accept() | 服务端 | 阻塞等连接进来,来一个就返回一个新的”连接 socket” |
connect() | 客户端 | 主动连服务端的 IP:端口,触发三次握手 |
send()/recv() | 双方 | 发送 / 接收数据 |
close() | 双方 | 关闭连接,触发四次挥手 |
客户端一般不用 bind:它的源端口由操作系统随机分配一个空闲端口(临时端口),所以你写客户端时只管
connect就行。
5. 上手代码:一个能跑的 Echo 服务器(Python)
Python 的 socket 模块几乎是对系统调用的直接封装,最适合看清 socket 本质。下面这个”回声服务器”把收到的内容原样发回:
服务端 echo_server.py:
import socket
# 1. 创建 socket:AF_INET=IPv4,SOCK_STREAM=TCP
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 关键:允许端口复用,避免重启时报 "Address already in use"(见第 10 节)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. bind:绑定到本机 8888 端口。'' 表示监听所有网卡(0.0.0.0)
server.bind(('', 8888))
# 3. listen:开始监听,参数是等待队列(backlog)长度
server.listen(5)
print('服务器在 8888 端口监听中...')
while True: # 监听循环
# 4. accept:阻塞,直到有客户端连进来
conn, addr = server.accept() # conn 是"连接 socket",addr 是客户端地址
print(f'客户端接入: {addr}')
with conn:
while True:
# 5. recv:收数据,参数是一次最多收多少字节
data = conn.recv(1024)
if not data: # 收到空 = 对方关闭了连接
break
print(f'收到: {data.decode()}')
conn.sendall(data) # 6. 原样发回(echo)
print(f'客户端断开: {addr}')客户端 echo_client.py:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8888)) # 连接服务器,触发三次握手
client.sendall('你好 socket'.encode()) # 发数据
reply = client.recv(1024) # 收回显
print(f'服务器回复: {reply.decode()}')
client.close() # 关闭,触发四次挥手python echo_server.py # 一个终端起服务端
python echo_client.py # 另一个终端跑客户端
# 也可以用 nc 当客户端测试:
nc 127.0.0.1 8888对照第 4 节的图,代码里的
socket/bind/listen/accept/recv/send/close一一对应。你平时用的 Flask、Express 只是把这套东西藏在框架里了。
6. 建连接与断连接:三次握手 & 四次挥手
connect() 和 close() 背后是 TCP 的握手/挥手,socket 帮你自动完成:
三次握手(建立连接,connect 时发生):
客户端 ──── SYN ────▶ 服务端 "我想连你,序号x"
客户端 ◀── SYN+ACK ── 服务端 "行,我序号y,确认你的x"
客户端 ──── ACK ────▶ 服务端 "确认你的y,开始吧"
连接建立 ✓
为什么要三次?为了双方都确认”我能发、我能收、你能发、你能收”都正常,避免用一条实际已失效的旧连接。
四次挥手(关闭连接,close 时发生):
主动方 ──── FIN ────▶ 被动方 "我没数据要发了"
主动方 ◀─── ACK ───── 被动方 "知道了"
主动方 ◀─── FIN ───── 被动方 "我也发完了"
主动方 ──── ACK ────▶ 被动方 "好,拜拜"
为什么四次?因为关闭是双向的,一方说完不代表另一方也说完,两个方向要各关一次,所以中间的 ACK 和 FIN 不能像握手那样合并。
主动关闭的一方最后会进入 TIME_WAIT 状态等一会儿(约 2 倍报文最大存活时间)才真正释放,确保最后的 ACK 对方收到、且旧连接的残留报文消散。这解释了第 10 节”服务器刚重启端口还被占用”的坑。
7. 阻塞 / 非阻塞 / IO 多路复用(连接并发的关键)
第 5 节的 accept() 和 recv() 默认是阻塞的——没连接/没数据就卡住等。这就带来 server.md 第 7 节讲的并发问题。socket 层面有几种应对:
| 模式 | 做法 | 特点 |
|---|---|---|
| 阻塞 (blocking) | 调用后卡住直到有结果 | 简单,但一个线程同时只能盯一个 socket |
| 非阻塞 (non-blocking) | 没数据立刻返回错误码,程序轮询 | 不卡住,但空转轮询浪费 CPU |
| IO 多路复用 | 用 select/poll/epoll 一次性盯一堆 socket,谁就绪通知谁 | 一个线程高效管上万连接,高并发首选 |
# Python 用 selectors 封装了 epoll/kqueue,一个线程管多个连接
import selectors, socket
sel = selectors.DefaultSelector()
server = socket.socket()
server.bind(('', 8888)); server.listen(); server.setblocking(False)
sel.register(server, selectors.EVENT_READ) # 把监听socket交给selector盯着
while True:
events = sel.select() # 阻塞直到"任意一个"注册的socket就绪
for key, _ in events:
sock = key.fileobj
# 谁就绪就处理谁(新连接 or 有数据可读)...演进逻辑:
select→poll→epoll(Linux) /kqueue(macOS/BSD)。epoll是 Nginx、Redis、Node.js 能用单线程/少量线程扛百万连接的底层秘密——它避免了每次都遍历所有连接,只回调”有变化”的那些。详见server.md第 7 节并发模型。
8. TCP Socket vs UDP Socket
创建 socket 时的第二个参数决定走哪种协议:
TCP (SOCK_STREAM) | UDP (SOCK_DGRAM) | |
|---|---|---|
| 连接 | 面向连接(要握手) | 无连接(直接发) |
| 可靠性 | 可靠:保证到达、有序、不重复 | 不保证:可能丢、可能乱序 |
| 数据形态 | 字节流(无消息边界,会”粘包”) | 数据报(一发一收,有边界) |
| 速度/开销 | 较慢,开销大 | 快,开销小 |
| 典型场景 | HTTP、数据库、文件传输——不能错的 | 直播、游戏、DNS、语音——快比全更重要 |
| 关键 API | listen/accept/connect | 没有连接,直接 sendto/recvfrom |
# UDP 服务端:没有 listen/accept,收到就地回
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.bind(('', 9999))
while True:
data, addr = udp.recvfrom(1024) # 收数据 + 对方地址
udp.sendto(data, addr) # 按地址发回一句话:TCP 像打电话(先拨通、保证听清、按顺序);UDP 像寄明信片(写上地址直接扔进邮筒,不保证送到、不保证顺序,但快)。
9. Socket 的几种类型
| 类型 | 说明 | 用途 |
|---|---|---|
| 流式 (SOCK_STREAM) | 基于 TCP,字节流 | 绝大多数网络服务 |
| 数据报 (SOCK_DGRAM) | 基于 UDP,数据报 | 实时性优先的场景 |
| Unix Domain Socket | 同一台机器内进程间通信,走文件系统路径而非 IP:端口 | 本机进程通信,比走网络快(如 Nginx 连本机 PHP-FPM、Docker 的 /var/run/docker.sock) |
| 原始 socket (RAW) | 绕过 TCP/UDP 直接收发 IP 包 | 抓包、ping、自定义协议(需管理员权限) |
Unix Domain Socket 值得记一笔:当两个程序在同一台机器上,用它比走 TCP 回环(
127.0.0.1)更快,因为省掉了协议栈的封装/解封。你在docker.md里见过的docker.sock就是它。
10. 常见坑 / 误区
- ❌ “socket 是一种协议” → 错,它是接口/抽象,底层协议是 TCP 或 UDP(见第 2 节)
- ❌ “一个端口只能服务一个客户端” → 错,连接靠四元组区分,一个监听端口能同时维持海量连接(见第 3 节)
- ❌
Address already in use(重启服务器最常见) → 上次的连接还在 TIME_WAIT,端口没完全释放。开发时加SO_REUSEADDR(见第 5 节代码)即可复用 - ❌ TCP “粘包/拆包” → TCP 是字节流没有消息边界,你
send两次对方可能一次recv全收到、或分几次收到。要自己定协议界定消息边界(如加长度头、用分隔符),别以为一次 recv 就是一条完整消息 - ❌ “recv 到多少就是对方发了多少” → 不一定,
recv(1024)只是”最多收 1024”,实际可能更少,要循环收直到够一条完整消息 - ❌ “recv 返回空字符串是出错” → 不是,TCP 里
recv返回空 (b'') 表示对方正常关闭了连接,这是判断断开的标志 - ❌ “客户端也要 bind” → 一般不用,源端口由系统自动分配(见第 4 节)
- ❌ “阻塞 socket 也能扛高并发” → 单线程阻塞模型一次只能处理一个连接,高并发要用多线程/IO 多路复用/异步(见第 7 节)
- ❌ “close 了数据一定发完了” → 想确保发完再关,注意缓冲区和
SO_LINGER;粗暴 close 可能丢未发数据
11. 延伸阅读 / 关联概念
- 服务器原理 — socket 是服务器”监听循环”的底层实现,务必对照着看;见
server.md - DNS 解析绑定 —
connect前要先把域名解析成 IP,socket 才知道连哪;见dns-binding.md - ping / traceroute — ping 用的就是原始 socket(ICMP);见
ping.md - TCP / UDP — socket 底下的两种传输层协议,决定可靠性与形态;见
tcp-udp.md - HTTP / HTTPS — 最典型的 TCP 之上的应用层协议;见
http.md - epoll / kqueue / select — IO 多路复用的系统调用,高并发服务器的基石
- WebSocket — 建立在 HTTP 之上的全双工长连接,名字带 socket 但属于应用层协议,别和本文的系统 socket 混淆
- Unix Domain Socket — 本机进程间通信,比 TCP 回环更快