前言
上一篇文章socket网络程序设计实验三中,我们用socket实现了客户与服务器一对一聊天的功能,也就是客户与服务器之间的一对一通信,这次我们来尝试一下服务器与多个客户的并发通信,实现一个类似聊天室的小程序。这一篇也会讲的详细一些,把能想到的功能尽量完善,把能想到的bug都解决掉。总之这是一篇比较干的,干货。
老规矩先放图!
一、实验目的
- 掌握 socket编程 的基本模式,了解客户端与服务器端的主要工作;
- 进一步巩固 PyQt5 的设计和使用,完善一些友好的交互式小细节;
- 练习多线程编程的用法,明白什么时候需要用到多线程。
二、实验环境
不同环境配置的实现其实也大同小异,以下是我的实验环境:
- Windows10_64
- pycharm(安装了Qt Designer和Py UIC工具)
- python3.7.1
- PyQt5
三、实验内容
(一)设计界面
打开pycharm,通过顶部的Tools -> External tools -> Qt Designer,打开Qt Designer,(没有安装Qt Designer的百度自行安装一下)
ps:为什么要用Qt Designer?
因为用它设计界面直观可见,可以很方便的调整控件的大小位置以及属性等,搭配Py UIC工具自动生成代码,十分方便。
Ok,打开之后是这样的:
我们选择第三个Dialog Without Button就好,然后点Create创建我们的主窗口
现在开始设计我们的界面,要设计服务器端和客户端两个界面,我的是这么摆的:
服务器端界面:
客户端界面:
简单明了呀
这里会发现有的按钮和输入框是灰色的,也就是禁用状态,选择这个控件,在右边的属性里找到一个enabled选项,把勾去掉就是灰色的了,也可以生成代码了之后在代码中改,待会儿会讲的。
完了之后保存就好,保存的话养成改名的好习惯,服务器的就叫server_5.ui,客户端的就叫client_5.ui。
(二)生成代码
在pycharm左边project中找到我们刚才保存的server_5.ui和client_5.ui文件,右键一下,还是External Tools -> Py UIC,我们会发现同目录下已经自动生成了我们最爱的.py文件了,名字都是一样的
Ok,打开它们,补充代码
在开头加上需要导入的库:
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.Qt import *
在最后加上main:
if __name__ == '__main__':
app = QApplication(sys.argv)
MainWindow = QMainWindow()
ui = Ui_Dialog()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
至此,程序就可以运行了
但是目前还没有任何功能,我们运行的只是个空的界面而已,要实现我们想要的功能还需要添加一些代码。
(三)主要功能实现
服务器端
有了上一篇文章的经验,这里其实很easy了,主要的区别就是这次我们要同时处理多个客户端的连接请求,所以涉及到多线程,每当收到连接请求时,都要新建一个进程,来持续服务
话不多说,一步一步来,先用init初始化方法加一些我们要用的变量:
def __init__(self):
self.t2 = [] # 用来存放线程的列表
self.s = socket.socket() # 服务器socket
self.c = [] # 客户端socket列表
self.count = 0 # 用来计数正在连接的客户
self.msg = '' # 需要显示的消息
self.flag = False # 服务器的开启状态
然后定义我们的监听按钮的方法:
def listen_button(self): # 监听按钮
add1 = '127.0.0.1'
add2 = self.lineEdit.text()
self.s = socket.socket()
self.s.bind((add1, int(add2)))
self.s.listen()
print('正在监听。。。')
self.msg = '服务器开始运行~\n' + '*'*53 + '\n'
self.textBrowser.setText(self.msg)
t1 = threading.Thread(target=self.accept_socket)
t1.start() # 启动线程
self.pushButton.setEnabled(False) # 点击监听按钮后该按钮将不可点击
self.pushButton_2.setEnabled(True) # 断开按钮设为可用
self.flag = True # 服务器开启
这里self.pushButton.setEnabled(False) 再点击“监听”按钮后直接设置“监听”按钮不能被点击,就不会出现重复点击的异常了,同理“断开”按钮也是:
def break_button(self): # 断开按钮
if self.count > 0:
for cc in self.c:
if cc != 0: # 因为后面断开的客户端socket将它置为0了
cc.send('{}服务器已断开!\n'.format(time.ctime()).encode('GB2312'))
cc.close()
self.c.clear()
self.s.close()
self.pushButton.setEnabled(True) # 点击断开按钮后,监听按钮可用
self.pushButton_2.setEnabled(False)
self.flag = False
定义监听按钮listen_button()中用到的接收连接accept_socket()方法:
def accept_socket(self): # 接收socket连接
try:
while 1:
print('等待连接中。。。')
c_, addr = self.s.accept()
self.c.append(c_) # 将c_即客户端socket存入列表
self.count += 1 # 计数+1
print(addr, '已连接')
t2_ = threading.Thread(target=self.rec_msg, args=(len(self.c)-1,))
self.t2.append(t2_)
t2_.start() # 启动线程
except:
pass
定义接收消息方法:
def rec_msg(self, i): # 接受信息
try:
while 1:
print('等待接受消息中。。。')
msg = self.c[i].recv(1024).decode('GB2312')
print('收到消息', msg)
if '进入聊天室~' in msg: # 服务器端不会显示客户聊天的内容,只显示进入的用户
self.msg += msg
for step, x in enumerate(self.c): # 当收到客户消息时,转发给其他所有在线客户
if step != i:
x.send(msg.encode('GB2312'))
if '退出聊天室' in msg:
self.msg += msg
self.count -= 1
self.c[i] = 0
# self.t2[i]._stop() # 线程不能强制退出,所以下面用break正常结束
print('结束', len(self.t2))
break
if self.flag is False:
break # 如果服务器停止,则退出循环
except ConnectionAbortedError as e1:
print(e1)
注意这里的方法要和客户端联系起来,这里我预想的是客户端连接后会发送一条“xxx进入聊天室”的消息给服务器,断开连接前会发送一条"xxx退出聊天室"的消息给服务器,所以这样。
再把按钮和我们定义的方法关联起来,在retranslateUi()的后面加上:
self.pushButton.clicked.connect(self.listen_button)
self.pushButton_2.clicked.connect(self.break_button)
self.pushButton_2.setEnabled(False) # 初始化未监听时断开按钮不可点击
还有上一篇中一样的,实时刷新需要用到的,在类内定义一个显示信息的方法:
def handleDisplay(self): # 显示收到的信息
self.textBrowser.setText(self.msg)
self.label_2.setText('在线人数:{}'.format(self.count))
self.textBrowser.moveCursor(self.textBrowser.textCursor().End) # 文本框拉到底部
在类外新建一个自定义类:
class BackendThread(QObject): # 用来实时更新显示收到的消息
# 通过类成员对象定义信号
update_date = pyqtSignal(str)
def run(self):
while True:
self.update_date.emit('')
time.sleep(1)
最后同样也是在retranslateUi()方法下加上:
# 调用实时刷新显示消息自定义类
self.backend = BackendThread()
self.backend.update_date.connect(self.handleDisplay) # 连接信号事件
self.thread = QThread() # 创建进程
self.backend.moveToThread(self.thread)
self.thread.started.connect(self.backend.run)
self.thread.start()
Ok,服务器端就完成了
客户端
大多数事情都交给服务器端处理了,客户端要处理的事情就没有那么复杂,只需要请求连接,接受和发送消息并实时显示出来就好了
先来定义连接按钮的方法:
def connect_button(self): # 连接按钮
try:
name = self.lineEdit.text()
add1 = self.lineEdit_2.text()
add2 = self.lineEdit_3.text()
self.s = socket.socket()
self.s.connect((add1, int(add2)))
print('已连接到服务器')
self.msg += '连接成功~\n您已进入聊天室\n'+'*'*74+'\n'
self.textBrowser.setText(self.msg)
self.s.send('[{}]{}进入聊天室~\n'.format(time.ctime(), name).encode('GB2312'))
t = threading.Thread(target=self.rec_msg)
t.start()
self.pushButton.setEnabled(True) # 连接后可以断开
self.pushButton_2.setEnabled(False) # 连接后不可以再连接
self.pushButton_3.setEnabled(True) # 连接后才能发送
self.lineEdit.setEnabled(False) # 连接后不可以修改用户名
except:
self.msg += '服务器未开放!\n'
print('服务器未开放!')
这里添加了异常处理,如果服务器没有开放,会提示服务器未开放,防止闪退。
这里创建的线程还用到了rec_msg()方法用来接收消息:
def rec_msg(self): # 接受消息
try:
while 1:
print('等待接受消息中。。。')
msg = self.s.recv(1024).decode('GB2312')
print('收到消息', msg)
self.msg += msg
print(self.msg)
except ConnectionAbortedError as e1:
print(e1)
except ConnectionResetError as e2: # 这里是捕获服务器端强行断开异常
print(e2)
self.pushButton.setEnabled(False)
self.pushButton_2.setEnabled(True)
self.pushButton_3.setEnabled(False)
再定义断开按钮:
def break_button(self): # 断开按钮
name = self.lineEdit.text()
self.s.send('[{}]{}退出聊天室\n'.format(time.ctime(), name).encode('GB2312'))
self.s.close()
print('已关闭服务器')
self.msg += '您已退出登录\n'
self.pushButton.setEnabled(False) # 断开后不能再断开
self.pushButton_2.setEnabled(True) # 断开后可以连接
self.pushButton_3.setEnabled(False) # 断开后不能发送
self.lineEdit.setEnabled(True) # 断开后可以修改用户名
断开前发送消息给服务器端,以便进行服务器端的处理工作
定义发送按钮:
def send_button(self): # 发送按钮
name = self.lineEdit.text()
MSG = '[' + time.ctime() + ']' + name + ':' + self.lineEdit_4.text() + '\n'
self.s.send(MSG.encode('GB2312'))
print('已发送:', MSG)
self.msg += MSG
self.lineEdit_4.setText('')
还有实时显示的部分,定义显示收到的信息:
def handleDisplay(self, data): # 显示收到的消息
self.textBrowser.setText(self.msg)
self.textBrowser.moveCursor(self.textBrowser.textCursor().End) # 文本框显示到底部
导入server代码中的自定义的类:
from test5.server_5 import BackendThread
再在retranslateUi方法下加入:
self.pushButton.clicked.connect(self.break_button)
self.pushButton_2.clicked.connect(self.connect_button)
self.pushButton_3.clicked.connect(self.send_button)
self.pushButton.setEnabled(False)
self.pushButton_3.setEnabled(False)
# 调用自定义类,同server_4
self.backend = BackendThread()
self.backend.update_date.connect(self.handleDisplay)
self.thread = QThread() # 创建进程
self.backend.moveToThread(self.thread)
self.thread.started.connect(self.backend.run)
self.thread.start()
至此,客户端也大功告成了!
然后我们可以直接copy,parse一下,搞出好几个客户端来,在retranslateUi下改一下名字:
self.lineEdit.setText(_translate("Dialog", "Mike"))
self.lineEdit.setText(_translate("Dialog", "LiePy"))
self.lineEdit.setText(_translate("Dialog", "Eric"))
…
一个人扮演N个人开始吹。。,不,调试一下看看还有没有什么bug或者异常之类的没有考虑到的
嗯,暂时我是没有发现新的bug了,至于功能方面,其实还有很多地方可以完善,比如聊天记录的保存啊、读取啊,消息界面的清空啊、发送图片啊、视频啊,发送文件之类的,有想一起学习的就评论我,私聊我呀,人生苦短,我们一起学python
Ok,这篇文章就到这里吧
pass~
ps:源代码已上传至我的github:https://github.com/LiePy/socket_test.git