Сервер чата Python с небольшими предупреждениями и общими улучшениями кода - PullRequest
0 голосов
/ 11 июня 2019

Прежде всего, спасибо, что нашли время, чтобы помочь мне. Я работал над созданием базового сервера / клиента чата в течение нескольких дней (в свободное время). У меня работает пользовательский интерфейс, и он у меня даже есть, поэтому сервер и клиент могут находиться в одном файле. Сервер может размещаться для нескольких клиентов и является многопоточным. Супер доволен собой, чтобы получить это далеко: D.

Однако я получаю три незначительные ошибки в своем коде и буду признателен за любые отзывы о моем коде, чтобы я мог улучшить.

Ошибка № 1:

QObject :: connect: Невозможно поставить в очередь аргументы типа 'QTextCursor' (Убедитесь, что QTextCursor зарегистрирован с использованием qRegisterMetaType ().)

Из того, что я смог найти в Интернете, это как-то связано с тем, что я многопоточен, чтобы сделать так, чтобы пользовательский интерфейс не зависал во время True True. Я должен использовать сигнальные слоты PyQt, но я не совсем уверен, что и как. У меня есть базовые знания о сигнальных слотах, и я не совсем уверен в том, что я хочу сделать, и поэтому мне трудно исследовать, какие изменения мне нужно сделать.

Ошибка 2

Ошибка чтения: [WinError 10054] Существующее соединение было принудительно закрыто удаленным хостом

Я знаю, ПОЧЕМУ эта ошибка возникает (я закрываю сервер перед клиентом). Но я не уверен, что будет лучшим способом справиться с этим. В идеале я хочу, чтобы window.chat говорил «Сервер выключен». а затем в основном остановить код, не закрывая программу. У меня возникли проблемы, зная, как именно обработать ошибку изящно.

Ошибка 3

Traceback (последний вызов был последним): Файл "C: /Users/User/PycharmProjects/Program/client/client.py", строка 44, в работе client_socket.connect ((IP, PORT)) ConnectionRefusedError: [WinError 10061] Невозможно установить соединение, поскольку целевая машина активно отказала ему

По сути, это то же самое, что и Ошибка 2 , но происходит, когда вы запускаете клиент до запуска сервера.

Для получения Ошибка 1 все, что вам нужно сделать, это загрузить сервер.

Чтобы получить Ошибка 2 , просто закройте сервер, прежде чем закрыть клиент.

Чтобы получить Ошибка 3 , просто запустите клиент перед запуском сервера.

Код сервера / клиента Обратите внимание, что вам придется сохранить в двух отдельных файлах. Для сервера убедитесь, что строка 274 = "Сервер", для клиента убедитесь, что это! = "Сервер".

import socket
import select
import errno
import sys
from PyQt5 import QtGui, QtCore

from PyQt5.QtWidgets import QScrollBar, QSplitter, QTableWidgetItem, QTableWidget, QComboBox, QVBoxLayout, QGridLayout, \
    QDialog, QWidget, QPushButton, QApplication, QMainWindow, QAction, QMessageBox, QLabel, QTextEdit, QProgressBar, \
    QLineEdit
from PyQt5.QtCore import QCoreApplication, QThread
import socket
from socketserver import ThreadingMixIn
from PyQt5 import QtWidgets, uic
from PyQt5.QtWidgets import QMainWindow
import os
import threading

import time

class ClientThread(QThread):
    def __init__(self, parent=None):
        super(ClientThread, self).__init__()

    def run(self):
        global HEADER_LENGTH
        global IP
        global PORT
        global client_socket
        global my_username

        HEADER_LENGTH = 10

        IP = "127.0.0.1"
        PORT = 1234
        # my_username = input("Username: ")


        # Create a socket
        # socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
        # socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # Connect to a given ip and port
        client_socket.connect((IP, PORT))

        # Set connection to non-blocking state, so .recv() call won;t block, just return some exception we'll handle
        client_socket.setblocking(False)

        # Prepare username and header and send them
        # We need to encode username to bytes, then count number of bytes and prepare header of fixed size, that we encode to bytes as well
        username = my_username.encode('utf-8')
        username_header = f"{len(username):<{HEADER_LENGTH}}".encode('utf-8')
        client_socket.send(username_header + username)
        if my_username != "Server":
            self.receive()

    def receive(self):
        threading.Timer(0.1, self.receive).start()
        # print("looking for messages")
        try:
            # Now we want to loop over received messages (there might be more than one) and print them
            while True:

                # Receive our "header" containing username length, it's size is defined and constant
                username_header = client_socket.recv(HEADER_LENGTH)

                # If we received no data, server gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
                if not len(username_header):
                    print('Connection closed by the server')
                    sys.exit()

                # Convert header to int value
                username_length = int(username_header.decode('utf-8').strip())

                # Receive and decode username
                username = client_socket.recv(username_length).decode('utf-8')

                # Now do the same for message (as we received username, we received whole message, there's no need to check if it has any length)
                message_header = client_socket.recv(HEADER_LENGTH)
                message_length = int(message_header.decode('utf-8').strip())
                message = client_socket.recv(message_length).decode('utf-8')

                # Print message
                window.chat.append(f'{username}: {message}')
                # print(f'{username} > {message}')

        except IOError as e:
            # This is normal on non blocking connections - when there are no incoming data error is going to be raised
            # Some operating systems will indicate that using AGAIN, and some using WOULDBLOCK error code
            # We are going to check for both - if one of them - that's expected, means no incoming data, continue as normal
            # If we got different error code - something happened
            if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
                print('Reading error: {}'.format(str(e)))
                sys.exit()

            # We just did not receive anything
            # continue

        except Exception as e:
            # Any other exception - something happened, exit
            print('Reading error: '.format(str(e)))
            sys.exit()

    def send(self):
        # while True:

        # Wait for user to input a message
        # message = input(f'{my_username} > ')

        message = window.chatTextField.text()

        window.chatTextField.setText("")

        # If message is not empty - send it
        if message:
            # Encode message to bytes, prepare header and convert to bytes, like for username above, then send
            if my_username != "Server":
                window.chat.append(f'{my_username}: {message}')
            message = message.encode('utf-8')
            message_header = f"{len(message):<{HEADER_LENGTH}}".encode('utf-8')
            client_socket.send(message_header + message)



