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等問題

但是以上代碼是都解決了這些問題,當然還有很多不足之處,比如說用戶列表那一塊,懶得搞了,以後再說吧


 

 

 

 

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