PyQt5+ Python3 多线程通信

 关于PyQt5+Python3开发环境安装可以参考上篇。这里实现的是一种群聊工具,socket类使用的是Qt的TcpSocket, TcpServer 类;线程类使用的QTread;收发数据使用的QDataStream(当然也可以用其他的方式,不唯一)

先看服务器端, 服务端界面设计,下面标注红色的是后面代码中要使用的控件,其它的可要可不要   # Ui_server.py

# -*- coding: utf-8 -*-
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(541, 355)
        self.splitter_2 = QtWidgets.QSplitter(Form)
        self.splitter_2.setGeometry(QtCore.QRect(10, 10, 516, 331))
        self.splitter_2.setOrientation(QtCore.Qt.Vertical)
        self.splitter_2.setObjectName("splitter_2")
        self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.label = QtWidgets.QLabel(self.layoutWidget)
        self.label.setObjectName("label")
        self.horizontalLayout.addWidget(self.label)
        self.txtPort = QtWidgets.QLineEdit(self.layoutWidget)
        self.txtPort.setObjectName("txtPort")
        self.horizontalLayout.addWidget(self.txtPort)
        self.btnCreate = QtWidgets.QPushButton(self.layoutWidget)
        self.btnCreate.setObjectName("btnCreate")
        self.horizontalLayout.addWidget(self.btnCreate)
        self.splitter = QtWidgets.QSplitter(self.splitter_2)
        self.splitter.setOrientation(QtCore.Qt.Horizontal)
        self.splitter.setObjectName("splitter")
        self.layoutWidget1 = QtWidgets.QWidget(self.splitter)
        self.layoutWidget1.setObjectName("layoutWidget1")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.lbUserInfo = QtWidgets.QLabel(self.layoutWidget1)
        self.lbUserInfo.setObjectName("lbUserInfo")
        self.verticalLayout.addWidget(self.lbUserInfo)
        self.listUser = QtWidgets.QListWidget(self.layoutWidget1)
        self.listUser.setObjectName("listUser")
        self.verticalLayout.addWidget(self.listUser)
        self.layoutWidget2 = QtWidgets.QWidget(self.splitter)
        self.layoutWidget2.setObjectName("layoutWidget2")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget2)
        self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.chatInfo = QtWidgets.QLabel(self.layoutWidget2)
        self.chatInfo.setObjectName("chatInfo")
        self.verticalLayout_2.addWidget(self.chatInfo)
        self.browerChat = QtWidgets.QTextBrowser(self.layoutWidget2)
        self.browerChat.setObjectName("browerChat")
        self.verticalLayout_2.addWidget(self.browerChat)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "ADDRESS:PORT:"))
        self.btnCreate.setText(_translate("Form", "create"))
        self.lbUserInfo.setText(_translate("Form", "用户列表"))
        self.chatInfo.setText(_translate("Form", "聊天信息"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())
        self.btnCreate = QtWidgets.QPushButton(self.layoutWidget)
        self.btnCreate.setObjectName("btnCreate")
        self.horizontalLayout.addWidget(self.btnCreate)
        self.splitter = QtWidgets.QSplitter(self.splitter_2)
        self.splitter.setOrientation(QtCore.Qt.Horizontal)
        self.splitter.setObjectName("splitter")
        self.layoutWidget1 = QtWidgets.QWidget(self.splitter)
        self.layoutWidget1.setObjectName("layoutWidget1")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1)
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout.setObjectName("verticalLayout")
        self.lbUserInfo = QtWidgets.QLabel(self.layoutWidget1)
        self.lbUserInfo.setObjectName("lbUserInfo")
        self.verticalLayout.addWidget(self.lbUserInfo)
        self.listUser = QtWidgets.QListWidget(self.layoutWidget1)
        self.listUser.setObjectName("listUser")
        self.verticalLayout.addWidget(self.listUser)
        self.layoutWidget2 = QtWidgets.QWidget(self.splitter)
        self.layoutWidget2.setObjectName("layoutWidget2")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget2)
        self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.chatInfo = QtWidgets.QLabel(self.layoutWidget2)
        self.chatInfo.setObjectName("chatInfo")
        self.verticalLayout_2.addWidget(self.chatInfo)
        self.browerChat = QtWidgets.QTextBrowser(self.layoutWidget2)
        self.browerChat.setObjectName("browerChat")
        self.verticalLayout_2.addWidget(self.browerChat)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "ADDRESS:PORT:"))
        self.btnCreate.setText(_translate("Form", "create"))
        self.lbUserInfo.setText(_translate("Form", "用户列表"))
        self.chatInfo.setText(_translate("Form", "聊天信息"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

服务器端实现:

 

 在服务器端,使用QTcpServer类监听接入客户端,对于QTcpServer类,一旦有用户接入,就调用

其incomingConnection函数 。要使用多线程的话,就要在incomingConnection中调用线程类QThread,生成新的线程,

然后再新的线程run函数中调用QTcpSocket实例,进行数据的收发,在这里我也把QTcpSocket类重新实现了,也可以就在

重载的Thread实现QTcpSocket收发的代码,代码如下:

 

# -*- coding: utf-8 -*-

"""
Module implementing Server.
"""
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtNetwork import *
from Ui_server import Ui_Form
import sys

PORT =26711
SIZEOF_UINT16 = 2

class TcpSocket(QTcpSocket):
    '''
    实现了客户端读取信息与发送信息到客户端的功能
    '''
    #信号用于Thread类中,信号在接受客户端信息后发送,一直发送到Server更新其界面
    #上级为Thead, 先将其交付给连接Thread中的slotRecv
    signRecv= pyqtSignal(str,  str)
    def __init__(self, socketId, parent=None):
        super(TcpSocket, self).__init__(parent)
        self.socketId = socketId  
        #客户端发送信息就接受
        self.readyRead.connect(self.slotRecv)
    
    #接受信息槽函数, client-> slotRecv -> Server
    def slotRecv(self):
        #使用QDataStream接受信息,也可以选择其他接受方式
        while self.state() == QAbstractSocket.ConnectedState:
            nextBlockSize = 0
            stream = QDataStream(self)
            if self.bytesAvailable() >= SIZEOF_UINT16:
                nextBlockSize = stream.readUInt16()
            else: 
                #print('cannot read client')
                break
            if self.bytesAvailable() < nextBlockSize:
               break
            action = ''
            msg = ''
            action = stream.readQString()
            msg = stream.readQString()
            clientAddress = self.peerAddress().toString()  
            clientPort = str(self.peerPort())
            msg= clientAddress+ ':'+clientPort + ' '+ msg+'\n'
            
            #发射给上级Thread
            self.signRecv.emit(action,  msg)
            
    #发送信息槽函数,其变量数据来源于上级   Server -> slotSend -> client 
    def slotSend(self,  action,  msg,  id):    
        #print(id)
        #print(int(self.socketId))
        if id == int(self.socketId):
            reply = QByteArray()
            stream = QDataStream(reply,  QIODevice.WriteOnly)
            stream.writeUInt16(0)
            stream.writeQString(action)
            stream.writeQString(msg)
            stream.device().seek(0)
            stream.writeUInt16(reply.size() - SIZEOF_UINT16)
            self.write(reply)

class Thread(QThread):
    '''
    线程类,在run中创建socket变量, 然后充当中介,向下交付slotSend中参数,
    向上交付slotRecv中得到的参数
    '''
    #被TcpServer 使用,在类中发射
    signRecv = pyqtSignal(str,  str)
    #在类中使用, 连接socket类中的slotSend
    signSend = pyqtSignal(str,  str,  int)

    
    def __init__(self, socketId, parent):
        super(Thread, self).__init__(parent)
        self.socketId = socketId
     
    def run(self):
        socket = TcpSocket(self.socketId) 
        if not socket.setSocketDescriptor(self.socketId):
            return
        #socket类中的signRecv信号在此连接,把socket接受到的信息作为参数传递给Thread的slotSend    
        socket.signRecv.connect(self.slotRecv)
        #Thread类中的signSend连接socket中slotSend, 将从上级得到的信息传递给socket,最后发送到客户端
        self.signSend.connect(socket.slotSend)
        
        #循环,不加这一句会出问题
        self.exec_()


    def slotSend(self,  action,  msg,  id):
        if action == '':
            return 
        #发射到socket中
        self.signSend.emit(action,  msg,   id)
        
    def slotRecv(self,  action,  msg):
        #发射到TcpServer
        self.signRecv.emit(action,  msg)
         
class TcpServer(QTcpServer):
    signRecv= pyqtSignal(str,  str)
    signSend =  pyqtSignal(str,  str,  int)
    #存放客户端socket的socketId
    socketList = []
    def __init__(self, parent=None):
        super(TcpServer, self).__init__(parent)

        
    def incomingConnection(self, socketId):
        #一有客户端进来就加入列表,当然在此要判断列表是否存在该客户端
        if socketId not in self.socketList:
            self.socketList.append(socketId)
        
            #创建线程
            thread = Thread(socketId,  self)
            #thread发射后,调用tcpserver的slotRecv ,然后其又发射自身的signRecv信号,该信号在Server中连接Server的slotRecv
            thread.signRecv.connect(self.slotRecv)
            #连接thread的slotSend
            self.signSend.connect(thread.slotSend)
            thread.finished.connect(thread.deleteLater)  
            thread.start()
  
    def slotRecv(self,  action,  msg):
        self.signRecv.emit(action,  msg)


        
class Server(QWidget, Ui_Form):
    """
    Class documentation goes here.
    """
    signSend = pyqtSignal(str,  str,  int)
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(Server, self).__init__(parent)
        self.setupUi(self)
        
        self.server= TcpServer(self)
        self.server.listen(QHostAddress("0.0.0.0"), PORT)
        #tcoServer中的signRecv信号,连接自身的slotRecv
        self.server.signRecv.connect(self.slotRecv)

        
    def slotRecv(self,  action,  msg):
        self.updateServer(action,  msg)
        for id in self.server.socketList:
            self.server.signSend.emit(action,  msg,  id)
        
    def showConnection(self):
        print(self.server.socketList)
        
    #def slotSend(self,  action,  msg):
        
    
    def updateServer(self, action,  msg):
        if action == 'USER':
            self.listUser.addItem('*user*: '+ msg)
        if action == 'MSG':
            self.browerChat.append(msg)
          
    @pyqtSlot()
    def on_btnCreate_clicked(self):
        """
        Slot documentation goes here.
        """
        # TODO: not implemented yet
   
if __name__ == '__main__':
    app = QApplication(sys.argv)
    serverDlg = Server()
    serverDlg.show()
    app.exec_()

 

 

 

客户端实现, 客户端类似服务器端,只不过多了一个发送消息的LineEdit, 取名为txtInput, 发送按钮btnSend

代码(Ui_client.py):

 

# -*- coding: utf-8 -*-

#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(455, 366)
        self.listUser = QtWidgets.QListWidget(Form)
        self.listUser.setGeometry(QtCore.QRect(0, 10, 161, 341))
        self.listUser.setObjectName("listUser")
        self.splitter_2 = QtWidgets.QSplitter(Form)
        self.splitter_2.setGeometry(QtCore.QRect(180, 10, 256, 341))
        self.splitter_2.setOrientation(QtCore.Qt.Vertical)
        self.splitter_2.setObjectName("splitter_2")
        self.setNameWidget = QtWidgets.QWidget(self.splitter_2)
        self.setNameWidget.setObjectName("setNameWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.setNameWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.lbName = QtWidgets.QLabel(self.setNameWidget)
        self.lbName.setObjectName("lbName")
        self.horizontalLayout_3.addWidget(self.lbName)
        self.txtName = QtWidgets.QLineEdit(self.setNameWidget)
        self.txtName.setObjectName("txtName")
        self.horizontalLayout_3.addWidget(self.txtName)
        self.btnSet = QtWidgets.QPushButton(self.setNameWidget)
        self.btnSet.setObjectName("btnSet")
        self.horizontalLayout_3.addWidget(self.btnSet)
        self.horizontalLayout.addLayout(self.horizontalLayout_3)
        self.browerChat = QtWidgets.QTextBrowser(self.splitter_2)
        self.browerChat.setObjectName("browerChat")
        self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.txtInput = QtWidgets.QLineEdit(self.layoutWidget)
        self.txtInput.setObjectName("txtInput")
        self.horizontalLayout_2.addWidget(self.txtInput)
        self.btnSend = QtWidgets.QPushButton(self.layoutWidget)
        self.btnSend.setObjectName("btnSend")
        self.horizontalLayout_2.addWidget(self.btnSend)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.lbName.setText(_translate("Form", "name"))
        self.txtName.setText(_translate("Form", "deafault"))
        self.btnSet.setText(_translate("Form", "SetConnection"))
        self.btnSend.setText(_translate("Form", "Send"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

 

 

client.py 代码:

 

# -*- coding: utf-8 -*-

"""
Module implementing Client.
"""

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtNetwork import *

from Ui_client import Ui_Form
import sys
import time

PORT = 26711
SIZEOF_UINT16 = 2

class Client(QWidget, Ui_Form):
    """
    Class documentation goes here.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(Client, self).__init__(parent)
        self.setupUi(self)
        

        self.socket = QTcpSocket() 
        
        self.request = None
        #self.nextBlockSize = 0
        self.name = ''

        #信号
        #self.socket.connected.connected.connect(self.sendRequest)
        self.socket.readyRead.connect(self.readResponse)
        self.socket.disconnected.connect(self.serverHasStopped)
        self.socket.error.connect(self.serverHasError)

        self.socket.connectToHost("localhost", PORT)

        
    @pyqtSlot()
    def on_btnSend_clicked(self):
        """
        Slot documentation goes here.
        """
        # TODO: not implemented yet

        msg =self.name +'     '+\
                time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+'\n'+\
                self.txtInput.text()
        self.issueRequest('MSG',  msg)
        self.txtInput.setText('')
    
    @pyqtSlot()
    def on_txtInput_returnPressed(self):
        """
        Slot documentation goes here.
        """
        # TODO: not implemented yet
        pass
    
    @pyqtSlot()
    def on_btnSet_clicked(self):
        """
        Slot documentation goes here.
        """
        # TODO: not implemented yet
        self.name = self.txtName.text()
        self.issueRequest('USER',  self.name)
            
    def issueRequest(self,  action,  msg):
        print('sendRequest')
        self.request= QByteArray()
        stream = QDataStream(self.request, QIODevice.WriteOnly)
        stream.writeUInt16(0)
        stream.writeQString(action)
        stream.writeQString(msg)
        stream.device().seek(0)
        stream.writeUInt16(self.request.size() - SIZEOF_UINT16)#overwrite seek(0)        

        self.socket.write(self.request)    
        self.nextBlockSize = 0
        self.requst = None
    
    def readResponse(self):
        nextBlockSize = 0 
        stream = QDataStream(self.socket)
        while 1:
            if nextBlockSize == 0 :
                if self.socket.bytesAvailable() <SIZEOF_UINT16:
                    break
                nextBlockSize = stream.readUInt16()
            if self.socket.bytesAvailable() < nextBlockSize:
                break
            action = ''
            msg = ''
            action = stream.readQString()
            msg = stream.readQString()
            print(msg)
            if action == 'MSG':
                self.browerChat.append(msg)
            if action == 'USER':
                self.listUser.addItem('*user*: '+ msg)

    
    def serverHasStopped(self):
        #self.issueRequest('CLOSE',  self.name)
        #self.socket.close()
        print('socket is close')
        
    def serverHasError(self,  error):

        self.socket.close()

        
if __name__ == '__main__':
    app = QApplication(sys.argv)
    dlg = Client()
    dlg.show()
    sys.exit(app.exec_())

 

 

运行结果如下:

 



之前的设计中经常出现Cannot create children for a parent that is in a different thread

Socket notifiers cannot be enabled or disabled from another thread等问题

但是以上代码是都解决了这些问题,当然还有很多不足之处,比如说用户列表那一块,懒得搞了,以后再说吧


 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章