实验 2 TCP群聊系统实现
一、实验目的和任务
- 掌握TCP Socket编程的基本原理与实现方法
- 理解多线程在网络编程中的应用
- 实现基于TCP协议的群聊系统
- 学习GUI与网络编程的结合应用
二、实验内容
1.服务器端程序开发:实现用户连接管理、消息广播功能、私聊消息处理、在线用户列表维护
2.客户端程序开发:用户界面设计、消息发送与接收、私聊功能实现、在线用户列表显示
3.系统测试:多客户端连接测试、消息收发测试、私聊功能测试
三、实验步骤
实验 2.1 服务器端实现
代码参考:
python
mport socket
import threading
import time
class ChatServer:
def __init__(self, host='localhost', port=12345):
self.host = host
self.port = port
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clients = []
self.nicknames = []
def start(self):
self.server.bind((self.host, self.port))
self.server.listen()
print(f"服务器已启动,监听 {self.host}:{self.port}")
# 在start方法中启动用户列表广播线程
userlist_thread = threading.Thread(target=self.broadcast_userlist)
userlist_thread.daemon = True
userlist_thread.start()
持续接收用户连接:
while True:
client, address = self.server.accept()
print(f"新连接来自: {str(address)}")
# 要求客户端发送昵称
client.send('NICK'.encode('utf-8'))
nickname = client.recv(1024).decode('utf-8')
self.nicknames.append(nickname)
self.clients.append(client)
print(f"昵称是: {nickname}")
self.broadcast(f"{nickname} 加入了聊天室!".encode('utf-8'))
# 为每个客户端启动线程
thread = threading.Thread(target=self.handle_client, args=(client,))
thread.start()
广播用户列表:
def broadcast_userlist(self):
while True:
try:
userlist = "USERLIST:" + ",".join(self.nicknames)
self.broadcast(userlist.encode('utf-8'))
time.sleep(5) # 每5秒更新一次
except:
break
广播信息:
def broadcast(self, message):
for client in self.clients:
try:
client.send(message)
except:
# 移除断开连接的客户端
index = self.clients.index(client)
self.clients.remove(client)
client.close()
nickname = self.nicknames[index]
self.broadcast(f"{nickname} 离开了聊天室!".encode('utf-8'))
self.nicknames.remove(nickname)
处理客户线程的具体函数,负责接收消息:
def handle_client(self, client):
while True:
try:
message = client.recv(1024).decode('utf-8')
# 处理私聊消息
if message.startswith("/pm"):
parts = message.split(" ", 2) # 格式: /pm 目标昵称 消息内容
if len(parts) == 3:
target_nick = parts[1]
if target_nick in self.nicknames:
target_index = self.nicknames.index(target_nick)
private_msg = f"[私聊] {self.nicknames[self.clients.index(client)]}: {parts[2]}"
self.clients[target_index].send(private_msg.encode('utf-8'))
# 给发送者回显
client.send(f"[私聊] 你 -> {target_nick}: {parts[2]}".encode('utf-8'))
continue # 私聊消息不广播
# 普通消息广播
self.broadcast(message.encode('utf-8'))
except:
# 移除断开连接的客户端
index = self.clients.index(client)
self.clients.remove(client)
client.close()
nickname = self.nicknames[index]
self.broadcast(f"{nickname} 离开了聊天室!".encode('utf-8'))
self.nicknames.remove(nickname)
break
完整python脚本如下:
python
import socket
import threading
import time
class ChatServer:
def __init__(self, host='localhost', port=12345):
self.host = host
self.port = port
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.clients = []
self.nicknames = []
def start(self):
self.server.bind((self.host, self.port))
self.server.listen()
# 获取本机IP地址
hostname = socket.gethostname()
ip_address = socket.gethostbyname(hostname)
print(f"服务器已启动,监听 {ip_address}:{self.port}")
print(f"本地地址: {self.host}:{self.port}")
print("\n客户端连接说明:")
print("1. 打开新的终端窗口")
print("2. 运行命令: python 1220911101-fyt-2-client.py")
print("3. 在客户端界面输入服务器地址: {ip_address}")
print("4. 输入端口号: {self.port}")
print("5. 输入您的昵称即可开始聊天")
print("\n提示: 可以同时运行多个客户端程序进行测试")
# 在start方法中启动用户列表广播线程
userlist_thread = threading.Thread(target=self.broadcast_userlist)
userlist_thread.daemon = True
userlist_thread.start()
while True:
client, address = self.server.accept()
print(f"新连接来自: {str(address)}")
# 要求客户端发送昵称
client.send('NICK'.encode('utf-8'))
nickname = client.recv(1024).decode('utf-8')
self.nicknames.append(nickname)
self.clients.append(client)
print(f"昵称是: {nickname}")
self.broadcast(f"{nickname} 加入了聊天室!".encode('utf-8'))
# 为每个客户端启动线程
thread = threading.Thread(target=self.handle_client, args=(client,))
thread.start()
def broadcast_userlist(self):
while True:
try:
userlist = "USERLIST:" + ",".join(self.nicknames)
self.broadcast(userlist.encode('utf-8'))
time.sleep(5) # 每5秒更新一次
except:
break
def broadcast(self, message):
for client in self.clients:
try:
client.send(message)
except:
# 移除断开连接的客户端
index = self.clients.index(client)
self.clients.remove(client)
client.close()
nickname = self.nicknames[index]
self.broadcast(f"{nickname} 离开了聊天室!".encode('utf-8'))
self.nicknames.remove(nickname)
def handle_client(self, client):
while True:
try:
message = client.recv(1024).decode('utf-8')
# 处理私聊消息
if message.startswith("/pm"):
parts = message.split(" ", 2) # 格式: /pm 目标昵称 消息内容
if len(parts) == 3:
target_nick = parts[1]
if target_nick in self.nicknames:
target_index = self.nicknames.index(target_nick)
private_msg = f"[私聊] {self.nicknames[self.clients.index(client)]}: {parts[2]}"
self.clients[target_index].send(private_msg.encode('utf-8'))
# 给发送者回显
client.send(f"[私聊] 你 -> {target_nick}: {parts[2]}".encode('utf-8'))
continue # 私聊消息不广播
# 普通消息广播
self.broadcast(message.encode('utf-8'))
except:
# 移除断开连接的客户端
index = self.clients.index(client)
self.clients.remove(client)
client.close()
nickname = self.nicknames[index]
self.broadcast(f"{nickname} 离开了聊天室!".encode('utf-8'))
self.nicknames.remove(nickname)
break
if __name__ == "__main__":
server = ChatServer()
server.start()
功能效果如下图所示:

