- 服务器端:负责接收客户端连接,并将一个客户端发送的消息广播给所有其他客户端。
- 客户端:负责连接到服务器,发送用户输入的消息,并接收来自服务器的其他消息。
服务器端代码 (server.py)
这个脚本会监听来自客户端的连接,并为每个连接创建一个新的线程来处理。
# server.py
import socket
import threading
# 定义服务器的地址和端口
HOST = '127.0.0.1' # 本地主机地址
PORT = 12345 # 任意非特权端口
# 创建一个 socket 对象
# AF_INET 表示使用 IPv4 地址
# SOCK_STREAM 表示使用 TCP 协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
# 开始监听传入的连接
# backlog 参数指定了可以挂起的最大连接数
server.listen()
print(f"服务器正在监听 {HOST}:{PORT}...")
# 用于存储所有已连接的客户端
clients = []
# 用于存储所有客户端的用户名
usernames = []
# 广播消息给所有客户端
def broadcast(message):
for client in clients:
client.send(message)
# 处理单个客户端连接的函数
def handle_client(client):
# 获取客户端的用户名
username = client.recv(1024).decode('utf-8')
usernames.append(username)
clients.append(client)
# 广播新用户加入的消息
broadcast(f"{username} 已加入聊天室!\n".encode('utf-8'))
print(f"{username} 已连接。")
while True:
try:
# 接收来自客户端的消息
message = client.recv(1024)
if not message:
break
# 广播消息给其他客户端
broadcast(message)
except:
# 如果发生错误(例如客户端断开连接),则移除该客户端
index = clients.index(client)
clients.remove(client)
username = usernames[index]
usernames.pop(index)
broadcast(f"{username} 已离开聊天室,\n".encode('utf-8'))
print(f"{username} 已断开连接。")
break
# 关闭客户端连接
client.close()
# 主循环,用于接受新的客户端连接
while True:
# 接受新的客户端连接
client, address = server.accept()
print(f"已接受来自 {address} 的连接。")
# 为每个新客户端创建一个新线程
thread = threading.Thread(target=handle_client, args=(client,))
thread.start()
客户端代码 (client.py)
这个脚本允许用户连接到服务器,通过控制台输入和接收消息。
# client.py
import socket
import threading
import sys
# 定义服务器的地址和端口 (必须与服务器端一致)
HOST = '127.0.0.1'
PORT = 12345
def receive_messages(client_socket):
"""持续接收来自服务器的消息的线程函数"""
while True:
try:
message = client_socket.recv(1024).decode('utf-8')
if not message:
break
# 打印消息,如果消息是系统通知(如用户加入/离开),可以特殊处理
if message.endswith(" 已加入聊天室!\n") or message.endswith(" 已离开聊天室,\n"):
print(f"\n{message.strip()}", end="> ")
else:
# 避免打印自己的输入
if not message.startswith("你: "):
print(f"\n{message.strip()}", end="> ")
except:
print("连接已丢失。")
break
client_socket.close()
sys.exit() # 退出程序
def send_messages(client_socket):
"""处理用户输入并发送消息的函数"""
# 首先要求用户输入用户名
username = input("请输入您的用户名: ")
client_socket.send(username.encode('utf-8'))
while True:
message = input("> ")
if message.lower() == 'exit':
client_socket.send("用户已退出。".encode('utf-8'))
break
full_message = f"你: {message}"
client_socket.send(full_message.encode('utf-8'))
# 创建一个 socket 对象
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 连接到服务器
client.connect((HOST, PORT))
print(f"已连接到服务器 {HOST}:{PORT}")
# 创建并启动接收消息的线程
receive_thread = threading.Thread(target=receive_messages, args=(client,))
receive_thread.daemon = True # 设置为守护线程,这样主线程退出时它也会退出
receive_thread.start()
# 在主线程中发送消息
send_messages(client)
except ConnectionRefusedError:
print("无法连接到服务器,请确保服务器正在运行。")
except Exception as e:
print(f"发生错误: {e}")
finally:
client.close()
print("客户端已关闭。")
如何运行
-
保存文件:将上面的两段代码分别保存为
server.py和client.py,放在同一个文件夹下。 -
启动服务器:
- 打开一个终端(或命令提示符)。
- 使用
cd命令切换到你保存文件的目录。 - 运行服务器脚本:
python server.py
- 你会看到输出:
服务器正在监听 127.0.0.1:12345...,表示服务器已启动并等待连接。
-
启动客户端:
- 打开另一个新的终端(非常重要,不要在同一个终端里运行)。
- 同样,切换到文件所在的目录。
- 运行客户端脚本:
python client.py
- 客户端会提示你输入用户名,输入后按回车。
-
启动更多客户端:
- 重复步骤 3,打开第三个、第四个...终端,运行
python client.py。 - 每当新客户端加入时,所有已连接的客户端都会收到“XXX 已加入聊天室!”的通知。
- 当一个客户端输入消息并按回车时,其他所有客户端都会看到该消息。
- 当一个客户端输入
exit并按回车时,它会断开连接,其他客户端会收到“XXX 已离开聊天室。”的通知。
- 重复步骤 3,打开第三个、第四个...终端,运行
代码解析
服务器端 (server.py)
socket.socket()和bind(): 创建网络套接字并将其绑定到指定的 IP 地址和端口。listen(): 使服务器进入监听模式,准备接受客户端连接。clients和usernames列表: 全局列表,用于跟踪所有连接的客户端套接字和它们对应的用户名。broadcast(message): 一个辅助函数,遍历clients列表,将消息发送给每一个客户端。handle_client(client): 这是核心的处理函数,它在一个独立的线程中运行,专门为一个客户端服务。- 首先接收并存储客户端的用户名。
- 使用
while True循环持续接收该客户端的消息。 client.recv(1024): 从套接字读取最多 1024 字节的数据。recv()返回空数据,表示客户端已正常断开连接。- 如果发生异常(如网络错误),表示客户端异常断开,此时会从列表中移除该客户端并广播其离开的消息。
- 主
while True循环: 持续调用server.accept()来等待新的客户端连接,每当有新客户端连接,就为它创建一个新的handle_client线程,这样服务器就可以同时处理多个客户端而不会阻塞。
客户端 (client.py)
receive_messages(client_socket): 在一个独立的线程中运行。- 它的唯一任务就是不停地接收来自服务器的消息并打印到控制台。
- 这样做可以防止在等待服务器消息时,用户无法输入新消息。
send_messages(client_socket): 在主线程中运行。- 它首先让用户输入用户名,并发送给服务器。
- 它进入一个循环,使用
input()获取用户输入,并将消息(加上“你: ”前缀)发送给服务器。 - 如果用户输入
exit,则关闭连接并退出。
threading.Thread: 客户端使用两个线程,一个用于接收,一个用于发送,这使得客户端可以同时进行I/O操作,实现了全双工通信,接收线程被设置为守护线程 (daemon=True),这样当主线程(发送线程)结束时,程序会自动退出,确保资源被释放。
这个例子非常经典,是学习网络编程和多线程的绝佳入门项目,你可以基于此进行扩展,例如添加私聊功能、文件传输、图形用户界面等。
