socket网络程序设计实验三,服务器与多个客户的并发通信(基于python3.7、PyQt5)

前言

上一篇文章socket网络程序设计实验三中,我们用socket实现了客户与服务器一对一聊天的功能,也就是客户与服务器之间的一对一通信,这次我们来尝试一下服务器与多个客户的并发通信,实现一个类似聊天室的小程序。这一篇也会讲的详细一些,把能想到的功能尽量完善,把能想到的bug都解决掉。总之这是一篇比较干的,干货。

老规矩先放图!
在这里插入图片描述

一、实验目的

  1. 掌握 socket编程 的基本模式,了解客户端与服务器端的主要工作;
  2. 进一步巩固 PyQt5 的设计和使用,完善一些友好的交互式小细节;
  3. 练习多线程编程的用法,明白什么时候需要用到多线程。

二、实验环境

不同环境配置的实现其实也大同小异,以下是我的实验环境:

  • 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,打开之后是这样的:
1
我们选择第三个Dialog Without Button就好,然后点Create创建我们的主窗口

现在开始设计我们的界面,要设计服务器端和客户端两个界面,我的是这么摆的:
服务器端界面:
2
客户端界面:
3
简单明了呀
这里会发现有的按钮和输入框是灰色的,也就是禁用状态,选择这个控件,在右边的属性里找到一个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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值