作业一

1. 实验范围

基于UDP或TCP/IP协议的通讯小程序。

要求:

  1. 能够完成基本的客户与服务器的信息交互功能,按要求撰写实验报告【60分】(已完成)
  2. 多客户端交互【加分项】(已完成, 支持最大20个应用[可调整]连接)
  3. 历史数据存储【加分项】(已完成, 使用数据库)
  4. 使用数据库【加分项】(已完成, 使用sqlite轻量级数据库)
  5. 界面友好【加分项】(已完成, 使用tkinter构建美观的GUI界面)
  6. 测试分析【加分项】(已完成, 使用logging模块实现服务器日志打印, 进行分析)
  7. 使用云资源【加分项】(已完成, server部分部署到腾讯云centos服务器)

2. 需求分析

根据题意,系统需要完成的功能有:

  • 创建服务器,以供用户聊天
  • 提供用户登录
  • 实现用户之间的聊天
  • 使用数据库永久存储聊天信息
  • 构建用户界面
  • 能够记录日志
  • 部署服务器

3.功能描述

  1. 用户登录
    用户使用默认的服务器端口地址,输入昵称,接着点击‘登录’按钮,实现登录聊天系统。
  2. 用户聊天
    用户点击‘用户列表’,然后点击发送的对象,此时聊天窗口的标题变为当前用户指向目标用户,此时输入信息后点击‘发送’按钮实现一对一聊天。
  3. 群发信息
    用户点击‘用户列表’,然后点击‘群发’,输入信息后点击‘发送’按钮实现群发消息。

4. 设计与代码描述

本项目分为服务器端和客户端,采用TCP协议进行网络数据传输。服务端主要用于存放用户连接信息包括用户的ip地址和端口和用户信息,如下图所示

  • 服务端

    1. 初始化地址,端口和日志模块 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)
              
    2. 使用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])  # 创建数据表
              
    3. 实现服务器 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())
              
    4. 主函数 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)
              
  • 客户端

    1. 基础配置
      
      IP = "blog.fengqiwu.xyz"
      PORT = 8888
      user = ""
      list_open = False
      users = []
      chat = "[群发]"
              
    2. 连接服务器
      
      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)
              
    3. 登录界面
      
      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()
              
    4. 聊天窗口
      
      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)
              
    5. 主逻辑
      
      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)
              
    6. 主函数
      
      from client import Client
      
      if __name__ == "__main__":
          client = Client()
              

未完待续


这里是凤栖梧,一颗平平凡凡的木头