第十四章 项目案例《多人聊天室》
案例需求描述
多人聊天室比如微信群、QQ群等就属于多人聊天室项目。多人聊天室项目的特点就是可以拥有多个客户端,每个客户端都有自己的唯一的名称,而且当一个客户端发送数据到聊天室时,整个聊天室中所有成员都可以看到这条数据。
客户端可以有多个,但是服务器端只有一个。一个服务器要处理多个客户端之间的通信就需要使用到多线程。当一个客户端连接服务器成功后,服务器端就会开启一个线程与之通信。
这里使用第三方库wxPython来绘制聊天室界面。
wxPython:
是Python的第三方库(用于图形化界面的),代码实现基于C++的wxWidgets库封装,呈现的界面风格和系统本地风格一致。
安装方式:
pip install wxpython
使用pip命令安装wxpython:
项目功能实现分析
该项目代码功能实现:
- 启动服务器功能实现
- 客户端连接服务器
- 显示聊天信息
- 发送消息到聊天室
- 客户端断开连接
- 客户端重置
- 保存聊天记录
- 停止服务
使用wxPython绘制客户端界面
在使用wxPython去布局客户端界面时,是一层一层嵌套的,要把按钮放到可伸缩的网格布局中,可伸缩的网格布局又要放到盒子中,盒子放在面板上,面板放在窗体上。
客户端界面client.py:
# 注意:编码格式要加
# coding:utf-8
import wx # 注意:导入wxPython用的是import wx
class LxlClient(wx.Frame): # 绘制窗体界面,因此要继承父类Frame
def __init__(self, client_name):
# 调用父类的初始化方法绘制窗体
'''
None表示没有父级窗口
id表示当前窗口的编号
pos:窗体的打开位置,DefaultPosition:默认位置
size:窗体的大小,单位是像素,400宽,450高
'''
wx.Frame.__init__(self, None, id=1001, title=client_name + "的客户端界面",
pos=wx.DefaultPosition, size=(400, 450))
# 创建面板对象
pl = wx.Panel(self)
# 在面板上放置盒子
box = wx.BoxSizer(wx.VERTICAL) # 盒子里的内容垂直布局--->VERTICAL
# 可伸缩的网格布局
fgz1 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建两个按钮--->按钮放在面板上,参数填面板pl
conn_btn = wx.Button(pl, size=(200, 40), label='连接')
dis_conn_btn = wx.Button(pl, size=(200, 40), label='断开')
# 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz1.Add(conn_btn, 1, wx.TOP | wx.LEFT)
fgz1.Add(dis_conn_btn, 1, wx.TOP | wx.RIGHT)
# (可伸缩的网格布局)添加到box中
box.Add(fgz1, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
# (只读文本框)添加到box中
box.Add(self.show_text, 1, wx.ALIGN_CENTER) # 只读文本框在盒子中居中--->ALIGN_CENTER
# 创建聊天内容文本框。放到面板pl中。多行显示--->TE_MULTILINE
self.chat_text = wx.TextCtrl(pl, size=(400, 120), style=wx.TE_MULTILINE)
# (聊天内容文本框)添加到box中
box.Add(self.chat_text, 1, wx.ALIGN_CENTER) # 聊天内容文本框在盒子中居中--->ALIGN_CENTER
# 可伸缩的网格布局
fgz2 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建两个按钮--->按钮放在面板上,参数填面板pl
reset_btn = wx.Button(pl, size=(200, 40), label='重置')
send_btn = wx.Button(pl, size=(200, 40), label='发送')
# 把两个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz2.Add(reset_btn, 1, wx.TOP | wx.LEFT)
fgz2.Add(send_btn, 1, wx.TOP | wx.RIGHT)
# (可伸缩的网格布局)添加到box中
box.Add(fgz2, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 到这里所有内容都在盒子中了,盒子又在面板中
# 将盒子放到面板中
pl.SetSizer(box)
if __name__ == '__main__':
# 初始化App()
app = wx.App()
# 创建自己的客户端界面对象
client = LxlClient('lxl')
client.Show() # 可以改成LxlClient('lxl').Show()
# 循环刷新显示
app.MainLoop()
现在只显示了界面,具体的功能还未实现。
使用wxPython绘制服务器界面
在使用wxPython去布局服务器界面时,是一层一层嵌套的,要把按钮放到可伸缩的网格布局中,可伸缩的网格布局又要放到盒子中,盒子放在面板上,面板放在窗体上。
服务器界面server.py:
# 注意:编码格式要加
# coding:utf-8
import wx # 注意:导入wxPython用的是import wx
class LxlServer(wx.Frame): # 绘制窗体界面,因此要继承父类Frame
def __init__(self):
# 调用父类的初始化方法绘制窗体
'''
None表示没有父级窗口
id表示当前窗口的编号
pos:窗体的打开位置,DefaultPosition:默认位置
size:窗体的大小,单位是像素,400宽,450高
'''
wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
pos=wx.DefaultPosition, size=(400, 450))
# 窗口上放一个面板
pl = wx.Panel(self)
# 在面板上放置盒子
box = wx.BoxSizer(wx.VERTICAL) # 盒子里的内容垂直布局--->VERTICAL
# 可伸缩的网格布局
fgz1 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建三个按钮--->按钮放在面板上,参数填面板pl
start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')
# 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz1.Add(start_server_btn, 1, wx.TOP)
fgz1.Add(record_btn, 1, wx.TOP)
fgz1.Add(stop_server_btn, 1, wx.TOP)
# (可伸缩的网格布局)添加到box中
box.Add(fgz1, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
self.show_text = wx.TextCtrl(pl, size=(400, 210), style=wx.TE_MULTILINE | wx.TE_READONLY)
# (只读文本框)添加到box中
box.Add(self.show_text, 1, wx.ALIGN_CENTER) # 只读文本框在盒子中居中--->ALIGN_CENTER
# 到这里所有内容都在盒子中了,盒子又在面板中
# 将盒子放到面板中
pl.SetSizer(box)
if __name__ == '__main__':
# 初始化App()
app = wx.App()
# 创建自己的服务器界面对象
server = LxlServer()
server.Show()
# 循环刷新显示
app.MainLoop()
现在只显示了界面,具体的功能还未实现。
设置启动服务器的必要属性
server.py:
# 注意:编码格式要加
# coding:utf-8
import wx # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
class LxlServer(wx.Frame): # 绘制窗体界面,因此要继承父类Frame
def __init__(self):
# 调用父类的初始化方法绘制窗体
'''
None表示没有父级窗口
id表示当前窗口的编号
pos:窗体的打开位置,DefaultPosition:默认位置
size:窗体的大小,单位是像素,400宽,450高
'''
wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
pos=wx.DefaultPosition, size=(400, 450))
# 窗口上放一个面板
pl = wx.Panel(self)
# 在面板上放置盒子
box = wx.BoxSizer(wx.VERTICAL) # 盒子里的内容垂直布局--->VERTICAL
# 可伸缩的网格布局
fgz1 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建三个按钮--->按钮放在面板上,参数填面板pl
start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')
# 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz1.Add(start_server_btn, 1, wx.TOP)
fgz1.Add(record_btn, 1, wx.TOP)
fgz1.Add(stop_server_btn, 1, wx.TOP)
# (可伸缩的网格布局)添加到box中
box.Add(fgz1, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
# (只读文本框)添加到box中
box.Add(self.show_text, 1, wx.ALIGN_CENTER) # 只读文本框在盒子中居中--->ALIGN_CENTER
# 到这里所有内容都在盒子中了,盒子又在面板中
# 将盒子放到面板中
pl.SetSizer(box)
'''--------------------------以上代码都是界面的绘制代码--------------------------'''
'''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
self.isOn = False # 存储服务器的启动状态,默认值False,默认没有启动
# 服务器端绑定的IP地址和端口
self.host_port = ('', 8888) # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
# 创建socket对象。(这里采用TCP编程)
self.server_socket = socket(AF_INET, SOCK_STREAM) # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
# 绑定IP地址和端口
self.server_socket.bind(self.host_port)
# 监听
self.server_socket.listen(5)
# 创建一个字典,存储与客户端对话的会话线程
self.session_thread_dict = {
} # key-value {客户端的名称key:会话线程value}
'''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
# Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)
def start_server(self, event): # 绑定事件--->event
print('启动服务的按钮被点击了')
if __name__ == '__main__':
# 初始化App()
app = wx.App()
# 创建自己的服务器界面对象
server = LxlServer()
server.Show()
# 循环刷新显示
app.MainLoop()
服务器端启动服务的功能实现
server.py:
# 注意:编码格式要加
# coding:utf-8
import threading
import wx # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
class LxlServer(wx.Frame): # 绘制窗体界面,因此要继承父类Frame
def __init__(self):
# 调用父类的初始化方法绘制窗体
'''
None表示没有父级窗口
id表示当前窗口的编号
pos:窗体的打开位置,DefaultPosition:默认位置
size:窗体的大小,单位是像素,400宽,450高
'''
wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
pos=wx.DefaultPosition, size=(400, 450))
# 窗口上放一个面板
pl = wx.Panel(self)
# 在面板上放置盒子
box = wx.BoxSizer(wx.VERTICAL) # 盒子里的内容垂直布局--->VERTICAL
# 可伸缩的网格布局
fgz1 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建三个按钮--->按钮放在面板上,参数填面板pl
start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')
# 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz1.Add(start_server_btn, 1, wx.TOP)
fgz1.Add(record_btn, 1, wx.TOP)
fgz1.Add(stop_server_btn, 1, wx.TOP)
# (可伸缩的网格布局)添加到box中
box.Add(fgz1, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
# (只读文本框)添加到box中
box.Add(self.show_text, 1, wx.ALIGN_CENTER) # 只读文本框在盒子中居中--->ALIGN_CENTER
# 到这里所有内容都在盒子中了,盒子又在面板中
# 将盒子放到面板中
pl.SetSizer(box)
'''--------------------------以上代码都是界面的绘制代码--------------------------'''
'''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
self.isOn = False # 存储服务器的启动状态,默认值False,默认没有启动
# 服务器端绑定的IP地址和端口
self.host_port = ('', 8888) # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
# 创建socket对象。(这里采用TCP编程)
self.server_socket = socket(AF_INET, SOCK_STREAM) # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
# 绑定IP地址和端口
self.server_socket.bind(self.host_port)
# 监听
self.server_socket.listen(5)
# 创建一个字典,存储与客户端对话的会话线程
self.session_thread_dict = {
} # key-value {客户端的名称key:会话线程value}
'''--------------------------当鼠标点击“启动服务”按钮时,要执行的操作--------------------------'''
# Bind()是父类的方法。EVT--->事件。第二个参数表示当点击按钮时要执行的操作。第三个参数表示哪个按钮
self.Bind(wx.EVT_BUTTON, self.start_server, start_server_btn)
def start_server(self, event): # 绑定事件--->event
# 判断服务器是否已经启动,只有服务器没有启动时才启动
if not self.isOn: # 等价于 self.isOn==False
# 启动服务器
self.isOn = True
# 创建主线程对象,函数式创建主线程
main_thread = threading.Thread(target=self.do_work)
# 设置为守护线程。其目的是当父线程(窗体界面)执行结束后,子线程也自动关闭
main_thread.daemon = True
# 启动主线程
main_thread.start()
def do_work(self):
# 判断isOn的值
while self.isOn:
# 接收客户端的连接请求
session_socket, client_addr = self.server_socket.accept()
# 客户端发送连接请求之后,发送过来的第一条数据为客户端的名称,客户端的名称去作为字典中的键
user_name = session_socket.recv(1024).decode('utf-8')
# 创建会话线程对象(会话线程较多,这里采用继承式创建线程)
# 第三个参数是服务器对象,这个类本身就是服务器类,因此用这个类的对象self
session_thread = SessionThread(session_socket, user_name, self)
# 将会话线程存储到字典中
self.session_thread_dict[user_name] = session_thread
# 启动会话线程
session_thread.start()
# 当self.isOn的值为False时,关闭socket对象
self.server_socket.close()
# 服务器端会话线程的类
class SessionThread(threading.Thread):
def __init__(self, client_socket, user_name, server):
pass
def run(self):
pass
if __name__ == '__main__':
# 初始化App()
app = wx.App()
# 创建自己的服务器界面对象
server = LxlServer()
server.Show()
# 循环刷新显示
app.MainLoop()
服务器端会话线程代码实现
server.py:
# 注意:编码格式要加
# coding:utf-8
import threading
import wx # 注意:导入wxPython用的是import wx
from socket import socket, AF_INET, SOCK_STREAM # AF_INET-->Internet协议,SOCK_STREAM-->TCP协议
class LxlServer(wx.Frame): # 绘制窗体界面,因此要继承父类Frame
def __init__(self):
# 调用父类的初始化方法绘制窗体
'''
None表示没有父级窗口
id表示当前窗口的编号
pos:窗体的打开位置,DefaultPosition:默认位置
size:窗体的大小,单位是像素,400宽,450高
'''
wx.Frame.__init__(self, None, id=1002, title="lxl的服务器界面",
pos=wx.DefaultPosition, size=(400, 450))
# 窗口上放一个面板
pl = wx.Panel(self)
# 在面板上放置盒子
box = wx.BoxSizer(wx.VERTICAL) # 盒子里的内容垂直布局--->VERTICAL
# 可伸缩的网格布局
fgz1 = wx.FlexGridSizer(wx.HSCROLL) # 内部水平布局--->HSCROLL
# 创建三个按钮--->按钮放在面板上,参数填面板pl
start_server_btn = wx.Button(pl, size=(133, 40), label='启动服务')
record_btn = wx.Button(pl, size=(133, 40), label='保存聊天记录')
stop_server_btn = wx.Button(pl, size=(133, 40), label='停止服务')
# 把三个按钮放到可伸缩的网格布局。第二个参数一般填1
fgz1.Add(start_server_btn, 1, wx.TOP)
fgz1.Add(record_btn, 1, wx.TOP)
fgz1.Add(stop_server_btn, 1, wx.TOP)
# (可伸缩的网格布局)添加到box中
box.Add(fgz1, 1, wx.ALIGN_CENTER) # 可伸缩的网格布局在盒子中居中--->ALIGN_CENTER
# 只读文本框,显示聊天内容。放到面板pl中。多行显示--->TE_MULTILINE;只读--->TE_READONLY
self.show_text = wx.TextCtrl(pl, size=(400, 410), style=wx.TE_MULTILINE | wx.TE_READONLY)
# (只读文本框)添加到box中
box.Add(self.show_text, 1, wx.ALIGN_CENTER) # 只读文本框在盒子中居中--->ALIGN_CENTER
# 到这里所有内容都在盒子中了,盒子又在面板中
# 将盒子放到面板中
pl.SetSizer(box)
'''--------------------------以上代码都是界面的绘制代码--------------------------'''
'''--------------------------以下代码是服务器功能实现的必要属性--------------------------'''
self.isOn = False # 存储服务器的启动状态,默认值False,默认没有启动
# 服务器端绑定的IP地址和端口
self.host_port = ('', 8888) # 空的字符串表示的是本机的所有IP,当然写127.0.0.1也可以
# 创建socket对象。(这里采用TCP编程)
self.server_socket = socket(AF_INET, SOCK_STREAM) # AF_INET-->Internet协议&#