2. TCP Server 端:从 Socket 创建到连接关闭
这一章,我们来亲手搭建一个 TCP 服务端。说白了,就是让一台机器「蹲」在一个端口上,等着别人来找它说话。
我刚开始学网络编程时,总觉得这玩意儿很玄乎。后来做第一个线上项目,服务端一启动就崩,排查了半天才发现是端口被占用了……嗯,从那以后,我对每一步都格外小心。
2.1 整体流程概览
一个标准的 TCP Server,生命周期就这六步:
- 创建 Socket —— 向操作系统申请一个网络通信的「门」
- 绑定地址 —— 把这个门挂到某个 IP 和端口上
- 监听连接 —— 告诉内核:我准备好了,有人敲门就通知我
- 接受连接 —— 真正把来访者「领进门」,得到一个专用的通信通道
- 收发数据 —— 用这个通道和客户端聊天
- 关闭连接 —— 聊完了,礼貌地说再见,释放资源
下面这张图,把这六个步骤串了起来,你可以先看个全貌:
2.2 创建 Socket
在 Python 里,创建 Socket 就一行代码:
import socket
# 创建一个 IPv4 的 TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
这里有两个参数:
- AF_INET —— 指定使用 IPv4 地址族。如果你要玩 IPv6,就用 AF_INET6。
- SOCK_STREAM —— 指定使用 TCP 协议,面向连接、可靠传输。
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这行代码的作用是允许端口重用。为什么?因为服务端崩溃后,端口会进入 TIME_WAIT 状态,不加这个选项,重启时会报「Address already in use」。我在生产环境吃过这个亏,后来就养成了这个习惯。
2.3 绑定地址
绑定地址,就是把 Socket 和某个 IP + 端口拴在一起。
server_address = ('0.0.0.0', 8888)
server_socket.bind(server_address)
关于 IP 地址,有几种常见写法:
| IP 地址 | 含义 | 适用场景 |
|---|---|---|
'0.0.0.0' |
监听所有网络接口 | 服务器有多块网卡时,接受任意 IP 的访问 |
'127.0.0.1' |
仅监听本地回环地址 | 只允许本机程序连接,外部无法访问 |
'192.168.1.100' |
监听特定网卡 | 只接受来自某个网段的连接 |
2.4 监听连接
绑定完地址,Socket 还处于「待机」状态。你需要调用 listen() 告诉内核:我开始接客了。
server_socket.listen(5)
print("服务器已启动,等待客户端连接...")
listen() 的参数叫「backlog」,表示连接请求队列的最大长度。什么意思呢?
当多个客户端同时发起连接,而你的服务端来不及处理时,内核会把它们排成一个队列。backlog 就是这个队列的上限。超过上限的请求,会被内核直接拒绝。
我个人建议:
- 开发环境设 5 就够了
- 生产环境根据并发量,一般设 128 或 256
- 不要设太大,否则队列积压会消耗大量内存
2.5 接受连接
这一步是关键。调用 accept() 后,程序会阻塞在这里,直到有客户端连上来。
client_socket, client_address = server_socket.accept()
print(f"客户端 {client_address} 已连接")
accept() 返回两个东西:
- client_socket —— 一个全新的 Socket 对象,专门用来和这个客户端通信
- client_address —— 客户端的 IP 和端口
2.6 收发数据
有了 client_socket,就可以和客户端「聊天」了。TCP 是流式协议,数据像水流一样,没有边界。所以收发数据时,你要自己定义「什么时候算一条完整的消息」。
# 接收数据
data = client_socket.recv(1024)
print(f"收到: {data.decode('utf-8')}")
# 发送数据
response = "Hello, client!"
client_socket.send(response.encode('utf-8'))
recv() 的参数是缓冲区大小,单位是字节。1024 表示一次最多读 1024 字节。如果对方发来的数据超过 1024 字节,剩下的会留在内核缓冲区里,下次 recv() 再读。
这里有个坑:recv() 返回空字节 b'' 时,表示对方已经关闭了连接。你一定要检查这个条件,否则会陷入死循环。
while True:
data = client_socket.recv(1024)
if not data:
print("客户端已断开连接")
break
print(f"收到: {data.decode('utf-8')}")
2.7 关闭连接
聊完了,要记得关门。关闭连接有两个层面:
# 关闭客户端连接
client_socket.close()
# 关闭服务端监听(通常在程序退出时)
server_socket.close()
为什么要分开关?因为 server_socket 是「大门」,client_socket 是「客房」。你关掉客房,不影响大门继续接待新客人。只有程序彻底退出时,才需要关大门。
另外,Python 的 with 语句可以帮你自动管理资源:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('0.0.0.0', 8888))
s.listen(5)
# ... 业务逻辑
# 退出 with 块时,s 会自动 close()
但要注意,with 只关闭 server_socket,不会自动关闭 accept() 返回的 client_socket。你需要自己管理每个 client_socket 的生命周期。
2.8 完整示例:一个能跑起来的 TCP Server
把上面所有步骤串起来,就是一个最简单的 TCP 服务端:
import socket
def main():
# 1. 创建 Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址
server_address = ('0.0.0.0', 8888)
server_socket.bind(server_address)
# 3. 监听连接
server_socket.listen(5)
print(f"服务器启动,监听 {server_address}")
try:
while True:
# 4. 接受连接
client_socket, client_address = server_socket.accept()
print(f"客户端 {client_address} 已连接")
try:
# 5. 收发数据
while True:
data = client_socket.recv(1024)
if not data:
break
message = data.decode('utf-8')
print(f"收到: {message}")
response = f"服务器已收到: {message}"
client_socket.send(response.encode('utf-8'))
finally:
# 6. 关闭客户端连接
client_socket.close()
print(f"客户端 {client_address} 连接已关闭")
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
server_socket.close()
print("服务器已关闭")
if __name__ == '__main__':
main()
这个例子虽然简单,但已经包含了 TCP Server 的完整骨架。你可以直接复制运行,然后用 telnet 127.0.0.1 8888 或者 nc 127.0.0.1 8888 来测试。
- 六步流程:socket → bind → listen → accept → recv/send → close
- 监听 Socket 和连接 Socket 是两回事,别搞混
- recv() 返回空字节表示对方断开,一定要检查
- 记得设置 SO_REUSEADDR,避免端口被占用
- 用 try-finally 或 with 确保资源被释放
这六步,是 TCP 服务端编程的「基本功」。后面的章节,我们会在这个基础上,一步步加上并发处理、消息边界、心跳检测、断线重连……把骨架变成一个有血有肉的可靠通信框架。