pythonpyqt4qtcpsocketqtcpserver

PyQt QTcpServer: How to return data to multiple clients?


I am looking to create a QTcpServer using PyQt that can simultaneously return data to 2 or more clients. I assume that this will require threading.

Using the threadedfortuneserver.py example as a test case (included with PyQt4, on my system it is found in /usr/share/doc/python-qt4-doc/examples/network), I want to connect multiple clients and each time one of the clients asks for a fortune, the other clients also get updated with a message like "Client X just received the fortune 'blah blah blah'".

I understand how the fortuneserver/client program works, but it seems that the client connections are immediately terminated after the fortune is sent back to the client. My specific questions are:

  1. Is it possible to keep all of the connections open so that every time one of the clients requests a fortune, the other clients can be updated?

  2. If so, what is the best way to keep track of and loop over the connected clients?

This is a serious stumbling block for me because I want to develop an app where several clients can interact, and each client can be updated about the actions of the other clients.

Thanks in advance for your help, let me know if there is any other information I can provide.

I found this thread but there wasn't enough specific information to make use of. Other discussions have been for the Python socket package, but it is my understanding that when using PyQt, the server should be a QTcpServer so everything plays nice.

*** EDIT ***

Here are the beginning stages of my solution. I have created a basic server and client. The server just sends back what the client entered into a Line Edit box.

I am basing this on the "buildingservices" example from Chapter 18 of Rapid GUI Programming with Python and Qt.

The major change I made is that now the threads keep running indefinitely and their sockets stay open, listening for data being sent by the client.

It handles multiple clients fine. It is certainly ugly, but I think it is a good starting point.

What I would like is to be able to notify each client whenever one client enters text (like a typical chat program, say).

Also, to give you an idea of who you are dealing with, I am NOT a professional programmer. I am a physicist with many years of undisciplined scripting and fiddling around under my belt. But I would like to try to develop basic server/client programs that can pass data around.

Thanks for any help or suggestions!

SERVER:

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtNetwork import *

PORT = 9999
SIZEOF_UINT16 = 2

class Thread(QThread):

    #lock = QReadWriteLock()

    def __init__(self, socketId, parent):
        super(Thread, self).__init__(parent)
        self.socketId = socketId

    def run(self):
        self.socket = QTcpSocket()

        if not self.socket.setSocketDescriptor(self.socketId):
            self.emit(SIGNAL("error(int)"), socket.error())
            return

        while self.socket.state() == QAbstractSocket.ConnectedState:
            nextBlockSize = 0
            stream = QDataStream(self.socket)
            stream.setVersion(QDataStream.Qt_4_2)
            if (self.socket.waitForReadyRead(-1) and
                self.socket.bytesAvailable() >= SIZEOF_UINT16):
                nextBlockSize = stream.readUInt16()
            else:
                self.sendError("Cannot read client request")
                return
            if self.socket.bytesAvailable() < nextBlockSize:
                if (not self.socket.waitForReadyRead(-1) or
                    self.socket.bytesAvailable() < nextBlockSize):
                    self.sendError("Cannot read client data")
                    return

            textFromClient = stream.readQString()

            textToClient = "You wrote: \"{}\"".format(textFromClient)
            self.sendReply(textToClient)

    def sendError(self, msg):
        reply = QByteArray()
        stream = QDataStream(reply, QIODevice.WriteOnly)
        stream.setVersion(QDataStream.Qt_4_2)
        stream.writeUInt16(0)
        stream.writeQString("ERROR")
        stream.writeQString(msg)
        stream.device().seek(0)
        stream.writeUInt16(reply.size() - SIZEOF_UINT16)
        self.socket.write(reply)

    def sendReply(self, text):
        reply = QByteArray()
        stream = QDataStream(reply, QIODevice.WriteOnly)
        stream.setVersion(QDataStream.Qt_4_2)
        stream.writeUInt16(0)
        stream.writeQString(text)
        stream.device().seek(0)
        stream.writeUInt16(reply.size() - SIZEOF_UINT16)
        self.socket.write(reply)


class TcpServer(QTcpServer):

    def __init__(self, parent=None):
        super(TcpServer, self).__init__(parent)

    def incomingConnection(self, socketId):
        self.thread = Thread(socketId, self)
        self.thread.start()


class ServerDlg(QPushButton):

    def __init__(self, parent=None):
        super(ServerDlg, self).__init__(
                "&Close Server", parent)
        self.setWindowFlags(Qt.WindowStaysOnTopHint)

        self.tcpServer = TcpServer(self)
        if not self.tcpServer.listen(QHostAddress("0.0.0.0"), PORT):
            QMessageBox.critical(self, "Threaded Server",
                    "Failed to start server: {}".format(
                    self.tcpServer.errorString()))
            self.close()
            return

        self.connect(self, SIGNAL("clicked()"), self.close)
        font = self.font()
        font.setPointSize(24)
        self.setFont(font)
        self.setWindowTitle("Threaded Server")

