代碼下載
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;"))