c++pyqt5qt5pyside2qtcpsocket

Determine which descriptor ID belongs to which client - QTcpSocket


I am creating an app where the server and clients run on the same machine, see picture.

enter image description here

I want the user to be able to send data from the server to a specific client (= specific window). For this, the user needs to know which ID belongs to which client (for example the corresponding ID could be displayed in each window's title).

Is it possible to get the corresponding descriptor ID on the client side? If not, how could I achieve the same result anyway?

Here is an example code in pyside2 but I don't mind if the solution is using C++ qt.

QTCPServer:

import sys
from typing import List

from PySide2.QtCore import *
from PySide2.QtNetwork import *
from PySide2.QtWidgets import *


class MainWindow(QMainWindow):
    new_message = Signal(bytes)
    _connection_set: List[QTcpSocket] = []

    def __init__(self):
        super().__init__()
        self.server = QTcpServer()

        # layout
        self.setWindowTitle("QTCPServer")
        self._central_widget = QWidget()
        self._main_layout = QVBoxLayout()
        self.status_bar = QStatusBar()
        self.text_browser_received_messages = QTextBrowser()
        self._controller_layout = QHBoxLayout()
        self.combobox_receiver = QComboBox()
        self.lineEdit_message = QLineEdit()
        self._controller_layout.addWidget(self.combobox_receiver)
        self._controller_layout.addWidget(self.lineEdit_message)
        self._buttons_layout = QHBoxLayout()
        self.send_message_button = QPushButton("Send Message")
        self.send_message_button.clicked.connect(self.send_message_button_clicked)
        self._buttons_layout.addWidget(self.send_message_button)
        # end layout

        if self.server.listen(QHostAddress.Any, 8080):
            self.new_message.connect(self.display_message)
            self.server.newConnection.connect(self.new_connection)
            self.status_bar.showMessage("Server is listening...")
        else:
            QMessageBox.critical(self, "QTCPServer", f"Unable to start the server: {self.server.errorString()}.")

            self.server.close()
            self.server.deleteLater()

            sys.exit()

        # set layout
        self.setStatusBar(self.status_bar)
        self.setCentralWidget(self._central_widget)
        self._central_widget.setLayout(self._main_layout)
        self._main_layout.addWidget(self.text_browser_received_messages)
        self._main_layout.addLayout(self._controller_layout)
        self._main_layout.addLayout(self._buttons_layout)

    def new_connection(self) -> None:
        while self.server.hasPendingConnections():
            self.append_to_socket_list(self.server.nextPendingConnection())

    def append_to_socket_list(self, socket: QTcpSocket):
        self._connection_set.insert(len(self._connection_set), socket)
        self.connect(socket, SIGNAL("readyRead()"), self.read_socket)
        self.connect(socket, SIGNAL("disconnected()"), self.discard_socket)
        self.combobox_receiver.addItem(str(socket.socketDescriptor()))
        self.display_message(f"INFO :: Client with socket:{socket.socketDescriptor()} has just entered the room")

    def read_socket(self):
        socket: QTcpSocket = self.sender()
        buffer = QByteArray()

        socket_stream = QDataStream(socket)
        socket_stream.setVersion(QDataStream.Qt_5_15)

        socket_stream.startTransaction()
        socket_stream >> buffer

        if not socket_stream.commitTransaction():
            message = f"{socket.socketDescriptor()} :: Waiting for more data to come.."
            self.new_message.emit(message)
            return

        header = buffer.mid(0, 128)
        file_type = header.split(",")[0].split(":")[1]
        buffer = buffer.mid(128)

        if file_type == "message":
            message = f"{socket.socketDescriptor()} :: {str(buffer, 'utf-8')}"
            self.new_message.emit(message)

    def discard_socket(self):
        socket: QTcpSocket = self.sender()

        it = self._connection_set.index(socket)

        if it != len(self._connection_set):
            self.display_message(f"INFO :: A client has just left the room")
            del self._connection_set[it]
        socket.deleteLater()

        self.refresh_combobox()

    def send_message_button_clicked(self):
        receiver = self.combobox_receiver.currentText()
        if receiver == "Broadcast":
            for socket in self._connection_set:
                self.send_message(socket)
        else:
            for socket in self._connection_set:
                if socket.socketDescriptor() == int(receiver):
                    self.send_message(socket)
                    return
        self.lineEdit_message.clear()

    def send_message(self, socket: QTcpSocket):
        if not socket:
            QMessageBox.critical(self, "QTCPServer", "Not connected")
            return

        if not socket.isOpen():
            QMessageBox.critical(self, "QTCPServer", "Socket doesn't seem to be opened")
            return

        string = self.lineEdit_message.text()
        socket_stream = QDataStream(socket)
        socket_stream.setVersion(QDataStream.Qt_5_15)
        header = QByteArray()
        string_size = len(string.encode('utf-8'))
        fstring = f"fileType:message,fileName:null,fileSize:{string_size}"
        header.prepend(fstring.encode())
        header.resize(128)

        byte_array = QByteArray(string.encode())
        byte_array.prepend(header)

        socket_stream.setVersion(QDataStream.Qt_5_15)
        socket_stream << byte_array

    def display_message(self, string):
        self.text_browser_received_messages.append(string)

    def refresh_combobox(self):
        self.combobox_receiver.clear()
        self.combobox_receiver.addItem("Broadcast")
        for socket in self._connection_set:
            self.combobox_receiver.addItem(str(socket.socketDescriptor()))


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

QTCPClient

import sys

from PySide2.QtCore import *
from PySide2.QtNetwork import *
from PySide2.QtWidgets import *


class MainWindow(QMainWindow):
    new_message = Signal(bytes)

    def __init__(self):
        super().__init__()
        self.socket = QTcpSocket(self)
        # layout
        self.setWindowTitle("QTCPClient")
        self._central_widget = QWidget()
        self._main_layout = QVBoxLayout()
        self.status_bar = QStatusBar()
        self.text_browser_received_messages = QTextBrowser()
        self._controller_layout = QHBoxLayout()
        self.lineEdit_message = QLineEdit()
        self._controller_layout.addWidget(self.lineEdit_message)
        self._buttons_layout = QHBoxLayout()
        self.send_message_button = QPushButton("Send Message")
        self.send_message_button.clicked.connect(self.on_send_message_button_clicked)
        self._buttons_layout.addWidget(self.send_message_button)
        # end layout

        self.new_message.connect(self.display_message)
        self.connect(self.socket, SIGNAL("readyRead()"), self.read_socket)
        self.connect(self.socket, SIGNAL("disconnected()"), self.discard_socket)

        # set layout
        self.setStatusBar(self.status_bar)
        self.setCentralWidget(self._central_widget)
        self._central_widget.setLayout(self._main_layout)
        self._main_layout.addWidget(self.text_browser_received_messages)
        self._main_layout.addLayout(self._controller_layout)
        self._main_layout.addLayout(self._buttons_layout)

        self.socket.connectToHost(QHostAddress.LocalHost, 8080)

        if self.socket.waitForConnected():
            self.status_bar.showMessage("Connected to Server")
        else:
            QMessageBox.critical(self, "QTCPClient", f"The following error occurred: {self.socket.errorString()}.")
            if self.socket.isOpen():
                self.socket.close()
            sys.exit()

    def discard_socket(self):
        self.socket.deleteLater()
        self.socket = None
        self.status_bar.showMessage("Disconnected!")

    def read_socket(self):
        buffer = QByteArray()

        socket_stream = QDataStream(self.socket)
        socket_stream.setVersion(QDataStream.Qt_5_15)

        socket_stream.startTransaction()
        socket_stream >> buffer

        if not socket_stream.commitTransaction():
            message = f"{self.socket.socketDescriptor()} :: Waiting for more data to come.."
            self.new_message.emit(message)
            return

        header = buffer.mid(0, 128)
        file_type = header.split(",")[0].split(":")[1]

        buffer = buffer.mid(128)

        if file_type == "message":
            message = f"{self.socket.socketDescriptor()} :: {str(buffer, 'utf-8')}"
            self.new_message.emit(message)

    def on_send_message_button_clicked(self):
        if not self.socket:
            QMessageBox.critical(self, "QTCPServer", "Not connected")
            return

        if not self.socket.isOpen():
            QMessageBox.critical(self, "QTCPServer", "Socket doesn't seem to be opened")
            return

        string = self.lineEdit_message.text()
        socket_stream = QDataStream(self.socket)
        socket_stream.setVersion(QDataStream.Qt_5_15)
        header = QByteArray()
        string_size = len(string.encode('utf-8'))
        fstring = f"fileType:message,fileName:null,fileSize:{string_size}"
        header.prepend(fstring.encode())
        header.resize(128)

        byte_array = QByteArray(string.encode())
        byte_array.prepend(header)

        socket_stream << byte_array

        self.lineEdit_message.clear()

    def display_message(self, string: str):
        self.text_browser_received_messages.append(string)


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


Solution

  • The socket descriptors are only valid for the constructor and they do not match on both sides.

    One possibility is to automatically send a first "handshake" message to the client as soon as it's connected, the client will identify that message as a "descriptor id" type, and eventually set its window title.

    In the following changes to your code, I'm using a simple fileType:descriptor header, and the descriptor id is actually sent as an integer value into the datastream. You can obviously use a string there, if you want to send any other value.

        # server
        def append_to_socket_list(self, socket: QTcpSocket):
            # ...
    
            descriptor = int(socket.socketDescriptor())
            socket_stream = QDataStream(socket)
            fstring = 'fileType:descriptor,fileName:null,fileSize:{},'.format(descriptor.bit_length())
            header = QByteArray()
            header.prepend(fstring.encode())
            header.resize(128)
    
            socket_stream << header
            socket_stream.writeInt32(descriptor)
    
        # client
        def read_socket(self):
            # ...
            header = buffer.mid(0, 128)
            fields = header.split(",")
            file_type = fields[0].split(":")[1]
    
            buffer = buffer.mid(128)
    
            if file_type == "descriptor":
                self.id = socket_stream.readInt32()
                self.setWindowTitle("QTCPClient - id {}".format(self.id))
    

    Some suggestions:

    Note for PyQt users: socketDescriptor() returns a sip.voidptr, to obtain the actual value use int(socket.socketDescriptor()).