3、Python Socket编程入门:创建Socket服务端、创建Socket客户端、数据发送与接收、异常处理

好,咱们进入实战环节。前面聊了那么多理论,说白了,Socket就是操作系统给应用层开的一扇门。今天这章,我带你亲手把这扇门推开,写一个能跑起来的服务端和客户端。

我个人习惯,学任何网络编程,先写一个“回声”程序——客户端发什么,服务端原样返回什么。别看简单,它能把整个数据流跑通,后面再往上加业务逻辑就顺了。

3.1 一个最简的Socket服务端

先看服务端。Python标准库里的 socket 模块,开箱即用。

import socket

# 1. 创建 socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 绑定地址和端口
server_socket.bind(('0.0.0.0', 8888))

# 3. 开始监听
server_socket.listen(5)
print('服务端已启动,等待客户端连接...')

# 4. 接受客户端连接
client_socket, client_addr = server_socket.accept()
print(f'客户端 {client_addr} 已连接')

# 5. 接收数据
data = client_socket.recv(1024)
print(f'收到: {data.decode()}')

# 6. 发送响应
client_socket.send(b'Hello from server!')

# 7. 关闭连接
client_socket.close()
server_socket.close()

这里有几个关键点,我当年踩过坑:

  • AF_INET:指定IPv4协议。别写成 AF_INET6,除非你确定客户端也走IPv6。
  • SOCK_STREAM:TCP协议。UDP用 SOCK_DGRAM,但咱们日志系统走TCP,保证不丢包。
  • bind(('0.0.0.0', 8888)):0.0.0.0 表示监听所有网卡。如果只允许本机访问,可以写 '127.0.0.1'。
  • listen(5):这个5是连接等待队列的长度。不是最大连接数,是“还没被accept处理的连接”的上限。

注意: 端口号小于1024需要root权限。开发调试时,建议用 8000-9999 之间的端口。我曾经在客户现场用 bind(('0.0.0.0', 80)) 没加sudo,程序直接崩了,排查了半天才发现是权限问题。

3.2 对应的Socket客户端

服务端写好了,客户端更简单。说白了就是“连上去,说话,听回话,断开”。

import socket

# 1. 创建 socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 连接服务端
client_socket.connect(('127.0.0.1', 8888))

# 3. 发送数据
client_socket.send(b'Hello from client!')

# 4. 接收响应
response = client_socket.recv(1024)
print(f'服务端响应: {response.decode()}')

# 5. 关闭
client_socket.close()

嗯,这里要注意:connect 的地址必须和服务端 bind 的地址能通。如果服务端在另一台机器,把 '127.0.0.1' 换成它的IP。

3.3 数据发送与接收的底层逻辑

很多新手会问:recv(1024) 里的1024是什么意思?是不是一次最多收1024字节?

对,但不全对。1024是缓冲区大小。如果对方发了2000字节,第一次 recv 只能拿到前1024,剩下的需要第二次 recv 才能拿到。这就是TCP的“流”特性——它不帮你分消息边界。

核心概念: TCP是流协议,不是消息协议。你发两次 send,对方可能一次 recv 就全收了;你发一次 send,对方也可能分两次 recv 才能收完。这就是所谓的“粘包”和“拆包”。

我在项目中遇到过一个问题:客户端发了一条JSON日志,服务端 recv 只收到一半,解析JSON直接报错。后来怎么解决的?加了一个简单的长度头——先发4字节的消息长度,再发消息体。服务端先收4字节算出长度,再收对应长度的数据。这个模式后面章节会详细讲。

3.4 异常处理——别让程序裸奔

网络编程里,异常是常态。客户端突然断网、服务端重启、防火墙掐连接……任何一个环节出问题,你的程序都得兜住。

我建议至少捕获这几类异常:

异常类型 触发场景 处理建议
ConnectionRefusedError 服务端没启动,客户端就去连 重试或报错,不要静默失败
TimeoutError 网络延迟,连接或收发超时 设置超时时间,避免线程卡死
ConnectionResetError 对方突然关闭连接 清理资源,关闭socket
BrokenPipeError 向已关闭的连接写数据 写之前检查连接状态

来看一个带异常处理的服务端示例:

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.settimeout(30)  # 设置超时,避免死等
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)

try:
    while True:
        try:
            client_socket, addr = server_socket.accept()
            with client_socket:
                data = client_socket.recv(1024)
                if not data:
                    break  # 客户端关闭连接
                client_socket.send(b'ACK: ' + data)
        except socket.timeout:
            print('等待客户端连接超时,继续监听...')
            continue
        except ConnectionResetError:
            print('客户端异常断开')
            continue
except KeyboardInterrupt:
    print('服务端手动停止')
finally:
    server_socket.close()

小技巧: 用 with client_socket: 可以自动关闭连接,不用手动调 close()。但 server_socket 建议手动管理,因为它的生命周期通常贯穿整个程序。

3.5 本章知识体系总览

下面这张图,把服务端和客户端的核心流程串起来了。你写代码的时候,脑子里可以过一遍这个流程。

Socket 编程核心流程 socket() 创建 bind() 绑定地址 listen() 监听 accept() 等待连接 recv() / send() close() 关闭 socket() 创建 connect() 连接 send() / recv() close() 关闭 TCP 流特性:无消息边界 需自行设计协议(如长度头)解决粘包

这张图左边是服务端流程,右边是客户端流程。虚线箭头表示TCP三次握手建立连接。注意看,服务端多了一个 bind 和 listen,客户端多了一个 connect。其他步骤基本对称。

3.6 避坑指南

最后,分享几个我亲手踩过的坑:

  • 地址已被占用:服务端崩溃后立即重启,有时会报 Address already in use。这是因为操作系统还没释放端口。解决方案:在 bind 之前加 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。
  • send 不一定发完:send 返回的是实际发送的字节数。如果网络拥堵,可能只发了一半。严谨的做法是循环发送,直到所有数据发完。
  • recv 返回空字节:如果 recv 返回 b'',说明对方已经优雅关闭了连接。这时候应该退出接收循环,不要再调 recv。

调试小技巧: 用 telnet 127.0.0.1 8888 可以快速测试服务端是否正常。如果telnet能连上,说明服务端没问题,问题大概率在客户端代码里。