基於 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;"))

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM