4、Python Socket实战:创建一个简单的TCP回显服务器与客户端

好,咱们终于到了动手写代码的环节了。

前面几章聊了不少理论——TCP三次握手、Socket是什么、数据怎么在网络上流动。说实话,光看这些概念,你可能会觉得有点虚。我自己当年学网络编程时,也是看了好几遍理论,但真正写出一行能跑通的代码,才感觉「哦,原来是这么回事」。

这一章,我们就用Python写一个最简单的TCP回显服务器和客户端。所谓「回显」,就是客户端发什么,服务器就原样返回什么。别看它简单,它包含了TCP通信的完整骨架。你把这个搞懂了,后面理解TLS握手、加密通道,就有了最扎实的基础。

4.1 先搭个架子:TCP回显服务器

我们先写服务器端。服务器要做什么?说白了就三步:

  1. 创建一个Socket,绑定到某个IP和端口上。
  2. 开始监听,等待客户端连接。
  3. 接受连接,接收数据,再原样发回去。

嗯,就这么简单。我刚开始写的时候,总觉得应该有什么高大上的魔法,结果发现核心代码也就十几行。

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 再写个客户端:主动连接,发送数据

服务器写好了,客户端就更简单了。客户端只需要:

  1. 创建一个Socket。
  2. 连接到服务器的IP和端口。
  3. 发送数据,然后接收回显。
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通信模型。

核心要点:这个回显过程背后,其实发生了完整的TCP三次握手(建立连接)、数据传输、四次挥手(断开连接)。你现在看到的 connect() 和 accept() 成功返回,就意味着三次握手已经完成了。

4.4 这张图帮你理清流程

下面这张图,把服务器和客户端的交互过程画出来了。我建议你对照着代码看,会更清楚。

TCP回显服务器与客户端交互流程 TCP回显服务器与客户端交互流程 服务器 客户端 socket() → bind() → listen() socket() connect() → 三次握手 accept() 返回新socket connect() 返回 send("Hello") recv() 收到数据 send("Hello") 回显 recv() 收到回显 close() → 四次挥手 close() close() 虚线表示连接建立和断开阶段,实线表示数据传输阶段

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就轻松多了。