Python网络编程从入门到实战:用socket写个聊天机器人

先看最基础的TCP连接。Python的socket模块封装得挺好的,但官方文档那段对bind()和listen()的解释,读起来跟谜语一样。我直接用代码说话:

python
import socket

创建一个TCP socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

AF_INET表示IPv4,SOCK_STREAM表示TCP

这个设计真的反人类:地址重用必须放在bind之前

server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

绑定到本地地址和端口

server_socket.bind(('0.0.0.0', 8888)) # 0.0.0.0监听所有网卡
server_socket.listen(5) # 最大等待连接数

print(f"服务器启动,监听端口 8888")

while True:
client_socket, addr = server_socket.accept()
print(f"收到来自 {addr} 的连接")

data = client_socket.recv(1024)
print(f"收到数据: {data.decode('utf-8')}")

client_socket.send(b"Hello, client!")
client_socket.close()
`

这段代码看起来简单,但我第一次运行时,bind()就报错"Address already in use"。后来发现是上一个程序没有正常关闭socket,端口被占了。setsockopt那行就是解决这个问题的——设置SO_REUSEADDR让端口可以立即重用。

另一个坑是recv()的阻塞问题。上面这个例子,如果客户端不发数据,recv()就会一直卡在那里,整个服务器都被冻住了。这在生产环境中绝对不行,所以需要把socket改成非阻塞模式:

`python
import socket
import select

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)

设置非阻塞

server_socket.setblocking(False)

用select来管理所有socket

sockets_list = [server_socket]

print("非阻塞服务器启动,监听端口 8888")

while True:
# select会返回可读的socket列表
read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)

for notified_socket in read_sockets:
if notified_socket == server_socket:
# 新连接
client_socket, addr = server_socket.accept()
sockets_list.append(client_socket)
print(f"新连接: {addr}")
else:
# 已有连接发来数据
try:
data = notified_socket.recv(1024)
if data:
print(f"收到: {data.decode('utf-8')}")
notified_socket.send(b"OK")
else:
# 客户端断开连接
sockets_list.remove(notified_socket)
notified_socket.close()
except:
sockets_list.remove(notified_socket)
notified_socket.close()
`

用select的好处很明显:不会阻塞,而且可以同时处理多个连接。但是代码量翻倍了,而且select本身有性能上限——最多监控1024个socket。如果要处理更大量连接,得用epoll(Linux)或kqueue(macOS)。

还有个技巧:如果需要处理多个客户端并保持连接,更常见的方式是用多线程。我写了一个简单的多线程Echo服务器,性能从单线程的3.2秒降到0.8秒:

`python
import socket
import threading

def handle_client(client_socket, addr):
print(f"新线程处理 {addr}")
try:
while True:
data = client_socket.recv(1024)
if not data:
break
message = data.decode('utf-8')
print(f"从 {addr} 收到: {message}")
# echo回客户端
response = f"服务器回复: {message}"
client_socket.send(response.encode('utf-8'))
except ConnectionResetError:
print(f"{addr} 强制断开连接")
finally:
client_socket.close()
print(f"{addr} 连接已关闭")

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)

print("多线程服务器启动,监听端口 8888")

while True:
client_socket, addr = server_socket.accept()
# 每个连接启动一个线程
thread = threading.Thread(target=handle_client, args=(client_socket, addr))
thread.start()
print(f"当前活跃线程数: {threading.active_count() - 1}")
`

多线程版本最大的坑是线程安全问题。如果多个线程同时修改共享数据(比如全局计数器),必须加锁。我踩过一次:没有加锁,结果两个客户端同时发送消息,计数器的值乱掉了。

现在来写个简易聊天机器人,结合前面学的所有技巧:

`python
import socket
import threading
import random

简单的回复库

RESPONSES = [
"嗯,有意思,继续说",
"这个我知道,但你先说说你的看法",
"哈哈,你太有趣了",
"别急,让我想想...",
"真的吗?我不信",
]

class ChatBot:
def __init__(self, host='0.0.0.0', port=8888):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((host, port))
self.server.listen(5)
self.clients = {} # 保存所有客户端

def broadcast(self, message, sender_socket=None):
"""向所有客户端广播消息"""
for client in self.clients:
if client != sender_socket:
try:
client.send(message.encode('utf-8'))
except:
pass

def handle_client(self, client_socket, addr):
self.clients[client_socket] = addr
welcome_msg = f"欢迎 {addr} 加入聊天室!"
self.broadcast(welcome_msg)

try:
while True:
data = client_socket.recv(1024)
if not data:
break

message = data.decode('utf-8')
print(f"[{addr}] {message}")

# 处理特殊命令
if message.strip() == '/help':
response = "可用命令: /help, /quit, /list"
client_socket.send(response.encode('utf-8'))
elif message.strip() == '/list':
users = [str(addr) for addr in self.clients.values()]
response = f"在线用户: {', '.join(users)}"
client_socket.send(response.encode('utf-8'))
else:
# 随机回复
reply = random.choice(RESPONSES)
client_socket.send(reply.encode('utf-8'))

except ConnectionResetError:
pass
finally:
del self.clients[client_socket]
client_socket.close()
leave_msg = f"{addr} 离开了聊天室"
self.broadcast(leave_msg)

def start(self):
print("聊天机器人启动,监听端口 8888")
while True:
client_socket, addr = self.server.accept()
thread = threading.Thread(target=self.handle_client, args=(client_socket, addr))
thread.start()

if __name__ == '__main__':
bot = ChatBot()
bot.start()
`

这个聊天机器人支持多客户端同时连接,有广播功能,还有简单的命令系统。但还有几个问题可以改进:比如没有用户昵称、消息没有时间戳、不能持久化聊天记录。

总结一下,你可以立刻用的三个点:

  • 端口复用必须写socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)` 这句一定在bind前调用,否则开发时频繁重启会报错
  • 多线程处理连接:不要在主线程里用recv()阻塞,用多线程或者select来处理多个客户端,否则一个客户端卡住整个服务就挂了
  • 异常处理不能少:网络编程里ConnectionResetError和BrokenPipeError几乎一定会遇到,必须用try-except包住recv()和send()
  • 最后提醒一句:实际生产环境不要直接用socket写服务器,用asyncio、Twisted或者WebSocket框架更稳。但理解socket原理就像学开车先学挂挡一样,是基本功。

    滚动至顶部