实验2 python的TCP群聊系统实现

实验 2 TCP群聊系统实现

一、实验目的和任务

  1. 掌握TCP Socket编程的基本原理与实现方法
  2. 理解多线程在网络编程中的应用
  3. 实现基于TCP协议的群聊系统
  4. 学习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)或文件(如日志文件)。

按时间或会话分类存储。
客户端加载: 客户端启动时,从服务器请求历史消息。支持按时间范围或关键词检索。
**优化:**采用分页加载,避免一次性加载过多数据。支持消息加密存储(如敏感信息)。

相关推荐
终身学习基地10 分钟前
第七篇:linux之基本权限、进程管理、系统服务
linux·运维·服务器
安顾里15 分钟前
LInux平均负载
linux·服务器·php
unlockjy25 分钟前
Linux——进程优先级/切换/调度
linux·运维·服务器
铭阳(●´∇`●)33 分钟前
Python内置函数---breakpoint()
笔记·python·学习
zhanghongyi_cpp36 分钟前
python基础语法测试
python
MurphyStar39 分钟前
UV: Python包和项目管理器(从入门到不放弃教程)
开发语言·python·uv
linux kernel42 分钟前
Python基础语法3
python
成工小白1 小时前
【Linux】详细介绍进程的概念
linux·运维·服务器
种时光的人1 小时前
多线程出bug不知道如何调试?java线程几种常见状态
java·python·bug
wayuncn1 小时前
双卡 4090 服务器租用:释放强算力的新选择
运维·服务器