基于 Python Socket 的群聊聊天室(带图形界面,包含注册、登录、数据入库功能)
代码下载
https://github.com/juno3550/GroupChatRoom
实现框架
Chat 包:
- server.py:服务器端执行代码(TCP 服务器,根据客户端消息调用 mode 包的注册、登录、聊天功能)
- client.py:客户端执行代码(连接服务器端,进行注册、登录、聊天)
- client_draw.py:客户端图形界面绘制
mode 包:
- chat_mode.py:封装服务器端的聊天逻辑
- login_mode.py:封装服务器端的登录逻辑
- register_mode.py:封装服务器端的注册逻辑
db 包:
- user_info_util.py:基于 mysql_util 查询或新增用户信息
- mysql_util.py:封装 mysql 基础操作
实现效果
示例:执行一个 server.py 与多个 client.py
代码实现
Chat 包
server.py
1 from twisted.internet.protocol import Factory 2 from twisted.protocols.basic import LineReceiver # 事件处理器 3 from twisted.internet import reactor 4 import json 5 from mode import chat_mode, login_mode, register_mode 6 7 8 # 每一个客户端连接都会对应一个不同的Chat对象 9 class Chat(LineReceiver): 10 11 def __init__(self, users): 12 # 存储所有连接用户信息的字典 13 self.users = users 14 15 # 断开连接时候自动触发,从users字典去掉连接对象 16 def connectionLost(self, reason): 17 if self in self.users.keys(): 18 # print("%s断开连接" %self.users[self]) 19 del self.users[self] 20 21 # 对客户端的请求内容做处理,只要收到客户端消息,自动触发此方法 22 def dataReceived(self, data): 23 # 将字节数据解码成字符串 24 data = data.decode('utf-8') 25 data_dict = json.loads(data) 26 # 根据type字段的值,进入对应的逻辑 27 # 登录逻辑 28 if data_dict["type"] == "login": 29 login_mode.login(self, data_dict) 30 # 注册逻辑 31 elif data_dict["type"] == "register": 32 register_mode.register(self, data_dict) 33 # 聊天逻辑 34 elif data_dict["type"] == "chat": 35 chat_mode.chat(self, data_dict) 36 37 38 # 处理业务的工厂类,只会实例化一次 39 class ChatFactory(Factory): 40 41 def __init__(self): 42 # 有多个连接的时候,会有多个chat对象 43 # self.users 在内存地址中,只有一份,所有连接对象都只使用同一个实例变量 self.users(等价于一个全局变量) 44 self.users = {} 45 # key: 连接对象本身;value:登录成功的用户昵称 46 47 # 一个客户端连接会实例化一个新的Chat对象 48 def buildProtocol(self, addr): 49 print(type(addr), addr) 50 # 返回一个处理具体业务请求的对象,参数传递了字典(存有所有连接对象) 51 return Chat(self.users) 52 53 54 if __name__ == '__main__': 55 # 设定监听端口和对象 56 # 使用Tcp协议,实例化ChatFactory 57 reactor.listenTCP(1200, ChatFactory()) 58 59 print ("开始进入监听状态...") 60 reactor.run() # 开始监听
client.py
1 import tkinter 2 from tkinter import messagebox 3 import json 4 import time 5 import threading 6 import select 7 from socket import * 8 import traceback 9 from chat import client_draw 10 11 12 class Client: 13 14 # 配置连接 15 def connect(self): 16 # 创建socket 17 self.s = socket(AF_INET, SOCK_STREAM) 18 # 服务器端和客户端均在同个机器上运行 19 remote_host = gethostname() 20 # 设置端口号 21 port = 1200 22 # 发起连接 23 self.s.connect((remote_host, port)) 24 print("从%s成功连接到%s" % (self.s.getsockname(), self.s.getpeername())) 25 return self.s 26 27 # 监听(接收)消息 28 def receive(self, s): 29 # 需要监控的对象列表 30 self.my = [s] 31 while 1: 32 print("监听中...") 33 34 # 实参: 35 # 第1个实参 self.my:可读的对象,监听服务器端的响应消息 36 # 第2个实参:可写的对象(本例不用) 37 # 第3个实参:出现异常的对象(本例不用) 38 # 这三个参数内容都是被操作系统监控的,即select.select()会执行系统内核代码 39 # 1)当有事件发生时,立马往下执行代码;否则阻塞监控10秒 40 # 2)若监控10秒了仍无事件发生,才往下执行 41 rl, wl, error = select.select(self.my, [], [], 10) 42 # 返回值: 43 # rl:监听某个文件描述符是否发生了读的事件(server给client发了数据) 44 # rl列表一开始为空,只有当s发生事件了(如客户端与服务器端建立了连接),才会将s加到rl中 45 # wl:监听某个文件描述符是否发生了写的事件(如client给server发了数据) 46 # error:监听某个文件描述符是否发生了异常事件 47 # 如果发生事件的对象是客户端连接对象,则代表收到服务器端数据 48 if s in rl: 49 try: 50 data = s.recv(1024).decode("utf-8") 51 data_dict = json.loads(data) 52 # 根据服务器端返回的type值,执行不同逻辑 53 type = data_dict["type"] 54 # 登录逻辑 55 if type == "login": 56 # 登录成功,跳转聊天页面 57 if "000" == data_dict["code"]: 58 nickname = data_dict["nickname"] 59 self.chat_interface(nickname) 60 # 登录失败,获取失败信息 61 else: 62 messagebox.showinfo(title="登录提示", message=data_dict["msg"]) 63 # 注册逻辑 64 elif type == "register": 65 # 注册成功,跳转聊天页面 66 if "000" == data_dict["code"]: 67 nickname = data_dict["nickname"] 68 messagebox.showinfo(title="进入聊天室", message=data_dict["msg"]) 69 self.chat_interface(nickname) 70 # 注册失败 71 else: 72 messagebox.showinfo(title="注册提示", message=data_dict["msg"]) 73 # 聊天逻辑 74 elif type == "chat": 75 message = data_dict["message"] 76 nickname = data_dict["nickname"] 77 isMy = data_dict["isMy"] 78 chat_time = " " + nickname + "\t" + time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()) + "\n" 79 # 聊天页面,显示发送人及发送时间 80 self.txtMsgList.insert(tkinter.END, chat_time, "DimGray") 81 # 如果是自己发的消息,字体使用'DarkTurquoise' 82 if "yes" == isMy: 83 self.txtMsgList.insert(tkinter.END, " " + message + "\n\n", 'DarkTurquoise') 84 # 如果是别人发的消息,字体使用'Black' 85 else: 86 self.txtMsgList.insert(tkinter.END, " " + message + "\n\n", 'Black') 87 # 插入消息时,自动滚动到底部 88 self.txtMsgList.see(tkinter.END) 89 except (ConnectionAbortedError, ConnectionResetError): 90 # 将连接对象从监听列表去掉 91 self.my.remove(s) 92 print("客户端发生连接异常,与服务器端断开连接") 93 traceback.print_exc() 94 s.close() 95 except Exception as e: 96 print("客户端发生了其它异常: ") 97 traceback.print_exc() 98 s.close() 99 100 # 进入注册页面 101 def register_interface(self): 102 client_draw.draw_register(self) 103 104 # 进入聊天页面 105 def chat_interface(self, nickname): 106 client_draw.draw_chat(self, nickname) 107 108 # 返回登录页面 109 def return_login_interface(self): 110 # 将不需要的控件先销毁 111 self.label_nickname.destroy() 112 self.input_nickname.destroy() 113 self.label_password.destroy() 114 self.input_password.destroy() 115 client_draw.draw_login(self) 116 117 # 获取输入框内容,进行注册验证 118 def verify_register(self): 119 username = self.input_account.get() 120 password = self.input_password.get() 121 nickname = self.input_nickname.get() 122 try: 123 register_data = {} 124 register_data["type"] = "register" 125 register_data["username"] = username 126 register_data["password"] = password 127 register_data["nickname"] = nickname 128 # 将dict类型转为json字符串,便于网络传输 129 data = json.dumps(register_data) 130 self.s.send(data.encode("utf-8")) 131 except: 132 traceback.print_exc() 133 134 # 获取输入框内容,进行登录校验 135 def verify_login(self): 136 account = self.input_account.get() 137 password = self.input_password.get() 138 try: 139 login_data = {} 140 login_data["type"] = "login" 141 login_data["username"] = account 142 login_data["password"] = password 143 data = json.dumps(login_data) 144 self.s.send(data.encode('utf-8')) 145 except: 146 traceback.print_exc() 147 148 # 获取输入框内容,发送消息 149 def send_msg(self): 150 message = self.txtMsg.get('0.0', tkinter.END).strip() 151 if not message: 152 messagebox.showinfo(title='发送提示', message="发送内容不能为空,请重新输入") 153 return 154 self.txtMsg.delete('0.0', tkinter.END) 155 try: 156 chat_data = {} 157 chat_data["type"] = "chat" 158 chat_data["message"] = message 159 data = json.dumps(chat_data) 160 self.s.send(data.encode('utf-8')) 161 except: 162 traceback.print_exc() 163 164 # 发送消息事件 165 def send_msg_event(self, event): 166 # 如果捕捉到键盘的回车按键,触发消息发送 167 if event.keysym == 'Return': 168 self.send_msg() 169 170 # 聊天页面,点击右上角退出时执行 171 def on_closing(self): 172 if messagebox.askokcancel("退出提示", "是否离开聊天室?"): 173 self.window.destroy() 174 175 176 def main(): 177 chatRoom = Client() 178 client = chatRoom.connect() 179 t = threading.Thread(target=chatRoom.receive, args=(client,)) # 创建一个线程,监听消息 180 t.start() 181 # 创建主窗口,用于容纳其它组件 182 chatRoom.window = tkinter.Tk() 183 # 登录界面控件创建、布局 184 client_draw.draw_login(chatRoom) 185 # 进入事件(消息)循环 186 tkinter.mainloop() 187 188 if __name__ == "__main__": 189 main()
client_draw.py
1 import tkinter 2 3 4 # 登录页面 5 def draw_login(self): 6 # 设置主窗口标题 7 self.window.title("聊天室登录界面") 8 # 设置主窗口大小 9 self.window.geometry("450x300") 10 # 创建画布 11 self.canvas = tkinter.Canvas(self.window, height=200, width=500) 12 # 创建一个`Label`名为`账 号: ` 13 self.label_account = tkinter.Label(self.window, text='账 号') 14 # 创建一个`Label`名为`密 码: ` 15 self.label_password = tkinter.Label(self.window, text='密 码') 16 # 创建一个账号输入框,并设置尺寸 17 self.input_account = tkinter.Entry(self.window, width=30) 18 # 创建一个密码输入框,并设置尺寸 19 self.input_password = tkinter.Entry(self.window, show='*', width=30) 20 # 登录按钮 21 self.login_button = tkinter.Button(self.window, command=self.verify_login, text="登 录", width=10) 22 # 注册按钮 23 self.register_button = tkinter.Button(self.window, command=self.register_interface, text="注 册", width=10) 24 25 # 登录页面各个控件进行布局 26 self.label_account.place(x=90, y=70) 27 self.label_password.place(x=90, y=150) 28 self.input_account.place(x=135, y=70) 29 self.input_password.place(x=135, y=150) 30 self.login_button.place(x=120, y=235) 31 self.register_button.place(x=250, y=235) 32 33 34 # 注册界面 35 def draw_register(self): 36 # 登录按钮销毁 37 self.login_button.destroy() 38 # 注册按钮销毁 39 self.register_button.destroy() 40 self.window.title("聊天室注册界面") 41 self.window.geometry("450x300") 42 # 创建画布 43 self.canvas = tkinter.Canvas(self.window, height=200, width=500) 44 # 创建一个"Label",名为:"昵 称" 45 self.label_nickname = tkinter.Label(self.window, text='昵 称') 46 # 创建一个昵称输入框,并设置尺寸 47 self.input_nickname = tkinter.Entry(self.window, width=30) 48 # 创建注册按钮 49 self.register_submit_button = tkinter.Button(self.window, command=self.verify_register, text="提交注册", width=10) 50 # 创建注册按钮 51 self.return_login_button = tkinter.Button(self.window, command=self.return_login_interface, text="返回登录",width=10) 52 53 # 注册界面各个控件进行布局 54 self.label_account.place(x=90, y=70) 55 self.label_password.place(x=90, y=130) 56 self.input_account.place(x=135, y=70) 57 self.input_password.place(x=135, y=130) 58 self.label_nickname.place(x=90, y=190) 59 self.input_nickname.place(x=135, y=190) 60 self.register_submit_button.place(x=120, y=235) 61 self.return_login_button.place(x=250, y=235) 62 63 64 # 聊天室界面 65 def draw_chat(self, nickname): 66 self.window.title("【%s】的聊天室界面" % nickname) 67 self.window.geometry("520x560") 68 # 创建frame容器 69 # 放置聊天记录 70 self.frmLT = tkinter.Frame(width=500, height=320) 71 # 放置发送内容输入框 72 self.frmLC = tkinter.Frame(width=500, height=150) 73 # 放置发送按钮 74 self.frmLB = tkinter.Frame(width=500, height=30) 75 76 self.txtMsgList = tkinter.Text(self.frmLT) 77 # 设置消息时间字体样式 78 self.txtMsgList.tag_config('DimGray', foreground='#696969', font=("Times", "11")) 79 # 设置自己的消息字体样式 80 self.txtMsgList.tag_config('DarkTurquoise', foreground='#00CED1', font=("Message", "13"), spacing2=5) 81 # 设置其它人的消息字体样式 82 self.txtMsgList.tag_config('Black', foreground='#000000', font=("Message", "13"), spacing2=5) 83 84 self.txtMsg = tkinter.Text(self.frmLC) 85 # 触发键盘的回车按键事件,发送消息 86 self.txtMsg.bind("<KeyPress-Return>", self.send_msg_event) 87 self.btnSend = tkinter.Button(self.frmLB, text='发送', width=12, command=self.send_msg) 88 # 创建空的Label在左边占个位置,便于发送按钮靠右 89 self.labSend = tkinter.Label(self.frmLB, width=55) 90 91 # 窗口布局 92 self.frmLT.grid(row=0, column=0, columnspan=2, padx=10, pady=10) 93 self.frmLC.grid(row=1, column=0, columnspan=2, padx=10, pady=10) 94 self.frmLB.grid(row=2, column=0, columnspan=2, padx=10, pady=10) 95 96 # 固定大小 97 self.frmLT.grid_propagate(0) 98 self.frmLC.grid_propagate(0) 99 self.frmLB.grid_propagate(0) 100 101 self.labSend.grid(row=0, column=0) 102 # 发送按钮布局 103 self.btnSend.grid(row=0, column=1) 104 self.txtMsgList.grid() 105 self.txtMsg.grid() 106 107 # WM_DELETE_WINDOW 不能改变,这是捕获命令 108 self.window.protocol('WM_DELETE_WINDOW', self.on_closing)
mode 包
chat_mode.py
1 import json 2 3 4 # 聊天逻辑 5 def chat(self, data_dict): 6 """ 7 :param self: 连接对象 8 :param data_dict: 客户端的请求消息 9 """ 10 message = data_dict["message"].strip() 11 # 遍历所有的连接对象,群发消息 12 for user in self.users.keys(): 13 data = {} 14 data["type"] = "chat" 15 # 获取当前发送消息客户端的昵称 16 nickname = self.users[self] 17 data["nickname"] = nickname 18 # "isMy"键默认为no 19 data["isMy"] = "no" 20 # 如果遍历的对象与发消息客户端是同一个,则将isMy字段设为yes, 便于前端用来判断展示不同的字体样式 21 if user == self: 22 data["isMy"] = "yes" 23 data["message"] = message 24 data = json.dumps(data) 25 user.sendLine(data.encode("utf-8"))
login_mode.py
1 from db.user_info_util import user_util 2 import json 3 4 5 # 登录逻辑 6 def login(self, data_dict): 7 """ 8 :param self: 连接对象 9 :param data_dict: 客户端的请求消息 10 """ 11 username = data_dict["username"].strip() 12 password = data_dict["password"].strip() 13 # 服务器端的响应消息 14 data = {} 15 # 账号密码不能为空 16 if username and password: 17 code, msg, nickname = login_check(username, password) 18 elif not username: 19 code = "003" 20 msg = "登录用户名不能为空" 21 elif not password: 22 code = "004" 23 msg = "登录密码不能为空" 24 # 登录成功,将连接对象以及昵称加到users中,便于后续遍历发送消息 25 if code == "000": 26 # 在全局变量users中新增用户信息 27 self.users[self] = nickname 28 data["nickname"] = nickname 29 data["type"] = "login" 30 data["code"] = code 31 data["msg"] = msg 32 data = json.dumps(data) 33 self.sendLine(data.encode("utf-8")) 34 35 36 # 登录校验逻辑 37 def login_check(username, password): 38 # 通过用户名到数据库获取用户信息 39 user_info = user_util.user_check(username) 40 # 未查到该用户信息,代表未注册 41 if len(user_info) == 0: 42 data = ("001", "账号【%s】未注册,请先进行注册!" % username, None) 43 # 密码错误 44 elif password != user_info[0][1]: 45 data = ("002", "密码有误,请重新输入!", None) 46 # 正常登录 47 else: 48 # 获取昵称 49 nickname = user_info[0][2] 50 data = ("000", "账号【%s】登录成功!" % username, nickname) 51 return data
register_mode.py
1 from db.user_info_util import user_util 2 import json 3 4 5 # 注册逻辑 6 def register(self, data_dict): 7 """ 8 :param self: 连接对象 9 :param data_dict: 客户端的请求消息 10 """ 11 username = data_dict["username"].strip() 12 password = data_dict["password"].strip() 13 nickname = data_dict["nickname"].strip() 14 # 服务器端的响应消息 15 data = {} 16 # 三者均不为空才能走注册校验 17 if username and password and nickname: 18 code, msg = register_check(username, password, nickname) 19 elif not username: 20 code = "002" 21 msg = "注册账号不能为空" 22 elif not password: 23 code = "003" 24 msg = "注册密码不能为空" 25 elif not nickname: 26 code = "004" 27 msg = "注册昵称不能为空" 28 if code == "000": 29 self.users[self] = nickname 30 data["nickname"] = nickname 31 32 data["type"] = "register" 33 data["code"] = code 34 data["msg"] = msg 35 data = json.dumps(data) 36 self.sendLine(data.encode("utf-8")) 37 38 39 # 注册校验 40 def register_check(username, password, nickname): 41 user_info = user_util.user_check(username) 42 if len(user_info) > 0: 43 data = ("001", "账号【%s】已被注册过" % user_info) 44 else: 45 user_util.user_insert(username, password, nickname) 46 data = ("000", "账号【%s】注册成功,点击'确定'进入聊天页面" % username) 47 return data
db 包
user_info_util.py
1 from db import mysql_util 2 3 class UserUtil: 4 5 def __init__(self, host, port, db, user, passwd, charset="utf8"): 6 self.mysql = mysql_util.MysqlTool(host, port, db, user, passwd, charset) 7 8 def user_check(self, username): 9 check_sql = "SELECT username,password,nickname FROM user WHERE username = '%s'" % username 10 self.mysql.connect() 11 user_info = self.mysql.get_all(check_sql) 12 self.mysql.close() 13 return user_info 14 15 def user_insert(self, username, passwd, nickname): 16 insert_sql = "INSERT INTO user(username,password,nickname) VALUES('%s','%s','%s')" % (username, passwd, nickname) 17 self.mysql.connect() 18 self.mysql.insert(insert_sql) 19 self.mysql.close() 20 21 user_util = UserUtil("localhost", 3306, "test", "root", "admin") 22 23 if __name__ == "__main__": 24 print(user_util.user_check("username_test")) 25 user_util.user_insert("username_test2", "pwd", "nickname") 26 print(user_util.user_check("username_test2"))
mysql_util.py
1 import pymysql 2 3 4 class MysqlTool: 5 6 def __init__(self, host, port, db, user, passwd, charset="utf8"): 7 self.host = host 8 self.port = port 9 self.db = db 10 self.user = user 11 self.passwd = passwd 12 self.charset = charset 13 14 # 创建数据库连接与执行对象 15 def connect(self): 16 try: 17 self.conn = pymysql.connect(host=self.host, port=self.port, 18 db=self.db, user=self.user, passwd=self.passwd, charset=self.charset) 19 self.cursor = self.conn.cursor() 20 except Exception as e: 21 print(e) 22 23 # 关闭数据库连接与执行对象 24 def close(self): 25 try: 26 self.cursor.close() 27 self.conn.close() 28 except Exception as e: 29 print(e) 30 31 # 获取一行数据 32 def get_one(self, sql): 33 try: 34 self.cursor.execute(sql) 35 result = self.cursor.fetchone() 36 except Exception as e: 37 print(e) 38 else: 39 return result 40 41 # 获取全部行的数据 42 def get_all(self, sql): 43 try: 44 self.cursor.execute(sql) 45 result = self.cursor.fetchall() 46 except Exception as e: 47 print(e) 48 else: 49 return result 50 51 # 增删改查的私有方法 52 def __edit(self, sql): 53 try: 54 execute_count = self.cursor.execute(sql) 55 self.conn.commit() 56 except Exception as e: 57 print(e) 58 else: 59 return execute_count 60 61 # 插入数据 62 def insert(self, sql): 63 return self.__edit(sql) 64 65 # 删除数据 66 def delete(self, sql): 67 return self.__edit(sql) 68 69 70 if __name__ == "__main__": 71 mysql = MysqlTool("localhost", 3306, "test", "root", "admin") 72 mysql.connect() 73 mysql.insert('insert into user(username, password, nickname) values("username_test1", "pwd_test1", "nick_test1");') 74 print(mysql.get_all("select * from user;")) 75 mysql.delete('delete from user where username="%s"' % "username_test1") 76 print(mysql.get_all("select * from user;"))