4、Python Socket实战:创建一个简单的TCP回显服务器与客户端
好,咱们终于到了动手写代码的环节了。
前面几章聊了不少理论——TCP三次握手、Socket是什么、数据怎么在网络上流动。说实话,光看这些概念,你可能会觉得有点虚。我自己当年学网络编程时,也是看了好几遍理论,但真正写出一行能跑通的代码,才感觉「哦,原来是这么回事」。
这一章,我们就用Python写一个最简单的TCP回显服务器和客户端。所谓「回显」,就是客户端发什么,服务器就原样返回什么。别看它简单,它包含了TCP通信的完整骨架。你把这个搞懂了,后面理解TLS握手、加密通道,就有了最扎实的基础。
4.1 先搭个架子:TCP回显服务器
我们先写服务器端。服务器要做什么?说白了就三步:
- 创建一个Socket,绑定到某个IP和端口上。
- 开始监听,等待客户端连接。
- 接受连接,接收数据,再原样发回去。
嗯,就这么简单。我刚开始写的时候,总觉得应该有什么高大上的魔法,结果发现核心代码也就十几行。
import socket
# 1. 创建TCP Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
server_socket.bind(('127.0.0.1', 8888))
# 3. 开始监听,最大等待队列设为5
server_socket.listen(5)
print('服务器已启动,等待客户端连接...')
# 4. 接受客户端连接
client_socket, client_addr = server_socket.accept()
print(f'客户端 {client_addr} 已连接')
# 5. 接收数据并回显
while True:
data = client_socket.recv(1024)
if not data:
break
print(f'收到: {data.decode()}')
client_socket.send(data) # 原样返回
# 6. 清理
client_socket.close()
server_socket.close()
127.0.0.1(本地回环地址),它只在你的本机上通信,不经过网卡。调试阶段用这个最安全,不用担心被外部扫到。
你可能会问:recv(1024) 里的1024是什么意思?这是缓冲区大小,一次最多读1024字节。如果客户端发来的数据超过这个数,就得循环接收。我在项目中遇到过一个大坑——对方发了个几MB的文件,我一次只recv了1024,结果只收到了开头一点点。所以,生产环境一定要考虑粘包和分包的问题,这个我们后面会专门讲。
4.2 再写个客户端:主动连接,发送数据
服务器写好了,客户端就更简单了。客户端只需要:
- 创建一个Socket。
- 连接到服务器的IP和端口。
- 发送数据,然后接收回显。
import socket
# 1. 创建TCP Socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 连接服务器
client_socket.connect(('127.0.0.1', 8888))
# 3. 发送数据并接收回显
message = 'Hello, TCP Server!'
client_socket.send(message.encode())
response = client_socket.recv(1024)
print(f'服务器回显: {response.decode()}')
# 4. 关闭连接
client_socket.close()
你看,客户端比服务器还短。核心就两个动作:connect 和 send。这里有个细节——send 和 recv 都是阻塞的。也就是说,recv 没收到数据时,程序会卡在那里等。我曾经在线上排查过一个故障,就是因为客户端 recv 时服务器挂了,结果客户端一直阻塞,导致整个线程池被占满。嗯,这个坑后面讲超时设置时我们再细说。
4.3 跑起来看看效果
先启动服务器,再启动客户端。你会看到类似这样的输出:
服务器端:
服务器已启动,等待客户端连接...
客户端 ('127.0.0.1', 54321) 已连接
收到: Hello, TCP Server!
客户端:
服务器回显: Hello, TCP Server!
看到没?客户端发出去的字符串,服务器原封不动地回来了。这就是最基础的TCP通信模型。
connect() 和 accept() 成功返回,就意味着三次握手已经完成了。
4.4 这张图帮你理清流程
下面这张图,把服务器和客户端的交互过程画出来了。我建议你对照着代码看,会更清楚。
4.5 几个容易踩的坑
代码虽然简单,但新手第一次跑的时候,经常会遇到几个问题。我列出来,你心里有个数:
| 问题 | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 端口被占用 | OSError: [Errno 98] Address already in use |
上次程序退出后,端口还没释放 | 等一会儿再试,或者设置 SO_REUSEADDR |
| 连接被拒绝 | ConnectionRefusedError |
客户端连接时,服务器还没启动 | 先启动服务器,再启动客户端 |
| 数据没收到 | 客户端卡在 recv() 不动 |
服务器没发数据,或者连接断了 | 检查服务器是否执行了 send() |
| 粘包 | 两次发送的数据合并成一次收到 | TCP是流式协议,没有消息边界 | 需要自定义消息格式(后面会讲) |
TIME_WAIT 状态,导致重启失败。后来我在 bind() 之前加了一行 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),这个问题就解决了。建议你也养成这个习惯。
4.6 小结
这一章我们亲手写了一个TCP回显服务器和客户端。代码虽然短,但麻雀虽小五脏俱全——它包含了TCP通信的所有核心环节:创建Socket、绑定、监听、连接、收发数据、关闭。
你想想看,后面我们要讲的TLS,本质上就是在这一步的基础上,在传输数据之前先做一次加密握手。如果你现在把Socket的流程吃透了,后面理解TLS就轻松多了。