多人聊天室具体步骤
- 制作协议报头,响应数据,制定一个模块config.py
# ----数据协议相关配置-----
REQUEST_LOGIN='0001' #登录请求
REQUEST_CHAT='0002' # 请求聊天
REQUEST_LOGIN_RESULT='1001' # 登录结果响应
RESPONSE_CHAT='1002' # 聊天响应
DELIMITER='|' # 自定义协议数据分隔符
- 处理服务器响应字符串的拼接
from config import *
class ResponseProtocol(object):
""" 服务器响应协议的格式字符串处理"""
@staticmethod
def response_login_result(result,nickname,username):
"""
返回生成用户登录的结果字符串
:param result: 登录结果,0表示失败,1表示成功
:param nickname: 登录用户的昵称,登陆失败为空
:param username:登录用户的账号,如果失败为空
:return:返回给用户登录结果协议字符串
"""
return DELIMITER.join([REQUEST_LOGIN_RESULT],result,nickname,username)
@staticmethod
def response_chat(nicakname,messages): #这里是服务器发送给客户端的消息 转发的消息
"""
生成返回给用户的消息字符串
:param nicakname:发送消息的用户昵称
:param messages:消息正文
:return:返回给用户的消息字符串
"""
return DELIMITER.join(RESPONSE_CHAT,nicakname,messages)
主体框架搭建
基本逻辑业务-服务端
server.py 模块定义Server类来处理服务器业务逻辑,该类实现了服务器的主体框架
import socket
from config import *
class ServerSocket(socket.socket):
"""自定义套接字,负责初始化服务器套接字需要的相关参数"""
def __init__(self):
# 设置为tcp类型
super(ServerSocket,self).__init__(socket.AF_INET,socket.SOCK_STREAM)
# 绑定地址和端口
self.bind((SERVER_IP,SERVER_PORT))
# 监听端口
self.listen(128)
这里我们自定义一个套接字,让类继承socket ,super找父类的套接字有一个初始化,不初始化的类型告诉他 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=ServerSocket()
def startup(self):
"""获取客户端链接,提供服务"""
# 获取客户端的链接
while 1:
print('正在客户端链接')
soc,addr = self.server_socket.accept()
print('获取到客户端链接')
# 使用套接字生成包装对象
client_soc=SocketWrapper(soc)
# 开启线程
# t=Thread(target=self.request_handle,args=(client_soc,))
# t.start()
Thread(target=lambda: self.request_handle(client_soc)).start()
首先在__ init__ 方法里创建监听的套接字,当我们调用start方法启动服务器程序,在该函数中我们使用while来获取客户端的连接,有客户连接到服务器,服务器会获取一个套接字来标识与该客户的连接,然后我们开启新的线程来处理客户端的连接,该线程函数为Server类中的request_handle方法,该方法接收套接字作为参数,request_handle 方法是服务端请求处理的核心方法
服务端获取与标识客户端连接的套接字之后,我们要对他进行包装,用ServerWrapper类今昔那个封装,由于使用原始的套接字发送和接收数据的时候,需要encode和decode,我们就在ServerWrapper封装了编码解码的操作,该方法提供了接收的发放send_data,recv_dat 会在收到的时候自动解码编码。
class SocketWrapper(object):
'''套接字包装类'''
def __init__(self,sock):
self.sock=sock
def recv_data(self):
'''接收数据的功能'''
# 收发消息
return self.sock.recv(512).decode('utf-8')
def send_data(self,message):
return self.sock.send(message.encode('utf-8'))
def close(self):
''' 关闭套接字'''
self.sock.close()
request_handle 的处理
接收-->解析-->判断-->处理
class Server(object):
"""服务器的核心类"""
def __init__(self):
# 创建服务器套接字
self.server_socket=ServerSocket()
# 创建请求的id和方法关联字典
self.request_handle_function={}
self.register(REQUEST_LOGIN, self.request_login_handle)
self.register(REQUEST_CHAT, self.request_chat_handle)
def register(self,request_id,handle_function):
""" 注册 消息类型和处理函数到字典"""
self.request_handle_function[request_id]=handle_function
-----------------------------------------------------------
def request_handle(self,client_soc):
''' 处理客户端请求'''
while True:
# 接收客户端数据
recv_data = client_soc.recv_data()
if not recv_data:
# 没有接收到客户端应该关闭
self.remove_offline_user(client_soc)
client_soc.close()
break
# 解析数据
parse_data= self.parse_request_text(recv_data)
# 分析请求类型,调用相应的处理函数
handle_function=self.request_handle_function.get(parse_data['request_id'])
if handle_function:
handle_function()
我们接受到客户的数据之后看它发来的数据类型是什么,调用相应的处理函数,这里的id类型和方法是唯一的,我们只需要初始化一次就可以,在init初始化。在后面我们不可能只有发送信息的功能,可能还有图片,视频等等在初始化里面加功能id就可以,来梳理思路:假如发送的消息是0001|uu|11111 调用 parse_request_text,按照类型分析数据 ,发现id=0001,返回 request_data ,分析请求类型,调用相应的处理函数 ,调用 request_handle_function, 发现请求的id在里面,开始调用登录功能。
登录和聊天功能的处理
- 获取登录的用户名和密码
- 查询数据,是否存在对应的用户
- 如果登录成功,保存用户信息,失败什么都不做
- 返回登录结果给客户端
def request_login_handle(self,client_soc,parse_data):
""" 处理登录功能"""
print('收到登录请求')
# 获取账号和密码
username = parse_data['username']
password = parse_data['password']
# 检查是否能够登录
ret, nickname, username = self.check_user_login(username, password)
# 登录成功保存当前用户
if ret == '1':
self.clients[username] = {'sock': client_soc, 'nickname': nickname}
# 拼接返回给客户端的信息
response_text = ResponseProtocol.response_login_result(ret, nickname, username)
# 把消息发送给客户端
client_soc.send_data(response_text)
def check_user_login(self, username, password):
"""检查用户是否登录成功,并返回结果(0/失败,1/成功),昵称,用户账号"""
# 从数据库查询用户信息
result= self.db.get_one("select * from user where user_name='%s'" %(username))
#没有查询的结果,用户不存在
if not result:
return '0','',username
# 密码不匹配,密码错误
if password!=result['user_password']:
return '0','',username
# 登陆成功
return '1',result['user_nickname'],username
数据库的处理 :初始化数据库
from pymysql import connect
from config import *
class DB(object):
# 连接到数据库
def __init__(self):
self.conn = connect(host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASS,
charset='utf8',
)
# 获取游标
self.cursor = self.conn.cursor()
def get_one(self, sql):
""" 使用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 close(self):
"""释放数据库资源"""
self.cursor.close()
self.conn.close()
清理离线的用户
def remove_offline_user(self,client_soc):
""" 客户端下线的处理 """
print('有客户端下线了')
for username,info in self.clients.items():
if info['sock']== client_soc:
del self.clients[username]
break
聊天功能处理:通过服务器向每一个登录在线的人转发消息,不需要向自己发消息
def request_chat_handle(self,client_soc,request_data):
""" 处理聊天功能 """
print('可以聊天了')
# 获取消息的内容
username=request_data['username']
messages=request_data['messages']
nickname=self.clients[username]['nickname']
# 拼接发送给客户端的消息内容
msg=ResponseProtocol.response_chat(nickname,messages)
#转发给在线用户
for u_name,info in self.clients.items():
if username==u_name: # 不需要向发送消息的账号转发数据
continue
info['sock'].send_data(msg)
客户端
客户端我们采用GUI视图来写,不懂的可以提前看一下
登录窗口显示
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()
def window_init(self):
"""初始化窗口"""
# 设置窗口标题
self.title('登录窗口')
# 设置窗口不能拉伸
self.resizable(False,False)
# 获取窗口位置变量
window_width=255
window_height=95
screen_width=self.winfo_screenwidth()
screen_heigth=self.winfo_screenheight()
pos_x=screen_width/2 - window_width/2
pos_y=(screen_heigth - 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)
整体采用了grid表格的布局,其中用户名标签放置在(1,1)第一行第一列位置,对应的用户名的输入放置在(1,2),密码标签放置在(2,1),密码的输入放置在(2,2),重置和登录按钮放置在第三行居中的位置
,由于我们已经全局使用了grid表格布局,所有我们将他们放在一个Frame里面,两个按钮在Frame中水平布局
再将Frame整体放置在窗口的第三行,并占据两列
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)
- get()方法用于获取Entry文本输入的内容
- delete(0,END)删除从0位置到最后所有的输入内容
- self.protocol('WM_DELETE_WINDOW',command)用于给窗口设置当右上角关闭的按钮点击时要执行的函数
说明,我们在封装操作方法时获取控件使用的是类似self.children['password_entry']的方法,这是由于文本框按钮等等都是作为Tk的子对象,被存储在了Tk的children字典中