实验2 .2 客户端实现
核心代码参考
python
class ChatClient:
def __init__(self, host='localhost', port=12345):
# 初始化
self.host = host
self.port = port
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.nickname = ""
# 创建GUI
self.root = Tk()
self.root.title("群聊客户端")
# 聊天显示区域
self.chat_area = scrolledtext.ScrolledText(self.root, wrap=WORD, width=50, height=20)
self.chat_area.pack(padx=10, pady=10)
self.chat_area.config(state=DISABLED)
# 消息输入区域
self.msg_entry = Entry(self.root, width=40)
self.msg_entry.pack(padx=10, pady=5)
self.msg_entry.bind("<Return>", self.send_message)
# 发送按钮
self.send_btn = Button(self.root, text="发送", command=self.send_message)
self.send_btn.pack(pady=5)
# 在__init__方法中添加帮助标签
self.help_label = Label(self.root, text="私聊格式: /pm 昵称 消息", fg="gray")
self.help_label.pack()
# 用户列表框架
self.user_frame = Frame(self.root)
self.user_frame.pack(side=RIGHT, padx=10, pady=10)
self.user_label = Label(self.user_frame, text="在线用户")
self.user_label.pack()
self.user_list = Listbox(self.user_frame, width=20, height=15)
self.user_list.pack()
# 双击用户列表发起私聊
self.user_list.bind("<Double-Button-1>", self.start_private_chat)
启动连接方法,在初始化完成后调用。
def connect(self):
try:
self.client.connect((self.host, self.port))
# 获取昵称
self.nickname = simpledialog.askstring("昵称", "请输入你的昵称:")
if not self.nickname:
self.nickname = "匿名用户"
self.client.send(self.nickname.encode('utf-8'))
# 启动接收消息的线程
receive_thread = threading.Thread(target=self.receive)
receive_thread.daemon = True
receive_thread.start()
self.root.mainloop()
except Exception as e:
messagebox.showerror("错误", f"无法连接到服务器: {e}")
self.root.destroy()
接收消息的具体函数:
def receive(self):
while True:
try:
message = self.client.recv(1024).decode('utf-8')
if message.startswith("USERLIST:"):
# 更新用户列表
users = message[len("USERLIST:"):].split(",")
self.user_list.delete(0, END)
for user in users:
if user and user != self.nickname: # 不显示自己
self.user_list.insert(END, user)
else: #普通消息
self.display_message(message)
except:
self.display_message("与服务器的连接已断开!")
self.client.close()
break
进行私聊的方法:
def start_private_chat(self, event):
selected = self.user_list.get(self.user_list.curselection())
self.msg_entry.delete(0, END)
self.msg_entry.insert(0, f"/pm {selected} ")
self.msg_entry.focus()
在聊天框中显示信息:
def display_message(self, message):
self.chat_area.config(state=NORMAL)
self.chat_area.insert(END, message + "\n")
self.chat_area.config(state=DISABLED)
self.chat_area.see(END)
向服务端发送消息:
def send_message(self, event=None):
message = self.msg_entry.get()
if message:
# 检查是否是私聊命令
if message.startswith("/pm"):
# 直接发送原始命令到服务器
full_message = message
else:
full_message = f"{self.nickname}: {message}"
try:
self.client.send(full_message.encode('utf-8'))
self.msg_entry.delete(0, END)
except:
self.display_message("发送失败,请检查连接!")
完整python脚本如下:
python
import socket
import threading
from tkinter import *
from tkinter import scrolledtext, messagebox, simpledialog
class ChatClient:
def __init__(self, host='localhost', port=12345):
# 初始化
self.host = host
self.port = port
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.nickname = ""
# 创建GUI
self.root = Tk()
self.root.title("TCP群聊客户端")
self.root.geometry("800x600")
self.root.minsize(600, 400)
# 主框架
main_frame = Frame(self.root)
main_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# 用户列表框架
user_frame = Frame(main_frame, width=150, relief=RAISED, borderwidth=1)
user_frame.pack(side=RIGHT, fill=Y, padx=(10,0))
Label(user_frame, text="在线用户", font=('Arial', 10, 'bold')).pack(pady=5)
self.user_list = Listbox(user_frame, selectmode=SINGLE)
self.user_list.pack(fill=BOTH, expand=True, padx=5, pady=5)
self.user_list.bind("<Double-Button-1>", self.start_private_chat)
# 聊天区域框架
chat_frame = Frame(main_frame)
chat_frame.pack(side=LEFT, fill=BOTH, expand=True)
# 聊天显示区域
self.chat_area = scrolledtext.ScrolledText(
chat_frame, wrap=WORD,
font=('Arial', 10),
padx=5, pady=5
)
self.chat_area.pack(fill=BOTH, expand=True)
self.chat_area.config(state=DISABLED)
# 输入区域框架
input_frame = Frame(chat_frame)
input_frame.pack(fill=X, pady=(10,0))
# 消息输入区域
self.msg_entry = Entry(
input_frame,
font=('Arial', 10),
relief=SOLID
)
self.msg_entry.pack(side=LEFT, fill=X, expand=True, padx=(0,5))
self.msg_entry.bind("<Return>", self.send_message)
# 发送按钮
self.send_btn = Button(
input_frame,
text="发送",
command=self.send_message,
width=8,
relief=RAISED
)
self.send_btn.pack(side=RIGHT)
# 帮助标签
help_frame = Frame(chat_frame)
help_frame.pack(fill=X, pady=(5,0))
self.help_label = Label(
help_frame,
text="提示: 双击用户列表可发起私聊 | 私聊格式: /pm 昵称 消息",
fg="gray",
font=('Arial', 8)
)
self.help_label.pack(side=LEFT)
# 双击用户列表发起私聊
self.user_list.bind("<Double-Button-1>", self.start_private_chat)
def connect(self):
try:
# 获取服务器地址和端口
server_ip = simpledialog.askstring(
"服务器地址",
"请输入服务器IP地址(如:192.168.1.100):",
initialvalue=self.host
)
if not server_ip:
server_ip = self.host
port_str = simpledialog.askstring(
"端口号",
"请输入服务器端口号(默认12345):",
initialvalue=str(self.port)
)
try:
port = int(port_str) if port_str else self.port
except ValueError:
messagebox.showwarning("警告", "端口号必须是数字,将使用默认端口12345")
port = self.port
# 测试连接
self.client.settimeout(3) # 设置3秒超时
self.client.connect((server_ip, port))
self.client.settimeout(None) # 连接成功后取消超时
# 获取昵称
self.nickname = simpledialog.askstring("昵称", "请输入你的昵称:")
if not self.nickname:
self.nickname = "匿名用户"
self.client.send(self.nickname.encode('utf-8'))
# 启动接收消息的线程
receive_thread = threading.Thread(target=self.receive)
receive_thread.daemon = True
receive_thread.start()
self.root.mainloop()
except Exception as e:
messagebox.showerror("错误", f"无法连接到服务器: {e}")
self.root.destroy()
def receive(self):
while True:
try:
message = self.client.recv(1024).decode('utf-8')
if message.startswith("USERLIST:"):
# 更新用户列表
users = message[len("USERLIST:"):].split(",")
self.user_list.delete(0, END)
for user in users:
if user and user != self.nickname: # 不显示自己
self.user_list.insert(END, user)
else: #普通消息
self.display_message(message)
except:
self.display_message("与服务器的连接已断开!")
self.client.close()
break
def start_private_chat(self, event):
selected = self.user_list.get(self.user_list.curselection())
self.msg_entry.delete(0, END)
self.msg_entry.insert(0, f"/pm {selected} ")
self.msg_entry.focus()
def display_message(self, message):
self.chat_area.config(state=NORMAL)
self.chat_area.insert(END, message + "\n")
self.chat_area.config(state=DISABLED)
self.chat_area.see(END)
def send_message(self, event=None):
message = self.msg_entry.get()
if message:
# 检查是否是私聊命令
if message.startswith("/pm"):
# 直接发送原始命令到服务器
full_message = message
else:
full_message = f"{self.nickname}: {message}"
try:
self.client.send(full_message.encode('utf-8'))
self.msg_entry.delete(0, END)
except:
self.display_message("发送失败,请检查连接!")
if __name__ == "__main__":
client = ChatClient()
client.connect()
功能效果如下图所示:

四、思考题
1.为什么服务器需要为每个客户端创建单独的线程?如果不使用多线程会有什么问题?
答:原因:服务器需要同时处理多个客户端的请求,每个客户端的操作(如发送消息、接收消息)可能是独立的,且可能阻塞(如等待输入)。多线程允许服务器并行处理多个客户端的请求,提高并发性和响应速度。
不使用多线程的问题:单线程模式下,服务器只能依次处理客户端的请求。如果一个客户端阻塞(如长时间未发送消息),其他客户端会被阻塞,导致系统无法及时响应。用户体验差,系统吞吐量低。
2.实验中如何处理客户端异常断开连接的情况?
答:处理方法:
心跳机制:定期检测客户端是否存活(如定时发送心跳包,超时未响应则判定为断开)。
异常捕获 :在服务器线程中捕获客户端连接异常(如 SocketException
),关闭对应的套接字和线程。
资源清理:从用户列表中移除断开连接的客户端,并通知其他用户更新列表。
3.私聊功能是如何实现的?服务器如何区分私聊消息和普通消息?
答:实现方式 :客户端发送私聊消息时,需指定目标用户(如格式为 @username:message)。
服务器解析消息内容,检测是否有私聊标识(如 @username)。
如果是私聊消息,服务器仅将消息转发给目标用户;否则广播给所有用户。
区分机制:通过消息格式或协议字段(如消息头中包含 type: private 或 target: username)区分。
4.用户列表更新采用了什么机制?为什么需要单独线程处理?
答:机制: 当用户加入或离开时,服务器更新用户列表,并通过广播通知所有客户端。
客户端接收更新后,刷新本地用户列表显示。
单独线程的原因: 用户列表更新可能频繁且独立于消息收发,单独线程可以避免阻塞主线程或其他客户端线程。
提高系统响应速度和稳定性。
5.如果要将本系统改为UDP实现,哪些部分需要修改?会遇到什么挑战?
答:协议设计: UDP是无连接的,需实现自定义的可靠性机制(如消息序号、确认重传)。
消息格 式:需添加字段标识消息类型、序号等。
客户端管理: UDP无连接状态,需维护客户端状态表(如IP和端口)。
挑战:
可靠性: UDP不保证交付,需处理丢包、乱序问题。
**状态管理:**需额外维护客户端连接状态(如超时检测)
6.本实验中的GUI界面使用了哪些组件?各自的作用是什么?
答:主要组件:
消息显示区(文本框) :显示聊天消息。
消息输入框: 用户输入消息内容。
发送按钮: 触发消息发送操作。
用户列表: 显示当前在线用户。
私聊选择框(可选): 选择私聊目标用户。
**作用:**提供用户交互界面,支持消息收发、用户列表更新等功能。
7.如何改进当前系统以实现消息历史记录功能?
答:改进方案:
服务器端存储: 将消息保存到数据库(如SQLite、MySQL)或文件(如日志文件)。
按时间或会话分类存储。
客户端加载: 客户端启动时,从服务器请求历史消息。支持按时间范围或关键词检索。
**优化:**采用分页加载,避免一次性加载过多数据。支持消息加密存储(如敏感信息)。