基于python面向對象多人聊天室
1、項目環境
項目名稱:多人聊天室
項目模式:C/S
開發環境:win10+python3.8+pycharm
所需知識:python GUI編程,多線程編程,網絡編程,數據庫編程
2、流程
3、程序設計
了解一下服務器扮演的角色,下面是服務器的業務流程。大致工作模式
服務器:
客戶端:
- 首先服務器在指定的端口進行監聽,等待客戶的鏈接
- 客戶端鏈接到服務器之后,服務器開啟單線程來處理該用戶的請求
- 處理線程等待客戶端發送的請求
- 服務器根據客戶端請求類型的不同,調用不同處理的函數
- 處理完客戶端請求之后,再次回到第三步繼續等待處理客戶端新的請求
- 客戶端退出登錄,服務器也會關閉對客戶端的處理線程,釋放資源
4、響應協議設計
-
我們都知道三次握手和四次揮手,這里呢我們約定了客戶端發送什么樣格式的數據給服務器,服務器又需要返回什么樣格式的數據給客戶端,客戶端會有不同的請求,所以我們針對不同的請求個響應定義了需求個相應號,來區分不同的請求和響應
-
網絡上一般使用json和xml格式來傳輸數據,但是用他們來傳輸,對于我們的項目有點復雜,我們的項目沒有這么復雜的數據,我們采用|進行分割,然后拿到數據進行split一下就可以了。
-
登錄響應格式: 1001|ret|nickname|username,其中ret
代表服務器端驗證的結果,如果是0,表示服務端驗證失敗,后面的nickname username
會為空字符串,若是1 ,表示服務端驗證成功,nickname 為服務端返回的該用戶的昵稱,username 是該用戶的用戶名。
-
聊天的響應格式:1002|nickname|message, nicakname 是為聊天信息發送者的昵稱,message
是發送的聊天信息
下面我們定義了服務端需要的一些常量,以及為了實現客戶端和服務端通信定義的一些協議編號,協議編號如下:
#----數據協議相關配置----
REQUEST_LOGIN = '0001' #登陸請求
REQUEST_CHAT= '0002' #聊天請求
RESPONSE_LOGIN_RESULT = '1001' #登陸結果響應
RESPONSE_CHAT= '1002' #聊天響應
DELIMITER = '|' #自定義協議數據分割符
SERVER_IP = '127.0.0.1' #服務器地址
SERVER_PORT = 8090 #服務器端口
5、面向對象的思想
服務器、客戶端分離設計內容:
6、服務器通訊實現
制作協議報頭,響應數據,制定一個模塊config.py
#----數據協議相關配置----
REQUEST_LOGIN = '0001' #登陸請求
REQUEST_CHAT= '0002' #聊天請求
RESPONSE_LOGIN_RESULT = '1001' #登陸結果響應
RESPONSE_CHAT= '1002' #聊天響應
DELIMITER = '|' #自定義協議數據分割符
SERVER_IP = '127.0.0.1' #服務器地址
SERVER_PORT = 8090 #服務器端口#----數據庫相關配置---- #具體參數自己設置
DB_HOST='127.0.0.1'#這里是你數據庫IP地址
DB_PORT=3306 #這里是你數據庫端口
DB_NAME='py_chat' #這里是你數據庫名
DB_USER='root' #這里是你數據庫登陸賬號
DB_PASSWD='123456' #這里是你數據庫密碼
CHARSET='utf-8'
處理服務器響應字符串的拼接,制定一個模塊response_protocol.py
from config import *class ResponseProtocol (object):"""服務器響應協議的格式字符串處理"""@staticmethoddef response_login_result(result: str, nickname: str, username: str) -> str:"""拼接登陸響應:param result:登陸結果,0或則1,0標識登陸失敗,1標識登陸成功:param nickname:登陸名,登陸失敗,該值為空字符串:param username:登陸ID,登陸失敗,該值為空字符串:return 登陸結果相應格式字符串"""return DELIMITER.join ([RESPONSE_LOGIN_RESULT, result, nickname, username])@staticmethoddef response_chat(nickname, messages):"""拼接聊天相應,數據格式為:“相應協議編號|聊天發送者昵稱|聊天信息”:param nickname: 聊天內容發送者昵稱:param messages: 聊天內容:return: 聊天相應協議格式字符串"""return DELIMITER.join([RESPONSE_CHAT, nickname, messages])
7、主體框架搭建
基本邏輯業務-服務端
server.py 模塊定義Server類來處理服務器業務邏輯,該類實現了服務器的主體框架
from server_socket import ServerSoket
from socket_warpper import SocketWrapper
from threading import Thread
from config import *
from response_protocol import *
from dbHandle import DBHandleclass Server(object):"""自定義套接字,負責初始化服務器套接字需要的相關參數"""def __init__(self):# 初始化套結字self.server_socket = ServerSoket()# 創建請求的ID和方法關聯字典self.requset_handle_function = {}self.register(REQUEST_LOGIN, self.request_login_handle)self.register(REQUEST_CHAT, self.request_chat_handle)# 創建保存當前登錄用戶字典self.clients = {}self.db = DBHandle()'''注冊消息類型和處理函數到字典'''def register(self, requeset_id, handle_function):self.requset_handle_function[requeset_id] = handle_function'''啟動程序'''def startup(self):"""啟動器"""while True:print('正在等待客戶端連接')soc, addr = self.server_socket.accept()# print ('獲取到客戶端連接')client_soc = SocketWrapper(soc)# 啟動線程處理該用戶請求Thread(target=lambda: self.request_handle(client_soc)).start()'''處理客戶端數據'''def request_handle(self, client_soc):while True:# 接收客戶端數據recv_data = client_soc.recv_data()if not recv_data:# 沒有接收到數據客戶端應該已經關閉self.remve_offline_user(client_soc)client_soc.close()break'''解析數據'''parse_data = self.parse_request_text(recv_data)'''分析請求類型,并依據請求類型調用相應的分類處理''''''# 獲得使用的方法名 方法名 = 字典[value] 注: 如 字典[key]可以互相找到字典[value]# 此處 字典[key]=0001 對應得字典[value] = REQUEST_LOGIN#例子:parse_data = '0001|XXX|XXX'parse_data['requset_id'] = ‘0001’requset_handle_function['0001'] = self.request_login_handlehandle_funtion = self.request_login_handle'''handle_funtion = self.requset_handle_function.get(parse_data['requset_id'])if handle_funtion:# 按照方法名調用方法handle_funtion(client_soc, parse_data)else:# 如果傳輸內容不匹配,返回錯誤請求self.request_err_handle(client_soc)'''用戶離線操作'''def remve_offline_user(self, client_soc):for username, info in self.clients.items():if info['sock'] == client_soc:print(self.clients[username]['nickname'] + '已經離開')del self.clients[username]break'''解析客戶端發送來的數據''''''解析數據內容'''def parse_request_text(self, recv_data):'''登錄信息登錄信息:0001|username|password聊天信息:0002|username|messages錯誤信息:err'''print('解析客戶端數據:' + recv_data)requset_list = recv_data.split(DELIMITER)requset_data = {}requset_data['requset_id'] = requset_list[0]if requset_data['requset_id'] == REQUEST_LOGIN:requset_data['username'] = requset_list[1]requset_data['password'] = requset_list[2]elif requset_data['requset_id'] == REQUEST_CHAT:requset_data['username'] = requset_list[1]requset_data['messages'] = requset_list[2]return requset_data'''登錄處理'''def request_login_handle(self, client_sock, requet_data):# print('收到登錄請求')username = requet_data['username']password = requet_data['password']# 查詢用戶是否合法ret, nickname, username = self.check_user_login(username, password)# 如果登錄成功,則保存用戶連接套接字if ret == '1':self.clients[username] = {'sock': client_sock, 'nickname': nickname}# 組裝響應結果response_text = ResponseProtocol.response_login_result(ret, nickname, username)# 發送響應結果client_sock.send_data(response_text)'''聊天處理'''def request_chat_handle(self, client_sock, requet_data):# 獲取消息內容username = requet_data['username']messages = requet_data['messages']try:nickname = self.clients[username]['nickname']except:client_sock.send_data('您未登錄,請登錄后再發消息')return# 拼接發送給客戶的消息文本msg = ResponseProtocol.response_chat(nickname, messages)# 轉發消息給在線用戶for u_name, info in self.clients.items():if username == u_name:continueinfo['sock'].send_data(msg)print(msg)'''錯誤信息處理'''def request_err_handle(self, client_sock):print("傳輸數據出錯------")client_sock.send_data('數據無效,請重新確認')'''檢查用戶是否登錄成功,返回檢查結果(0/失敗,1/成功,昵稱,用戶賬號'''def check_user_login(self, username, password):# print("正在檢測是否成功")# 從數據庫查詢用戶信息sql = "select * from users where username = '%s' " % usernameresult = self.db.get_one(sql)# 如果沒有查詢結果,用戶不存在,登錄失敗if not result:# print("用戶不存在,登錄失敗")return '-1', ' ', username# 密碼不匹配,說明密碼錯誤,登錄失敗if password != result["password"]:# print("密碼錯誤,登錄失敗")return '0', ' ', username# 登錄成功# print("驗證正確,登錄成功")print(result['nickname'] + "進入聊天室")return '1', result['nickname'], usernameif __name__ == '__main__':Server().startup()
- 這里我們自定義一個套接字,讓類繼承socket、super找父類的套接字有一個初始化,不初始化的類型告訴他
import socket
import configclass ServerSoket(socket.socket):"""自定義套接字,負責初始化服務器套接字需要的相關參數"""def __init__(self ):#設置TCP類型#初始化套結字super(ServerSoket,self).__init__(socket.AF_INET,socket.SOCK_STREAM)self.bind((config.SERVER_IP,config.SERVER_PORT))#設置為監聽模式self.listen(128)
super(ServerSocket,self).init(socket.AF_INET,socket.SOCK_STREAM),綁定地址和端口,這里的參數不能寫死,因為你要是寫死,以后你要改代碼要找一大堆的代碼,這里我們把它固定在config.py 里面,以后要想改直接到配置相關項去改。
from server_socket import ServerSocket
from socket_wrapper import SocketWrapper
from threading import Thread
class Server(object):"""服務器的核心類"""def __init__(self):# 初始化套結字self.server_socket = ServerSoket()# 創建請求的ID和方法關聯字典self.requset_handle_function = {}self.register(REQUEST_LOGIN, self.request_login_handle)self.register(REQUEST_CHAT, self.request_chat_handle)# 創建保存當前登錄用戶字典self.clients = {}self.db = DBHandle()def startup(self):"""啟動器"""while True:print('正在等待客戶端連接')soc, addr = self.server_socket.accept()# print ('獲取到客戶端連接')client_soc = SocketWrapper(soc)# 啟動線程處理該用戶請求Thread(target=lambda: self.request_handle(client_soc)).start()
首先在__ init__ 方法里創建監聽的套接字,當我們調用start方法啟動服務器程序,在該函數中我們使用while來獲取客戶端的連接,有客戶連接到服務器,服務器會獲取一個套接字來標識與該客戶的連接,然后我們開啟新的線程來處理客戶端的連接,該線程函數為Server類中的request_handle方法,該方法接收套接字作為參數,request_handle 方法是服務端請求處理的核心方法
8、消息處理request_handle 的處理
接收–>解析–>判斷–>處理
def __init__(self):# 初始化套結字self.server_socket = ServerSoket()# 創建請求的ID和方法關聯字典self.requset_handle_function = {}self.register(REQUEST_LOGIN, self.request_login_handle)self.register(REQUEST_CHAT, self.request_chat_handle)# 創建保存當前登錄用戶字典self.clients = {}def register(self, requeset_id, handle_function):'''注冊消息類型和處理函數到字典'''self.requset_handle_function[requeset_id] = handle_functiondef request_handle(self, client_soc):'''處理客戶端數據'''while True:# 接收客戶端數據recv_data = client_soc.recv_data()if not recv_data:# 沒有接收到數據客戶端應該已經關閉self.remve_offline_user(client_soc)client_soc.close()break'''解析數據'''parse_data = self.parse_request_text(recv_data)'''分析請求類型,并依據請求類型調用相應的分類處理''''''# 獲得使用的方法名 方法名 = 字典[value] 注: 如 字典[key]可以互相找到字典[value]# 此處 字典[key]=0001 對應得字典[value] = REQUEST_LOGIN#例子:parse_data = '0001|XXX|XXX'parse_data['requset_id'] = ‘0001’requset_handle_function['0001'] = self.request_login_handlehandle_funtion = self.request_login_handle'''handle_funtion = self.requset_handle_function.get(parse_data['requset_id'])if handle_funtion:# 按照方法名調用方法handle_funtion(client_soc, parse_data)else:# 如果傳輸內容不匹配,返回錯誤請求self.request_err_handle(client_soc)
- 我們接受到客戶的數據之后看它發來的數據類型是什么,調用相應的處理函數,這里的id類型和方法是唯一的,我們只需要初始化一次就可以,在init初始化。在后面我們不可能只有發送信息的功能,可能還有圖片,視頻等等在初始化里面加功能id就可以,來梳理思路:假如發送的消息是0001|uu|11111
調用 parse_request_text,按照類型分析數據 ,發現id=0001,返回 request_data
,分析請求類型,調用相應的處理函數 ,調用 request_handle_function, 發現請求的id在里面,開始調用登錄功能。
9、登錄和聊天功能的處理
- 獲取登錄的用戶名和密碼
- 查詢數據,是否存在對應的用戶
- 如果登錄成功,保存用戶信息,失敗什么都不做
- 返回登錄結果給客戶端
'''登錄處理'''def request_login_handle(self, client_sock, requet_data):# print('收到登錄請求')username = requet_data['username']password = requet_data['password']# 查詢用戶是否合法ret, nickname, username = self.check_user_login(username, password)# 如果登錄成功,則保存用戶連接套接字if ret == '1':self.clients[username] = {'sock': client_sock, 'nickname': nickname}# 組裝響應結果response_text = ResponseProtocol.response_login_result(ret, nickname, username)# 發送響應結果client_sock.send_data(response_text)
'''聊天處理'''def request_chat_handle(self, client_sock, requet_data):# 獲取消息內容username = requet_data['username']messages = requet_data['messages']try:nickname = self.clients[username]['nickname']except:client_sock.send_data('您未登錄,請登錄后再發消息')return# 拼接發送給客戶的消息文本msg = ResponseProtocol.response_chat(nickname, messages)# 轉發消息給在線用戶for u_name, info in self.clients.items():if username == u_name:continueinfo['sock'].send_data(msg)print(msg)
'''檢查用戶是否登錄成功,返回檢查結果(0/失敗,1/成功,昵稱,用戶賬號'''def check_user_login(self, username, password):# print("正在檢測是否成功")# 從數據庫查詢用戶信息sql = "select * from users where username = '%s' " % usernameresult = self.db.get_one(sql)# 如果沒有查詢結果,用戶不存在,登錄失敗if not result:# print("用戶不存在,登錄失敗")return '-1', ' ', username# 密碼不匹配,說明密碼錯誤,登錄失敗if password != result["password"]:# print("密碼錯誤,登錄失敗")return '0', ' ', username# 登錄成功# print("驗證正確,登錄成功")print(result['nickname'] + "進入聊天室")return '1', result['nickname'], username
10、數據庫的處理
from pymysql import connect
from config import *class DBHandle(object):'''mysql管理器'''def __init__(self):'''初始化數據庫'''self.conn= connect(host=DB_HOST,port=DB_PORT,database=DB_NAME,user=DB_USER,password=DB_PASSWD)self.cursor = self.conn.cursor()# 釋放數據庫資源def close_db(self):self.cursor.close()self.conn.close()def get_one(self, sql):#執行SQL結果self.cursor.execute(sql)#獲取查詢結果query_result = self.cursor.fetchone()#判斷是否有結果if not query_result:return None#獲得字段名稱列表fileds = [filed[0] for filed in self.cursor.description]#保存返回結果return_data = {}for filed, value in zip(fileds, query_result):return_data[filed] = value#查詢結果return return_data
'''用戶離線操作'''def remve_offline_user(self, client_soc):for username, info in self.clients.items():if info['sock'] == client_soc:print(self.clients[username]['nickname'] + '已經離開')del self.clients[username]break
- 聊天功能處理:通過服務器向每一個登錄在線的人轉發消息,不需要向自己發消息
‘’‘聊天處理’’’
def request_chat_handle(self, client_sock, requet_data):# 獲取消息內容username = requet_data['username']messages = requet_data['messages']try:nickname = self.clients[username]['nickname']except:client_sock.send_data('您未登錄,請登錄后再發消息')return# 拼接發送給客戶的消息文本msg = ResponseProtocol.response_chat(nickname, messages)# 轉發消息給在線用戶for u_name, info in self.clients.items():if username == u_name:continueinfo['sock'].send_data(msg)print(msg)
11、客戶端實現
客戶端采用GUI視圖來寫
登錄窗口顯示
新建項目 Client
新建win_client.py
from tkinter import Tk
from tkinter import Label,Entry,Frame,Button,LEFT,ENDclass WindowLogin (Tk):"""登陸窗口"""def __init__(self):super (WindowLogin, self).__init__ ()# 設置窗口屬性self.window_init ()# 填充控件self.add_widgets ()# self.on_reset_button_click (lambda :print(self.get_username()))# self.on_login_button_click (lambda: print(self.get_password()))"""初始化窗口屬性"""def window_init(self):#設置窗口標題self.title('登陸窗口')#設置窗口不能被拉伸self.resizable (False,False)#獲取窗口的位置變量window_width = 255window_height =100screenWidth = self.winfo_screenwidth()screenHeight = self.winfo_screenheight()pos_x = (screenWidth-window_width)/2pos_y = (screenHeight-window_height)/2#設置窗口大小和位置self.geometry('%dx%d+%d+%d' % (window_width,window_height,pos_x,pos_y))"""添加控件到窗口"""def add_widgets(self):"""添加控件到窗口"""# 用戶名提示標簽username_label = Label (self)username_label['text'] = '用戶名:'username_label.grid (row=0, column=0, padx=10, pady=5)# 用戶名輸入文本框username_entry = Entry (self, name='username_entry')username_entry['width'] = 20username_entry.grid (row=0, column=1, padx=10, pady=5)# 密碼提示標簽password_label = Label (self)password_label['text'] = '密 碼:'password_label.grid (row=1, column=0)# 密碼輸入文本框password_entry = Entry (self, name='password_entry')password_entry['show'] = '*'username_entry['width'] = 20password_entry.grid (row=1, column=1)# 按鈕區button_frame = Frame (self, name='button_frame')# 重置按鈕reset_button = Button (button_frame, name='reset_button')reset_button['text'] = '重置'reset_button.pack (side=LEFT, padx=40)# 登錄按鈕login_button = Button (button_frame, name='login_button')login_button['text'] = '登錄'login_button.pack (side=LEFT)button_frame.grid (row=2, columnspan=2, pady=5)def get_username(self):"""獲取用戶名"""return self.children['username_entry'].get ()def get_password(self):"""獲取密碼"""return self.children['password_entry'].get ()def clear_username(self):""" 清空用戶名"""return self.children['username_entry'].delete (0, END)def clear_password(self):""" 清空用戶名"""return self.children['password_entry'].delete (0, END)def on_reset_button_click(self, command):"""重置按鈕的響應注冊"""reset_button = self.children['button_frame'].children['reset_button']reset_button['command'] = commanddef on_login_button_click(self, command):"""登錄按鈕的響應注冊"""login_button = self.children['button_frame'].children['login_button']login_button['command'] = command # 把command函數賦值給登錄按鈕的command,點擊時調用commanddef on_window_close(self, command):"""關閉窗口的響應注冊"""self.protocol ('WM_DELETE_WINDOW', command)
- 整體采用了grid表格的布局,其中用戶名標簽放置在(1,1)第一行第一列位置,對應的用戶名的輸入放置在(1,2),密碼標簽放置在(2,1),密碼的輸入放置在(2,2),重置和登錄按鈕放置在第三行居中的位置。
- 由于我們已經全局使用了grid表格布局,所有我們將他們放在一個Frame里面,兩個按鈕在Frame中水平布局
再將Frame整體放置在窗口的第三行,并占據兩列。
12、客戶端通訊實現
通訊模塊
制作協議報頭,響應數據,創建一個模塊config.py
#----數據協議相關配置----
REQUEST_LOGIN = '0001' #登陸請求
REQUEST_CHAT= '0002' #聊天請求
RESPONSE_LOGIN_RESULT = '1001' #登陸結果響應
RESPONSE_CHAT= '1002' #聊天響應
DELIMITER = '|' #自定義協議數據分割符
SERVER_IP = '127.0.0.1' #服務器地址
SERVER_PORT = 8090 #服務器端口
CHARSET='utf-8'
處理服務器響應字符串的拼接,制定一個模塊request_protocol.py
from config import *class RequestProtocol (object):"""服務器響應協議的格式字符串處理"""@staticmethoddef request_login_result(username,password):"""拼接登陸響應:param username:登陸用戶名,登陸失敗,該值為空字符串:param password:登陸密碼:return 登陸結果相應格式字符串"""return DELIMITER.join ([REQUEST_LOGIN,username, password])@staticmethoddef request_chat(username, messages):"""拼接聊天相應,數據格式為:“相應協議編號|聊天發送者賬號|聊天信息”:param REQUEST_CHAT::param username: 聊天內容發送者賬號:param messages: 聊天內容:return: 聊天相應協議格式字符串"""return DELIMITER.join ([REQUEST_CHAT, username, messages])
13、客戶端業務實現
新建模塊client.py
from request_protocol import RequestProtocol
from window_login import WindowLogin
from client_socket import ClientSocket
from threading import Thread
from config import *
from tkinter.messagebox import showinfoclass Client(object):def __init__(self):"""初始化客戶端資源"""# 初始化登陸窗口self.window = WindowLogin()self.window.on_login_button_click(self.send_login_data)self.window.on_reset_button_click(self.clear_inputs)# 創建客戶端套接字self.conn = ClientSocket()# 初始化消息處理函數self.response_handle_funtion = {}self.regist(RESPONSE_LOGIN_RESULT, self.response_login_handle)self.regist(RESPONSE_CHAT, self.response_chat_handle)def regist(self, requeset_id, handle_function):"""注冊消息和消息對應的方法到字典里"""self.response_handle_funtion[requeset_id] = handle_functiondef startup(self):'''開啟窗口'''self.conn.connect()Thread(target=self.response_handle).start()self.window.mainloop()def clear_inputs(self):"""清空窗口內容"""self.window.clear_password()self.window.clear_username()def send_login_data(self):username = self.window.get_username()password = self.window.get_password()request_text = RequestProtocol.request_login_result(username, password)self.conn.send_data(request_text)def response_handle(self):"不斷收發服務器消息"while True:recv_data = self.conn.recv_data()print('收到服務器消息:' + recv_data)response_data = self.parse_response_data(recv_data)# 根據事件類型,調用指定方法名handle_funtionn = self.response_handle_funtion[response_data['response_id']]if handle_funtionn:handle_funtionn(response_data)@staticmethoddef parse_response_data(recv_data):'''登陸響應消息:1001|成功/失敗|昵稱|賬號聊天響應消息:1002|發送者昵稱|消息內容'''# 使用協議約定的符號來切割消息response_data_list = recv_data.split(DELIMITER)# 解析消息的各個組成部分response_data = {}response_data['response_id'] = response_data_list[0]if response_data['response_id'] == RESPONSE_LOGIN_RESULT:response_data['result'] = response_data_list[1]response_data['nickname'] = response_data_list[2]response_data['username'] = response_data_list[3]elif response_data['response_id'] == RESPONSE_CHAT:response_data['nickname'] = response_data_list[1]response_data['message'] = response_data_list[2]return response_datadef response_chat_handle(self, response_data):print('接收到聊天消息~', response_data)def response_login_handle(self, response_data):result = response_data['result']if result !='1':showinfo('提示','賬號或密碼錯誤')returnnickname = response_data['nickname']username = response_data['username']print('%s 的昵稱為 %s ,已經登錄聊天室' % (username,nickname))
總結
以上是生活随笔為你收集整理的基于python面向对象多人聊天室的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。