class WorkerThread(QThread):
    def __init__(self, parent=None):
        super(WorkerThread, self).__init__()


    def run(self):
        print("Running thread...")
        HEADER_LENGTH = 10

        IP = "127.0.0.1"
        PORT = 1234

        # Create a socket
        # socket.AF_INET - address family, IPv4, some otehr possible are AF_INET6, AF_BLUETOOTH, AF_UNIX
        # socket.SOCK_STREAM - TCP, conection-based, socket.SOCK_DGRAM - UDP, connectionless, datagrams, socket.SOCK_RAW - raw IP packets
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # SO_ - socket option
        # SOL_ - socket option level
        # Sets REUSEADDR (as a socket option) to 1 on socket
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # Bind, so server informs operating system that it's going to use given IP and port
        # For a server using 0.0.0.0 means to listen on all available interfaces, useful to connect locally to 127.0.0.1 and remotely to LAN interface IP
        server_socket.bind((IP, PORT))

        # This makes server listen to new connections
        server_socket.listen()

        # List of sockets for select.select()
        sockets_list = [server_socket]

        # List of connected clients - socket as a key, user header and name as data
        clients = {}

        print(f'Listening for connections on {IP}:{PORT}...')

        # Handles message receiving
        def receive_message(client_socket):

            try:

                # Receive our "header" containing message length, it's size is defined and constant
                message_header = client_socket.recv(HEADER_LENGTH)

                # If we received no data, client gracefully closed a connection, for example using socket.close() or socket.shutdown(socket.SHUT_RDWR)
                if not len(message_header):
                    return False

                # Convert header to int value
                message_length = int(message_header.decode('utf-8').strip())

                # Return an object of message header and message data
                return {'header': message_header, 'data': client_socket.recv(message_length)}

            except:

                # If we are here, client closed connection violently, for example by pressing ctrl+c on his script
                # or just lost his connection
                # socket.close() also invokes socket.shutdown(socket.SHUT_RDWR) what sends information about closing the socket (shutdown read/write)
                # and that's also a cause when we receive an empty message
                return False

        while True:

            # Calls Unix select() system call or Windows select() WinSock call with three parameters:
            #   - rlist - sockets to be monitored for incoming data
            #   - wlist - sockets for data to be send to (checks if for example buffers are not full and socket is ready to send some data)
            #   - xlist - sockets to be monitored for exceptions (we want to monitor all sockets for errors, so we can use rlist)
            # Returns lists:
            #   - reading - sockets we received some data on (that way we don't have to check sockets manually)
            #   - writing - sockets ready for data to be send thru them
            #   - errors  - sockets with some exceptions
            # This is a blocking call, code execution will "wait" here and "get" notified in case any action should be taken
            read_sockets, _, exception_sockets = select.select(sockets_list, [], sockets_list)

            # Iterate over notified sockets
            for notified_socket in read_sockets:

                # If notified socket is a server socket - new connection, accept it
                if notified_socket == server_socket:

                    # Accept new connection
                    # That gives us new socket - client socket, connected to this given client only, it's unique for that client
                    # The other returned object is ip/port set
                    client_socket, client_address = server_socket.accept()

                    # Client should send his name right away, receive it
                    user = receive_message(client_socket)

                    # If False - client disconnected before he sent his name
                    if user is False:
                        continue

                    # Add accepted socket to select.select() list
                    sockets_list.append(client_socket)

                    # Also save username and username header
                    clients[client_socket] = user
                    window.serverLog.append('Accepted new connection from {}:{}, username: {}'.format(*client_address,
                                                                                    user['data'].decode('utf-8')))


                # Else existing socket is sending a message
                else:

                    # Receive message
                    message = receive_message(notified_socket)

                    # If False, client disconnected, cleanup
                    if message is False:
                        print('Closed connection from: {}'.format(clients[notified_socket]['data'].decode('utf-8')))

                        # Remove from list for socket.socket()
                        sockets_list.remove(notified_socket)

                        # Remove from our list of users
                        del clients[notified_socket]

                        continue

                    # Get user by notified socket, so we will know who sent the message
                    user = clients[notified_socket]
                    window.serverLog.append(f'{user["data"].decode("utf-8")}: {message["data"].decode("utf-8")}')
                    window.chat.append(f'{user["data"].decode("utf-8")}: {message["data"].decode("utf-8")}')
                    #self.chat.append(f'{user}: {message}')
                    #print(f'Received message from {user["data"].decode("utf-8")}: {message["data"].decode("utf-8")}')

                    # Iterate over connected clients and broadcast message
                    for client_socket in clients:

                        # But don't sent it to sender
                        if client_socket != notified_socket:
                            # Send user and message (both with their headers)
                            # We are reusing here message header sent by sender, and saved username header send by user when he connected
                            client_socket.send(user['header'] + user['data'] + message['header'] + message['data'])

            # It's not really necessary to have this, but will handle some socket exceptions just in case
            for notified_socket in exception_sockets:
                # Remove from list for socket.socket()
                sockets_list.remove(notified_socket)

                # Remove from our list of users
                del clients[notified_socket]

        print("Thread complete.")

