基于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):
"""服务器响应协议的格式字符串处理"""
@staticmethod
def 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])
@staticmethod
def 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 DBHandle
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 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_handle
handle_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:
continue
info['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' " % username
result = 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
if __name__ == '__main__':
Server().startup()
- 这里我们自定义一个套接字,让类继承socket、super找父类的套接字有一个初始化,不初始化的类型告诉他
import socket
import config
class 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_function
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_handle
handle_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:
continue
info['sock'].send_data(msg)
print(msg)
'''检查用户是否登录成功,返回检查结果(0/失败,1/成功,昵称,用户账号'''
def check_user_login(self, username, password):
# print("正在检测是否成功")
# 从数据库查询用户信息
sql = "select * from users where username = '%s' " % username
result = 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、数据库的处理
- 新建dbHandle.py
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:
continue
info['sock'].send_data(msg)
print(msg)
11、客户端实现
客户端采用GUI视图来写
登录窗口显示
新建项目 Client
新建win_client.py
from tkinter import Tk
from tkinter import Label,Entry,Frame,Button,LEFT,END
class 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 = 255
window_height =100
screenWidth = self.winfo_screenwidth()
screenHeight = self.winfo_screenheight()
pos_x = (screenWidth-window_width)/2
pos_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'] = 20
username_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'] = 20
password_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'] = command
def on_login_button_click(self, command):
"""登录按钮的响应注册"""
login_button = self.children['button_frame'].children['login_button']
login_button['command'] = command # 把command函数赋值给登录按钮的command,点击时调用command
def 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):
"""服务器响应协议的格式字符串处理"""
@staticmethod
def request_login_result(username,password):
"""
拼接登陆响应
:param username:登陆用户名,登陆失败,该值为空字符串
:param password:登陆密码
:return 登陆结果相应格式字符串
"""
return DELIMITER.join ([REQUEST_LOGIN,username, password])
@staticmethod
def 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 showinfo
class 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_function
def 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)
@staticmethod
def 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_data
def response_chat_handle(self, response_data):
print('接收到聊天消息~', response_data)
def response_login_handle(self, response_data):
result = response_data['result']
if result !='1':
showinfo('提示','账号或密码错误')
return
nickname = response_data['nickname']
username = response_data['username']
print('%s 的昵称为 %s ,已经登录聊天室' % (username,nickname))