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