class Window(QMainWindow):
    global my_username
    my_username = "Server"

    def init_ui(self):
        self.show()

    def __init__(self, parent=None):
        super(Window, self).__init__()
        uic.loadUi(str(os.getcwd()) + "\\" + "ui.ui", self)
        self.chatTextField.installEventFilter(self)
        # self.btnSend.clicked.connect(self.processData)
        self.init_ui()

        self.actionClient.triggered.connect(lambda: self.stackedWidget.setCurrentIndex(0))
        self.actionServer.triggered.connect(lambda: self.stackedWidget.setCurrentIndex(1))

        self.workerThread = WorkerThread()
        self.clientThread = ClientThread()
        self.processData()


    def eventFilter(self, widget, event):
        if (event.type() == QtCore.QEvent.KeyPress and widget is self.chatTextField):
            key = event.key()
            if key == QtCore.Qt.Key_Escape:
                print('escape')
            else:
                if key == QtCore.Qt.Key_Return or key == QtCore.Qt.Key_Enter:
                    # print("sending...")
                    self.clientThread.send()
                    # self.chatTextField.insertPlainText("\b")

        return QtWidgets.QWidget.eventFilter(self, widget, event)


    def processData(self):
        global my_username
        if my_username == "Server":
            self.workerThread.start()
        self.clientThread.start()


if __name__ == '__main__':
    app = QApplication(sys.argv)

    window = Window()
    # serverThread = ServerThread(window)
    # serverThread.start()
    window.init_ui()
    window.show()
    sys.exit(app.exec_())

Файл интерфейса пользователя

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>ChatApplication</class>
 <widget class="QMainWindow" name="ChatApplication">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Chat Window</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="1" column="1">
     <widget class="QStackedWidget" name="stackedWidget">
      <property name="currentIndex">
       <number>0</number>
      </property>
      <widget class="QWidget" name="page">
       <layout class="QGridLayout" name="gridLayout_2">
        <item row="1" column="1">
         <widget class="QPushButton" name="btnSend">
          <property name="text">
           <string>Send</string>
          </property>
         </widget>
        </item>
        <item row="1" column="0">
         <widget class="QLineEdit" name="chatTextField">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="inputMethodHints">
           <set>Qt::ImhNone</set>
          </property>
         </widget>
        </item>
        <item row="0" column="0" colspan="2">
         <widget class="QTextBrowser" name="chat">
          <property name="tabChangesFocus">
           <bool>true</bool>
          </property>
          <property name="openExternalLinks">
           <bool>true</bool>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
      <widget class="QWidget" name="page_2">
       <layout class="QGridLayout" name="gridLayout_3">
        <item row="0" column="0">
         <widget class="QTextBrowser" name="serverLog">
          <property name="enabled">
           <bool>false</bool>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>800</width>
     <height>30</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuView">
    <property name="title">
     <string>View</string>
    </property>
    <addaction name="actionClient"/>
    <addaction name="actionServer"/>
   </widget>
   <addaction name="menuView"/>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
  <action name="actionClient">
   <property name="text">
    <string>Client</string>
   </property>
  </action>
  <action name="actionServer">
   <property name="text">
    <string>Server</string>
   </property>
  </action>
 </widget>
 <resources/>
 <connections/>
</ui>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...