本文是针对最近学习的一个总结,如若文章中有什么问题或不足,麻烦各位能够指出。废话不多说,让我们开始行动。
实现准备:
需要保证有python的运行环境,后续代码需要使用到,socket库,wx库,time库,threading库,在做本次实验前需自行安装。
注:wx库安装不是直接安装wx,而是pip install wxpython
1. 基本构思
首先我们这个多人聊天室是基于服务端与客户端实现的,因此我们需要实现两个代码实现,服务端与客户端。
1.1 服务端的框架
实现步骤:
- 首先规划GUI的基本框架,在这里我们需要两个按钮,以及一个消息栏框架。
- 按钮是帮助我们实现服务器的连接与关闭。
- 消息栏是将客户端发送过来的信息在服务端这里能有一个记录。我们并不能在消息栏进行输入,毕竟消息只是用来看的。
基于上面的图的框架,我们先开始编写服务器的框架代码:
class Server(wx.Frame):
def __init__(self):
# None表示没有父类继承,id表示该框架的标识,DefaultPosition表示默认位置
wx.Frame.__init__(self, None, id=101, title='服务器', pos=wx.DefaultPosition, size=(400, 300))
# 面板,将控件放到面板当中去
pl = wx.Panel(self)
# 设置布局方向,VERTICAL垂直方向
box = wx.BoxSizer(wx.VERTICAL)
# 将布局管理器添加到面板中
# pl.SetSizer(box)
# 创建组件(控件位置),HORIZONTAL默认位置
gl = wx.FlexGridSizer(wx.HORIZONTAL)
start_up = wx.Button(pl, size=(180, 40), label='启动服务')
close_up = wx.Button(pl, size=(180, 40), label='关闭服务')
gl.Add(start_up, 1, wx.TOP)
gl.Add(close_up, 1, wx.TOP)
box.Add(gl, 1, wx.ALIGN_CENTER)
# pl.SetSizer(box)
# 文本控件
self.Interface = wx.TextCtrl(pl, size=(390, 200), style=wx.TE_MULTILINE | wx.TE_READONLY)
box.Add(self.Interface, 2, wx.ALIGN_CENTER)
pl.SetSizer(box)
if __name__ == '__main__':
# 每一个wxpython应用程序都是wx.App的一个实例
app = wx.App()
Server().Show()
# 处理各种事件
app.MainLoop()
通过上述代码我们就可以实现处服务器的一个基本框架:
此时你对按钮进行点击是不会出现任何反应的,因为咱们还没有给按钮进行赋予“灵魂“。
1.2 客户端的框架:
接下来我们开始对客户端的框架进行构思。
实现步骤:
- 首先规划GUI的基本框架,在这里我们需要四个按钮、一个消息栏框架、一个输入栏、
- 在这我们需要一个启动服务器按钮,一个断开服务器按钮,一个清空消息按钮,一个发送消息按钮。
- 消息栏存储每个客户端的聊天信息,我们并不能在消息栏进行输入,毕竟消息只是用来看的。
- 客户端输入的消息都在输入栏中进行。
基于上面的图的框架,我们先开始编写客户端的框架代码:
class Client(wx.Frame):
def __init__(self, k_name):
wx.Frame.__init__(self, None, id=100, title='%s的界面' % k_name, pos=wx.DefaultPosition, size=(400, 500))
# 面板,将控件放到面板当中去
pl = wx.Panel(self)
# 设置布局方向,VERTICAL垂直方向
box = wx.BoxSizer(wx.VERTICAL)
# 将布局管理器添加到面板中
pl.SetSizer(box)
# 创建组件(控件位置),HORIZONTAL默认位置
gl = wx.FlexGridSizer(wx.HORIZONTAL)
connect_server = wx.Button(pl, size=(180, 40), label='连接服务器')
break_server = wx.Button(pl, size=(180, 40), label='断开服务器')
gl.Add(connect_server, 1, wx.TOP | wx.LEFT)
gl.Add(break_server, 1, wx.TOP | wx.RIGHT)
box.Add(gl, 1, wx.ALIGN_CENTER)
pl.SetSizer(box)
# 文本框
self.Interface = wx.TextCtrl(pl, size=(390, 180), style=wx.TE_MULTILINE | wx.TE_READONLY)
box.Add(self.Interface, 2, wx.ALIGN_CENTER)
# 输入框
self.input_data = wx.TextCtrl(pl, size=(390, 100), style=wx.TE_MULTILINE)
box.Add(self.input_data, 2, wx.ALIGN_CENTER)
# 底部按钮
clear = wx.Button(pl, size=(180, 40), label='清空')
send = wx.Button(pl, size=(180, 40), label='发送')
gl2 = wx.FlexGridSizer(wx.HORIZONTAL)
gl2.Add(clear, 1, wx.TOP | wx.LEFT)
gl2.Add(send, 1, wx.TOP | wx.RIGHT)
box.Add(gl2, 1, wx.ALIGN_CENTER)
pl.SetSizer(box)
if __name__ == '__main__':
# 每一个wxpython应用程序都是wx.App的一个实例
app = wx.App()
name = input('请输入客户端的名字:')
Client(name).Show()
# 处理各种事件
app.MainLoop()
通过上述代码我们就可以实现处客户端的一个基本框架:
1.3 设置服务的初始状态:
- 服务器在最开始肯定是关闭的,因此我们对于服务器的最开始的连接状态设置为False。
- 设置服务器的ip以及端口。
- 使用socket模块进行绑定端口,监听客户端连接
- 创建会话字典,用于记录与服务器连接的客户端的会话线程
代码实现:
class Server(wx.Frame):
def __init__(self):
# 定义服务器本身的属性
self.isON = False # 代表服务器是否启动
self.host_port = ('', 8989) # 绑定发ip和端口
self.server_socket = socket(AF_INET, SOCK_STREAM)
self.server_socket.bind(self.host_port)
self.server_socket.listen(10)
# 创建空字典存放服务器会话的线程
self.session_thread_map = {}
1.4 设置客户端的初始状态
- 客户端在初始状态是未连接状态,因此应设置未 False
- 创建两个实例用于存储该客户端的姓名以及与服务端的连接信息,与服务端的连接信息在初始设为 None。
self.name = k_name
self.isConnect = False
self.client_socket = None
2 对于服务器的各个控件赋予“灵魂”
- 定义start_server函数用于封装启动服务器代码,将启动按钮与这个函数进行绑定。
- 定义close_server函数用于封装关闭服务器代码,将关闭服务器按钮与这个函数进行绑定。
- 定义 show_info_and_client 函数用于封装客户端之间的交流信息的代码
# 注入灵魂
self.Bind(wx.EVT_BUTTON, self.start_server, start_up)
self.Bind(wx.EVT_BUTTON, self.close_server, close_up)
2.1 start_server 函数处理:
- 服务开启此时的服务器的状态会发生变化,此时的服务器是True
- 构建工作线程的函数 do_work 用于服务端开始与客户端进行信息传递
代码构建:
# 启动服务器
def start_server(self, asd):
print('服务正在开启.....')
if not self.isON:
self.isON = True
main_thread = threading.Thread(target=self.do_work)
# 守护线程,当主线程关闭时,子线程立刻断开,子线程断开不影响主线程
main_thread.setDaemon = True
main_thread.start()
不知道为什么我在运行这个函数的时候解释器提醒我需要在里面多加一个参数,这个是我们的自定义函数,加一下也没什么,要是有明白的朋友麻烦告知一下。
此时咱对服务端的启动服务器按钮进行点击它就有反应了,它已经不再只是一个空虚的躯干了
2.2 close_server 函数处理:
- 将服务端的状态设为 False 关闭
- 关闭服务端套接字
代码构建:
# 关闭服务器
def close_server(self, abc) :
self.isON = False
print('服务器已关闭')
self.server_socket.close()
此时咱对服务端的启动服务器按钮进行点击它也有反应啦。
2.3 show_info_and_client 函数处理:
消息栏存储的是各个客户端之间的聊天信息,其实它最主要的功能是将客户端发送给服务器的消息发送给每一个客户端,这是为啥勒?
大家可以想考一下我们的聊天室是所有的消息都会在一个界面中,我们客户端发送消息的时候,其他人要是想要收到是不是得发送消息的客户端再发一份给这个客户端,但是如果这个客户端没有其他客户端的好友是不是很尴尬,岂不是尬聊,因此我们需要服务器这个拥有所有客户端的好友来做这件发送消息给每个客户端的事。
既然逻辑咱们说清楚了,接下来我们开始实现代码:
# 在文本框中显示聊天信息,同时将我们发送的信息展示到所有的客户端
# source 记录客户端的信息,data,该客户端发送的信息,data_time该客户端发送信息的时间
def show_info_and_client(self, source, data, data_time):
send_data = '时间: %s\n%s: %s\n' % (data_time, source, data)
# 服务器文本框
self.Interface.AppendText('-------------------------\n%s' % send_data)
# session_thread_map 存储每一个客户端与服务器之间的信息
for client in self.session_thread_map.values():
if client.isON:
client.user_socket.send(send_data.encode('utf-8'))
3 主线程 do_work 函数的处理
- 主线程函数应该放入一个循环当中,因为我们的聊天室不一定只有一个人在里面
- 定义一个会话线程类,将客户端的发送信息存储到之前定义的会话线程字典当中
- 客户端每次连接成功我们都得把连接成功的信息发送各个客户端以及存储到消息栏中
代码构建:
# 定义主线程的启动函数
def do_work(self):
if self.isON:
print('服务器正常工作')
while self.isON:
# 接收客户端的消息
session_socket, client_addr = self.server_socket.accept()
# 服务端接收客户端发送的第一条信息,就是客户端的姓名,因为客户端我们设置发送给服务端的第一条信息就是客户端的名字
user_name = session_socket.recv(1024).decode()
# 创建会话线程 后面的self其实就是将类server里面的实例以及方法都传递给类Session_Thread
session_thread = Session_Thread(session_socket,user_name,self)
# 键:客户,值:消息
self.session_thread_map[user_name] = session_thread
session_thread.start()
# 客户端进入聊天室
self.show_info_and_client(f'通知',f'欢迎{user_name}进入聊天室',time.strftime('%Y-%m-%d %H:%M:%S',time.localtime()))
3.1 服务端的会话线程类构建:
- 作为线程类首先得继承线程类的初始化方法
- 需要获取客户端的姓名,客户端与服务端的连接信息,以及服务端类的所有实例和方法(这样我们就不用重新构建方法了)
- 线程类需要放入一个循环当中,因为客户端随时都会向服务端发送消息
代码构建:
# 服务端的会话线程类
class Session_Thread(threading.Thread):
def __init__(self, user_socket, user_name, server):
threading.Thread.__init__(self)
self.user_socket = user_socket
self.user_name = user_name
self.server = server
self.isON = True
def run(self):
print(f'客户端{self.user_name}已经连接成功')
while self.isON:
data = self.user_socket.recv(1024).decode('utf-8')
if data == 'disconnect':
self.isON = False
# 给服务端发送消息,有人离开
print(f'客户端{self.user_name}断开服务器')
self.server.show_info_and_client(self.user_name, '退出服务器-----',
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
else:
self.server.show_info_and_client(self.user_name, data,
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
self.user_socket.close()
由此我们的服务端就构建好了。
4 对于客户端的各个控件赋予“灵魂”
- 定义 connect_to_server 函数用于封装客户端连接服务器代码,将连接服务器的按钮与这个函数进行绑定。
- 定义 break_to_server 函数用于封装客户端断开服务器代码,将断开服务器按钮与这个函数进行绑定。
- 定义 send_to_data 函数用于封装客户端发送给服务器信息的代码
- 定义 clear_to_data 函数用于清空客户端输入服务端未发送信息的代码
self.Bind(wx.EVT_BUTTON, self.connect_to_server, connect_server)
self.Bind(wx.EVT_BUTTON, self.break_to_server, break_server)
self.Bind(wx.EVT_BUTTON, self.send_to_data, send)
self.Bind(wx.EVT_BUTTON, self.clear_to_data, clear)
4.1 connect_to_server 函数处理:
- 客户端连接到服务器之后的连接状态变为 True
- 设置客户端的IP以及端口,由于咱这是单机的,因此IP就是我们的本机IP
- 连接成功后,将客户端的姓名发送给服务端,然后服务端就会发生do_work里面的操作
- 将接收信息的函数作为其子线程,因为客户端会一直处于接收信息的状态(除非连接状态为Flase)
代码构建:
def connect_to_server(self, abc):
print(f'客户端{self.name}开始连接服务器')
if not self.isConnect:
# 端口需要与服务器的端口一致
server_host_port = ('localhost', 8989)
self.client_socket = socket(AF_INET, SOCK_STREAM)
# 指定服务器的端口和地址
self.client_socket.connect(server_host_port)
self.client_socket.send(self.name.encode('utf-8'))
self.isConnect = True
client_thread = threading.Thread(target=self.recv_data)
# 设置守护线程
client_thread.setDaemon = True
self.isConnect = True
client_thread.start()
4.2 break_to_server 函数处理:
- 设置客户端断开服务器需要向服务器输入 disconnect 之后才能断掉
- 将连接状态设置为 False
代码构建:
def break_to_server(self,aaa):
self.client_socket.send('disconnect'.encode('utf-8'))
self.isConnect = False
4.3 send_to_data 函数处理:
- 我们是通过在输入框中输入点击发送后即可完成输入信息操作
- 在输入框中获取到输入的值
- 判断其是否为空,如果不为则将该值发送给服务器,并清空输入框
代码构建:
def send_to_data(self,aaa):
if self.isConnect:
# 在输入框里面获取值
info = self.input_data.GetValue()
if info != '':
self.client_socket.send(info.encode('utf-8'))
# 发送完消息后清空输入框
self.input_data.Clear()
4.4 clear_to_data 函数处理:
- 直接将输入框的数据清空即可
代码构建:
# 清空还未发送的消息
def clear_to_data(self,aaa):
self.input_data.Clear()
5 对于客户端接收服务端信息的进程函数处理
- 判断此时客户端是否连接服务器
- 获取服务器发送的信息,并保存到消息框中
代码构建:
def recv_data(self):
while self.isConnect:
data = self.client_socket.recv(1024).decode('utf-8')
# 显示文本框
self.Interface.AppendText('%s\n' % data)
由此我们的客户端也搭建完成。
6 服务器代码展示
# -*- coding:utf-8 -*-
from socket import *
import wx
import time
import threading
class Server(wx.Frame):
def __init__(self):
# None表示没有父类继承,id表示该框架的标识,DefaultPosition表示默认位置
wx.Frame.__init__(self, None, id=101, title='服务器', pos=wx.DefaultPosition, size=(400, 300))
# 面板,将控件放到面板当中去
pl = wx.Panel(self)
# 设置布局方向,VERTICAL垂直方向
box = wx.BoxSizer(wx.VERTICAL)
# 将布局管理器添加到面板中
# pl.SetSizer(box)
# 创建组件(控件位置),HORIZONTAL默认位置
gl = wx.FlexGridSizer(wx.HORIZONTAL)
start_up = wx.Button(pl, size=(180, 40), label='启动服务')
close_up = wx.Button(pl, size=(180, 40), label='关闭服务')
gl.Add(start_up, 1, wx.TOP)
gl.Add(close_up, 1, wx.TOP)
box.Add(gl, 1, wx.ALIGN_CENTER)
# pl.SetSizer(box)
# 文本控件
self.Interface = wx.TextCtrl(pl, size=(390, 200), style=wx.TE_MULTILINE | wx.TE_READONLY)
box.Add(self.Interface, 2, wx.ALIGN_CENTER)
pl.SetSizer(box)
# 定义服务器本身的属性
self.isON = False # 代表服务器是否启动
self.host_port = ('', 8989) # 绑定发ip和端口
self.server_socket = socket(AF_INET, SOCK_STREAM)
self.server_socket.bind(self.host_port)
self.server_socket.listen(10)
# 创建空字典存放服务器会话的线程
self.session_thread_map = {}
# 创建会话的线程
# 注入灵魂
self.Bind(wx.EVT_BUTTON, self.start_server, start_up)
self.Bind(wx.EVT_BUTTON, self.close_server, close_up)
# 启动服务器
def start_server(self, asd):
print('服务正在开启.....')
if not self.isON:
self.isON = True
main_thread = threading.Thread(target=self.do_work)
# 守护线程,当主线程关闭时,子线程立刻断开,子线程断开不影响主线程
main_thread.setDaemon = True
main_thread.start()
# 关闭服务器
def close_server(self, abc):
self.isON = False
print('服务器已关闭')
self.server_socket.close()
# 定义主线程的启动函数
def do_work(self):
if self.isON:
print('服务器正常工作')
while self.isON:
# 接收客户端的消息
session_socket, client_addr = self.server_socket.accept()
# 服务端接收客户端发送的第一条信息,就是客户端的姓名,因为客户端我们设置发送给服务端的第一条信息就是客户端的名字
user_name = session_socket.recv(1024).decode()
# 创建会话线程 后面的self其实就是将类server里面的实例以及方法都传递给类Session_Thread
session_thread = Session_Thread(session_socket, user_name, self)
# 键:客户,值:消息
self.session_thread_map[user_name] = session_thread
session_thread.start()
# 客户端进入聊天室
self.show_info_and_client(f'通知', f'欢迎{user_name}进入聊天室',
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
# 在文本框中显示聊天信息,同时将我们发送的信息展示到所有的客户端
# source 记录客户端的信息,data,该客户端发送的信息,data_time该客户端发送信息的时间
def show_info_and_client(self, source, data, data_time):
send_data = '时间: %s\n%s: %s\n' % (data_time, source, data)
# 服务器文本框
self.Interface.AppendText('-------------------------\n%s' % send_data)
# session_thread_map 存储每一个客户端与服务器之间的信息
for client in self.session_thread_map.values():
if client.isON:
client.user_socket.send(send_data.encode('utf-8'))
# 服务端的会话线程类
class Session_Thread(threading.Thread):
def __init__(self, user_socket, user_name, server):
threading.Thread.__init__(self)
self.user_socket = user_socket
self.user_name = user_name
self.server = server
self.isON = True
def run(self):
print(f'客户端{self.user_name}已经连接成功')
while self.isON:
data = self.user_socket.recv(1024).decode('utf-8')
if data == 'disconnect':
self.isON = False
# 给服务端发送消息,有人离开
print(f'客户端{self.user_name}断开服务器')
self.server.show_info_and_client(self.user_name, '退出服务器-----',
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
else:
self.server.show_info_and_client(self.user_name, data,
time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))
self.user_socket.close()
if __name__ == '__main__':
# 每一个wxpython应用程序都是wx.App的一个实例
app = wx.App()
Server().Show()
# 处理各种事件
app.MainLoop()
7 客户端代码展示
# -*- coding:utf-8 -*-
from socket import *
import wx
import time
import threading
class Client(wx.Frame):
def __init__(self, k_name):
wx.Frame.__init__(self, None, id=100, title='%s的界面' % k_name, pos=wx.DefaultPosition, size=(400, 500))
# 面板,将控件放到面板当中去
pl = wx.Panel(self)
# 设置布局方向,VERTICAL垂直方向
box = wx.BoxSizer(wx.VERTICAL)
# 将布局管理器添加到面板中
pl.SetSizer(box)
# 创建组件(控件位置),HORIZONTAL默认位置
gl = wx.FlexGridSizer(wx.HORIZONTAL)
connect_server = wx.Button(pl, size=(180, 40), label='连接服务器')
break_server = wx.Button(pl, size=(180, 40), label='断开服务器')
gl.Add(connect_server, 1, wx.TOP | wx.LEFT)
gl.Add(break_server, 1, wx.TOP | wx.RIGHT)
box.Add(gl, 1, wx.ALIGN_CENTER)
pl.SetSizer(box)
# 文本框
self.Interface = wx.TextCtrl(pl, size=(390, 180), style=wx.TE_MULTILINE | wx.TE_READONLY)
box.Add(self.Interface, 2, wx.ALIGN_CENTER)
# 输入框
self.input_data = wx.TextCtrl(pl, size=(390, 100), style=wx.TE_MULTILINE)
box.Add(self.input_data, 2, wx.ALIGN_CENTER)
# 底部按钮
clear = wx.Button(pl, size=(180, 40), label='清空')
send = wx.Button(pl, size=(180, 40), label='发送')
gl2 = wx.FlexGridSizer(wx.HORIZONTAL)
gl2.Add(clear, 1, wx.TOP | wx.LEFT)
gl2.Add(send, 1, wx.TOP | wx.RIGHT)
box.Add(gl2, 1, wx.ALIGN_CENTER)
pl.SetSizer(box)
self.Bind(wx.EVT_BUTTON, self.connect_to_server, connect_server)
self.Bind(wx.EVT_BUTTON, self.break_to_server, break_server)
self.Bind(wx.EVT_BUTTON, self.send_to_data, send)
self.Bind(wx.EVT_BUTTON, self.clear_to_data, clear)
self.name = k_name
self.isConnect = False
self.client_socket = None
def connect_to_server(self, abc):
print(f'客户端{self.name}开始连接服务器')
if not self.isConnect:
# 端口需要与服务器的端口一致
server_host_port = ('localhost', 8989)
self.client_socket = socket(AF_INET, SOCK_STREAM)
# 指定服务器的端口和地址
self.client_socket.connect(server_host_port)
self.client_socket.send(self.name.encode('utf-8'))
self.isConnect = True
client_thread = threading.Thread(target=self.recv_data)
# 设置守护线程
client_thread.setDaemon = True
self.isConnect = True
client_thread.start()
# 接收服务器发送过来的聊天数据
def recv_data(self):
while self.isConnect:
data = self.client_socket.recv(1024).decode('utf-8')
# 显示文本框
self.Interface.AppendText('%s\n' % data)
# 通过发送消息断开服务端的连接
def break_to_server(self,aaa):
self.client_socket.send('disconnect'.encode('utf-8'))
self.isConnect = False
def send_to_data(self,aaa):
if self.isConnect:
# 在输入框里面获取值
info = self.input_data.GetValue()
if info != '':
self.client_socket.send(info.encode('utf-8'))
# 发送完消息后清空输入框
self.input_data.Clear()
# 清空还未发送的消息
def clear_to_data(self,aaa):
self.input_data.Clear()
if __name__ == '__main__':
# 每一个wxpython应用程序都是wx.App的一个实例
app = wx.App()
name = input('请输入客户端的名字:')
Client(name).Show()
# 处理各种事件
app.MainLoop()
8 运行效果展示
先运行服务端,点击启动服务
运行 客户端,在解释器下输入客户端的姓名
点击连接服务器
客户端在输入框中输入信息后点击发送
设置多人聊天(Pycharm专业版操作)
点击Allow parallel run,然后点击Apply,最后点击ok
最后再次运行客户端代码,重复上述操作即可得到新的客户端
设置多人聊天(大众版)
由于pycharm社区版并不支持同时运行同一个代码块,而我们又不愿意多创建几个代码文件,我们可以在 cmd 中运行我们的代码就可以解决这个问题了,cd到你的代码目录,然后输入python+代码文件名
这样也可以完成。