app = QApplication(sys.argv)
form = ServerDlg()
form.show()
form.move(0, 0)
app.exec_()

CLIENT:

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtNetwork import *

PORT = 9999
SIZEOF_UINT16 = 2

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        # Ititialize socket
        self.socket = QTcpSocket()
        # Initialize data IO variables
        self.nextBlockSize = 0
        self.request = None
        # Create widgets/layout
        self.browser = QTextBrowser()
        self.lineedit = QLineEdit("Texty bits")
        self.lineedit.selectAll()
        self.connectButton = QPushButton("Connect")
        self.connectButton.setDefault(False)
        self.connectButton.setEnabled(True)
        layout = QVBoxLayout()
        layout.addWidget(self.browser)
        layout.addWidget(self.lineedit)
        layout.addWidget(self.connectButton)
        self.setLayout(layout)
        self.lineedit.setFocus()

        # Signals and slots for line edit and connect button
        self.lineedit.returnPressed.connect(self.sendToServer)
        self.connectButton.released.connect(self.connectToServer)

        self.setWindowTitle("Client")

        # Signals and slots for networking
        self.socket.readyRead.connect(self.readFromServer)
        self.socket.disconnected.connect(self.serverHasStopped)
        self.connect(self.socket,
                     SIGNAL("error(QAbstractSocket::SocketError)"),
                     self.serverHasError)

    # Update GUI
    def updateUi(self, text):
        self.browser.append(text)

    # Create connection to server
    def connectToServer(self):
        self.connectButton.setEnabled(False)
        print("Connecting to server")
        self.socket.connectToHost("localhost", PORT)

    # Send data to server
    def sendToServer(self):
        self.request = QByteArray()
        stream = QDataStream(self.request, QIODevice.WriteOnly)
        stream.setVersion(QDataStream.Qt_4_2)
        stream.writeUInt16(0)
        stream.writeQString(self.lineedit.text())
        stream.device().seek(0)
        stream.writeUInt16(self.request.size() - SIZEOF_UINT16)
        self.socket.write(self.request)
        self.nextBlockSize = 0
        self.request = None
        self.lineedit.setText("")

    # Read data from server and update Text Browser
    def readFromServer(self):
        stream = QDataStream(self.socket)
        stream.setVersion(QDataStream.Qt_4_2)

        while True:
            if self.nextBlockSize == 0:
                if self.socket.bytesAvailable() < SIZEOF_UINT16:
                    break
                self.nextBlockSize = stream.readUInt16()
            if self.socket.bytesAvailable() < self.nextBlockSize:
                break
            textFromServer = stream.readQString()
            self.updateUi(textFromServer)
            self.nextBlockSize = 0

    def serverHasStopped(self):
        self.socket.close()

    def serverHasError(self):
        self.updateUi("Error: {}".format(
                self.socket.errorString()))
        self.socket.close()


app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()

