内容目录
作业一
1. 实验范围
基于UDP或TCP/IP协议的通讯小程序。
要求:
- 能够完成基本的客户与服务器的信息交互功能,按要求撰写实验报告【60分】(已完成)
- 多客户端交互【加分项】(已完成, 支持最大20个应用[可调整]连接)
- 历史数据存储【加分项】(已完成, 使用数据库)
- 使用数据库【加分项】(已完成, 使用sqlite轻量级数据库)
- 界面友好【加分项】(已完成, 使用tkinter构建美观的GUI界面)
- 测试分析【加分项】(已完成, 使用logging模块实现服务器日志打印, 进行分析)
- 使用云资源【加分项】(已完成, server部分部署到腾讯云centos服务器)
2. 需求分析
根据题意,系统需要完成的功能有:
- 创建服务器,以供用户聊天
- 提供用户登录
- 实现用户之间的聊天
- 使用数据库永久存储聊天信息
- 构建用户界面
- 能够记录日志
- 部署服务器
3.功能描述
- 用户登录
用户使用默认的服务器端口地址,输入昵称,接着点击‘登录’按钮,实现登录聊天系统。 - 用户聊天
用户点击‘用户列表’,然后点击发送的对象,此时聊天窗口的标题变为当前用户指向目标用户,此时输入信息后点击‘发送’按钮实现一对一聊天。 - 群发信息
用户点击‘用户列表’,然后点击‘群发’,输入信息后点击‘发送’按钮实现群发消息。
4. 设计与代码描述
本项目分为服务器端和客户端,采用TCP协议进行网络数据传输。服务端主要用于存放用户连接信息包括用户的ip地址和端口和用户信息,如下图所示

-
服务端
-
初始化地址,端口和日志模块 config.py
import logging import os IP = "0.0.0.0" PORT = 8888 logger = logging.getLogger("server") logger.setLevel(logging.INFO) rf_handler = logging.StreamHandler() rf_handler.setLevel(logging.DEBUG) rf_handler.setFormatter(logging.Formatter("%(asctime)s [%(name)s][%(levelname)s]: %(message)s")) f_handler = logging.FileHandler(f"{os.path.dirname(__file__)}/log/server.log", "w", encoding="utf-8") f_handler.setLevel(logging.INFO) f_handler.setFormatter(logging.Formatter("%(asctime)s [%(name)s][%(levelname)s]: %(message)s")) logger.addHandler(rf_handler) logger.addHandler(f_handler) -
使用peewee建立数据库连接 sql.py
import os from peewee import SqliteDatabase, Model, AutoField, CharField db = SqliteDatabase(f"{os.path.dirname(__file__)}/chat.db") # 创建数据库对象 class Chat(Model): id = AutoField(primary_key=True) from_user = CharField() to_user = CharField() message = CharField() class Meta: database = db db.connect() # 连接数据库 db.create_tables([Chat]) # 创建数据表 -
实现服务器 server.py
from copy import copy import queue import socket import threading import json import time from config import logger from sql import Chat que = queue.Queue() users = [] lock = threading.Lock() def onlines(): online = [] for i in range(len(users)): online.append(users[i][1]) return online class Server(threading.Thread): def __init__(self, ip, port): threading.Thread.__init__(self) self.addr = (ip, port) self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def run(self): self.s.bind(self.addr) self.s.listen(20) logger.info("服务器开始监听 %s" % self.addr[1]) threading.Thread(target=self.send_data).start() while True: conn, addr = self.s.accept() logger.info("新连接 %s" % str(addr)) threading.Thread(target=self.tcp_connect, args=(conn, addr)).start() self.s.close() def recv(self, data, addr): global que, lock lock.acquire() try: que.put((addr, data)) finally: lock.release() def del_user(self, conn, addr): a = 0 for i in users: if i[0] == conn: users.pop(a) d = onlines() self.recv(d, addr) logger.info(f"在线用户: {d}") break a += 1 def tcp_connect(self, conn, addr): real = conn.recv(1024).decode().strip() user = copy(real) index = 1 while True: for i in range(len(users)): if users[i][1] == user: logger.info("用户 %s 已存在" % user) user = real + str(index) index += 1 break else: if user == "null": user = f"{addr[0]}:{addr[1]}" users.append((conn, user, addr)) logger.info(f"用户 [{user}] 已加入") break d = onlines() self.recv(d, addr) self.initialize(conn, user) try: while True: data = conn.recv(1024).decode() self.recv(data, addr) conn.close() except ConnectionResetError: logger.info(f"用户 [{user}] 已退出") self.del_user(conn, addr) conn.close() def send_data(self): global que, users # que[0] = 用户地址, que[1] = 消息内容 # users[0] = 用户连接, users[1] = 用户名, users[2] = 用户地址 while True: if not que.empty(): data = "" message = que.get() if isinstance(message[1], str): for i in range(len(users)): if message[0] == users[i][2]: logger.info(f"用户 [{users[i][1]}] 发送消息: {message[1]}") data = message[1] messages = data.split(":;") Chat.create(from_user=messages[1], to_user=messages[2], message=messages[0]) break for i in range(len(users)): try: users[i][0].send(f"{data}\n".encode()) except ConnectionResetError: pass elif isinstance(message[1], list): data = json.dumps(message[1]) for i in range(len(users)): try: users[i][0].send(f"{data}\n".encode()) except ConnectionResetError: pass def initialize(self, conn, user): data = Chat.select().where((Chat.to_user == "[群发]") | (Chat.to_user == user)).order_by(Chat.id) for message in data: conn.send(f"{message.message}:;{message.from_user}:;{message.to_user}\n".encode()) -
主函数 main.py
import sys import time from config import IP, PORT, logger from server import Server if __name__ == "__main__": app = Server(IP, PORT) app.start() while True: time.sleep(1) if not app.is_alive(): logger.info("服务器已关闭") sys.exit(0)
-
初始化地址,端口和日志模块 config.py
-
客户端
-
基础配置
IP = "blog.fengqiwu.xyz" PORT = 8888 user = "" list_open = False users = [] chat = "[群发]" -
连接服务器
class Connection(socket): def __init__(self, ip, port, user): super().__init__(AF_INET, SOCK_STREAM) self.ip = ip self.port = port self.user = user self.connect((self.ip, self.port)) if self.user: self.send(self.user.encode()) else: self.send("null".encode()) def __del__(self): self.shutdown(2) -
登录界面
class LoginRoot(Tk): def __init__(self): global IP, PORT, user super().__init__() self.title("登录") self.geometry("270x110") self.resizable(False, False) self.addr = StringVar() self.addr.set(f"{IP}:{PORT}") self.user = StringVar() self.user.set(user) # 服务器地址 self.addr_label = Label(self, text="地址:端口") self.addr_label.place(x=20, y=10, width=100, height=20) self.addr_entry = Entry(self, width=80, textvariable=self.addr) self.addr_entry.place(x=120, y=10, width=130, height=20) # 用户名 self.user_label = Label(self, text="用户名") self.user_label.place(x=30, y=40, width=80, height=20) self.user_entry = Entry(self, width=80, textvariable=self.user) self.user_entry.place(x=120, y=40, width=130, height=20) self.bind("", self.login) # 回车绑定登录事件 self.button = Button(self, text="登录", command=self.login) self.button.place(x=100, y=70, width=70, height=30) def login(self, *args): global IP, PORT, user IP, PORT = self.addr_entry.get().split(":") PORT = int(PORT) user = self.user_entry.get() if not user: messagebox.showerror("提示", message="用户名不能为空") else: self.destroy() -
聊天窗口
class ChatRoot(Tk): def __init__(self, conn: socket): global user super().__init__() self.conn = conn self.title("聊天室: " + user) self.geometry("580x400") self.resizable(False, False) # 多行文本框 self.chat = ScrolledText(self) self.chat.place(x=5, y=0, width=570, height=320) # 配置字体颜色 self.chat.tag_config("red", foreground="red") self.chat.tag_config("blue", foreground="blue") self.chat.tag_config("green", foreground="green") self.chat.tag_config("pink", foreground="pink") self.chat.insert(END, "欢迎来到聊天室!", "blue") # 在线用户列表 self.online_list = Listbox(self) self.online_list.place(x=445, y=0, width=130, height=320) # 在线用户按钮 self.btn1 = Button(self, text="用户列表", command=self.show_users) self.btn1.place(x=485, y=320, width=90, height=30) # 输入框 self.context = StringVar() self.context.set("") self.entry = Entry(self, width=120, textvariable=self.context) self.entry.place(x=5, y=350, width=570, height=40) # 发送按钮 self.btn2 = Button(self, text="发送", command=self.send) self.btn2.place(x=515, y=353, width=60, height=30) self.online_list.bind("", self.private) threading.Thread(target=self.recv).start() def show_users(self): global list_open if list_open: self.online_list.place(x=445, y=0, width=130, height=320) list_open = False else: self.online_list.place_forget() # 隐藏 list_open = True def send(self, *args): global users, user users.append("[群发]") print(chat) if chat not in users: messagebox.showerror("提示", message="没有聊天对象") return elif chat == user: messagebox.showerror("提示", message="不能给自己发送消息") return mes = f"{self.entry.get()}:;{user}:;{chat}" self.conn.send(mes.encode()) self.context.set("") def private(self, *args): global chat indexs = self.online_list.curselection() index = indexs[0] if index > 0: chat = self.online_list.get(index) if chat == "[群发]": self.title(f"聊天室: {user}") return self.title(f"聊天室: {user} -> {chat}") def recv(self): global users, user while True: data = self.conn.recv(1024).decode() messages = data.split("\n") messages.pop() for m in messages: try: data = json.loads(m) users = data self.online_list.delete(0, END) number = "在线人数: " + str(len(data)) self.online_list.insert(END, number) self.online_list.itemconfig(END, fg="green", bg="#f0f0ff") self.online_list.insert(END, "[群发]") self.online_list.itemconfig(END, fg="green") for i in range(len(data)): self.online_list.insert(END, (data[i])) self.online_list.itemconfig(END, fg="green") except: data = m.split(":;") message = data[0].strip() username = data[1] obj = data[2] if obj == "[群发]": if username == user: self.chat.insert( END, f"\n[{username}] 发送: {message}", "blue" ) else: self.chat.insert( END, f"\n来自 [{username}]: {message}", "green" ) elif obj == user or username == user: if username == user: self.chat.insert(END, f"\n发送给 [{obj}]: {message}", "red") else: self.chat.insert( END, f"\n来自[{username}]: {message}", "purple" ) self.chat.see(END) -
主逻辑
class Client: def __init__(self): global IP, PORT, user self.login_tk = LoginRoot() self.login_tk.protocol("WM_DELETE_WINDOW", self.close) self.login_tk.mainloop() self.conn = Connection(IP, PORT, user) self.chat_tk = ChatRoot(self.conn) self.chat_tk.protocol("WM_DELETE_WINDOW", self.close) self.chat_tk.mainloop() def close(self): if hasattr(self, "conn"): del self.conn if hasattr(self, "chat_tk"): self.chat_tk.destroy() os._exit(0) -
主函数
from client import Client if __name__ == "__main__": client = Client()
-
基础配置
未完待续








Comments | NOTHING