Home > Mobile >  Determine which descriptor ID belongs to which client - QTcpSocket
Determine which descriptor ID belongs to which client - QTcpSocket

Time:12-31

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?

  • I expect something like this as a result: enter image description here

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_())

CodePudding user response:

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:

  • both signals have a bytes signature, but this is wrong as you're emitting those signals as str types; if you're not sure, you can use the basic object type;

  • the self.connect syntax is considered obsolete, use the "new" (well, not so new anymore) style one: object.signal.connect(slot); for instance:

    self.socket.readyRead.connect(self.read_socket)

  • use QApplication.quit() instead of sys.exit(), so that the application properly does everything it needs before actually quitting the python interpreter;

  • instead of using the text value of the combo, you should use the user data:

    descriptor = socket.socketDescriptor()
    self.combobox_receiver.addItem(str(descriptor), descriptor)
    then you can access it by using self.combobox_receiver.currentData() (you can add the "broadcast" item with a -1 value); you could even add the socket itself as user data;

  • to properly split the header without getting garbled results for the last field, you must add a final comma, otherwise split() will return the whole rest of the string;

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

  • Related