Solution

  • As was probably exasperatingly obvious to most of you, I didn't fully understand how to deal with threads! Not to worry, I have discovered a way to design a server that can send data to multiple clients with nary a secondary thread to be found.

    Quite simple, really, but I'm not the quickest of cats at the best of times.

    SERVER:

    #!/usr/bin/env python3
    
    import sys
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    from PyQt4.QtNetwork import *
    
    PORT = 9999
    SIZEOF_UINT32 = 4
    
    class ServerDlg(QPushButton):
    
        def __init__(self, parent=None):
            super(ServerDlg, self).__init__(
                    "&Close Server", parent)
            self.setWindowFlags(Qt.WindowStaysOnTopHint)
    
            self.tcpServer = QTcpServer(self)               
            self.tcpServer.listen(QHostAddress("0.0.0.0"), PORT)
            self.connect(self.tcpServer, SIGNAL("newConnection()"), 
                        self.addConnection)
            self.connections = []
    
            self.connect(self, SIGNAL("clicked()"), self.close)
            font = self.font()
            font.setPointSize(24)
            self.setFont(font)
            self.setWindowTitle("Server")
    
        def addConnection(self):
            clientConnection = self.tcpServer.nextPendingConnection()
            clientConnection.nextBlockSize = 0
            self.connections.append(clientConnection)
    
            self.connect(clientConnection, SIGNAL("readyRead()"), 
                    self.receiveMessage)
            self.connect(clientConnection, SIGNAL("disconnected()"), 
                    self.removeConnection)
            self.connect(clientConnection, SIGNAL("error()"), 
                    self.socketError)
    
        def receiveMessage(self):
            for s in self.connections:
                if s.bytesAvailable() > 0:
                    stream = QDataStream(s)
                    stream.setVersion(QDataStream.Qt_4_2)
    
                    if s.nextBlockSize == 0:
                        if s.bytesAvailable() < SIZEOF_UINT32:
                            return
                        s.nextBlockSize = stream.readUInt32()
                    if s.bytesAvailable() < s.nextBlockSize:
                        return
    
                    textFromClient = stream.readQString()
                    s.nextBlockSize = 0
                    self.sendMessage(textFromClient, 
                                     s.socketDescriptor())
                    s.nextBlockSize = 0
    
        def sendMessage(self, text, socketId):
            for s in self.connections:
                if s.socketDescriptor() == socketId:
                    message = "You> {}".format(text)
                else:
                    message = "{}> {}".format(socketId, text)
                reply = QByteArray()
                stream = QDataStream(reply, QIODevice.WriteOnly)
                stream.setVersion(QDataStream.Qt_4_2)
                stream.writeUInt32(0)
                stream.writeQString(message)
                stream.device().seek(0)
                stream.writeUInt32(reply.size() - SIZEOF_UINT32)
                s.write(reply)
    
        def removeConnection(self):
            pass
    
        def socketError(self):
            pass
    
    
    app = QApplication(sys.argv)
    form = ServerDlg()
    form.show()
    form.move(0, 0)
    app.exec_()
    

    CLIENT

    import sys
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    from PyQt4.QtNetwork import *
    
    PORTS = (9998, 9999)
    PORT = 9999
    SIZEOF_UINT32 = 4
    
    class Form(QDialog):
    
        def __init__(self, parent=None):
            super(Form, self).__init__(parent)
    
            # Ititialize socket
            self.socket = QTcpSocket()
    
            # Initialize data IO variables
            self.nextBlockSize = 0
            self.request = None
    
            # Create widgets/layout
            self.browser = QTextBrowser()
            self.lineedit = QLineEdit("Enter text here, dummy")
            self.lineedit.selectAll()
            self.connectButton = QPushButton("Connect")
            self.connectButton.setEnabled(True)
            layout = QVBoxLayout()
            layout.addWidget(self.browser)
            layout.addWidget(self.lineedit)
            layout.addWidget(self.connectButton)
            self.setLayout(layout)
            self.lineedit.setFocus()
    
            # Signals and slots for line edit and connect button
            self.lineedit.returnPressed.connect(self.issueRequest)
            self.connectButton.clicked.connect(self.connectToServer)
    
            self.setWindowTitle("Client")
            # Signals and slots for networking
            self.socket.readyRead.connect(self.readFromServer)
            self.socket.disconnected.connect(self.serverHasStopped)
            self.connect(self.socket,
                         SIGNAL("error(QAbstractSocket::SocketError)"),
                         self.serverHasError)
    
        # Update GUI
        def updateUi(self, text):
            self.browser.append(text)
    
        # Create connection to server
        def connectToServer(self):
            self.connectButton.setEnabled(False)
            self.socket.connectToHost("localhost", PORT)
    
        def issueRequest(self):
            self.request = QByteArray()
            stream = QDataStream(self.request, QIODevice.WriteOnly)
            stream.setVersion(QDataStream.Qt_4_2)
            stream.writeUInt32(0)
            stream.writeQString(self.lineedit.text())
            stream.device().seek(0)
            stream.writeUInt32(self.request.size() - SIZEOF_UINT32)
            self.socket.write(self.request)
            self.nextBlockSize = 0
            self.request = None
            self.lineedit.setText("")
    
        def readFromServer(self):
            stream = QDataStream(self.socket)
            stream.setVersion(QDataStream.Qt_4_2)
    
            while True:
                if self.nextBlockSize == 0:
                    if self.socket.bytesAvailable() < SIZEOF_UINT32:
                        break
                    self.nextBlockSize = stream.readUInt32()
                if self.socket.bytesAvailable() < self.nextBlockSize:
                    break
                textFromServer = stream.readQString()
                self.updateUi(textFromServer)
                self.nextBlockSize = 0
    
        def serverHasStopped(self):
            self.socket.close()
            self.connectButton.setEnabled(True)
    
        def serverHasError(self):
            self.updateUi("Error: {}".format(
                    self.socket.errorString()))
            self.socket.close()
            self.connectButton.setEnabled(True)
    
    
    app = QApplication(sys.argv)
    form = Form()
    form.show()
    app.exec_()
    

    To summarize, each client connection opens a socket, and the socket is appended to a list of all client sockets. Then, when one of the clients sends text, the server loops over the client sockets, finds the one that has bytesAvailable, reads it in, and then sends the message to the other clients.

    I would be curious to hear what other people may think of this approach. Pitfalls, issues, etc.

    Thanks!