И снова чат -9


Привет, Хабр!

После прочтения постов про по созданию чат приложений, я решил попробовать написать свой чат (ну как свой, вот исходники) и прикрутить к нему GUI. Может кому нибудь пригодится, и так начнем. Я использовал Python 3.7 + PyQt5.

Qt Designer




Это основное окно серверной части нашего чата. Здесь у нас 2 поля listWidget, одно для событий на сервере, другое для отображения списка пользователей. 2 pushbutton, одна для отправки сообщений всем пользователям, вторая для остановки сервера. lineEdit для ввода и редактирования сообщений и Label к listWidget.



Это окно с настройками сервера. Здесь 2 pushbutton, их названия говорят сами за себя и 3 textlabel.



Основное окно клиентской части. Textbrowser отображает сообщения от других пользователей, listWidget — список пользователей, lineEdit и pushbutton для ввода редактирования и отправки соответственно.



Если это можно назвать окном авторизации, то пусть будет так. Здесь пользователь вводит данные для входа на сервер.

Клиент и Сервер


Сервер
from socket import *
import threading
import random
import pickle
import queue
from PyQt5.QtCore import QThread, QTimer

class Server(QThread):
    def __init__(self, host, port, clientField, debugField):
        super(Server, self).__init__(parent = None)
        self.connections = []
        self.messageQueue = queue.Queue()
        self.host = host
        self.port = port
        self.clientField = clientField
        self.debugField = debugField

        # создаем socket
        self.serversock = socket(AF_INET, SOCK_STREAM)
        self.serversock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

        # создаем a qtimer для вызова метода sendMsgs каждые 0.5 секунд
        self.timer = QTimer()
        self.timer.timeout.connect(self.sendMsgs)
        self.timer.start(500)

        # запускаем сервер на указанном порте
        try:
            self.port = int(self.port)
            self.serversock.bind((self.host, self.port))
        except Exception as e:
            print(e)
            # если произошла ошибка, попробуйте другой номер порта
            self.port = int(self.port) + random.randint(1, 1000)
            self.serversock.bind((self.host, self.port))

        self.serversock.listen(5)

        debugMsg = "Server running on {} at port {}".format(self.host, self.port) 
        self.debugField.addItem(debugMsg)
        print(debugMsg)

    def run(self):
        while True:
            self.clientsock, self.addr = self.serversock.accept()

            if self.clientsock:
                self.data = self.clientsock.recv(1024)
                self.nickname = pickle.loads(self.data)
                self.clientField.addItem(self.nickname)
                self.connections.append((self.nickname, self.clientsock, self.addr))
                threading.Thread(target=self.receiveMsg, args=(self.clientsock, self.addr), daemon=True).start()
    
    def receiveMsg(self, sock, addr):
        length = len(self.connections)
        debugMsg = "{} is connected with {} on port {} ".format(self.connections[length-1][0], self.connections[length-1][2][0], self.connections[length-1][2][1])
        self.debugField.addItem(debugMsg)

        while True:
            try:
                self.data = sock.recv(1024)
            except:
                self.data = None

            if self.data:
                self.data = pickle.loads(self.data)
                
                
                self.messageQueue.put(self.data)

    def sendMsgs(self):
        while(not self.messageQueue.empty()):
            if not self.messageQueue.empty():
                self.message = self.messageQueue.get()
                self.message = pickle.dumps(self.message)
                if self.message:
                    for i in self.connections:
                        try:
                            i[1].send(self.message)
                        except:
                            debugMsg = "[ERROR] Sending message to " + str(i[0])
                            print(debugMsg)
                            self.debugField.addItem(debugMsg)


Клиент
import socket
import pickle
import sys
from PyQt5.QtCore import QThread

class Client(QThread):
    def __init__(self, host, port, sendbtn, nickname, sendmsg, textbrowser):
        super(Client, self).__init__(parent = None)
        # устанавливаем локальные переменные для предоставленных аргументов
        self.textbrowser = textbrowser
        self.nickname = nickname        # для идентификации сервером и другими клиентами
        self.port = port                # порт, используемый для подключения к серверу
        self.host = host                # имя хоста, используемое для подключения к серверу
        self.sendmsg = sendmsg      # экземпляр поля, в котором набирается текст для отправки, необходимо определить,
                                    # возвращаются ли пользовательские нажатия для отправки сообщения

        # инициализируем переменную в пустую строку
        self.message = ""

        # флаг, который указывает, работает ли клиент
        self.running = True

        # подключаем слот «self.send» к его сигналам
        # при нажатии кнопки "SEND" вызывается метод отправки
        sendbtn.clicked.connect(self.send)
        # метод send вызывается при нажатии Enter в поле редактирования сообщения.
        self.sendmsg.returnPressed.connect(self.send)

        # создаем экземпляр socket
        self.clientsoc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # подключаемся к серверу
        # try except блок, потому что мы можем попытаться подключиться к серверу, который может не работать
        try:
            self.clientsoc.connect((self.host, self.port))
            self.clientsoc.send(pickle.dumps(self.nickname))
            self.textbrowser.append("Connected to server")
        except:
            print("Could not connect to server")        # print error
            self.stop()         # вызываем метод для закрытия соединения с сервером
            sys.exit()          # останавливаем скрипт
        

    # Этот метод будет связан с QThread, когда вызывается унаследованный метод start
    def run(self):
        while self.running:
            self.receive()      # вызываем метод receive

    # метод получения сообщений, пересылаемых сервером
    def receive(self):
        while self.running:
            # try except блок для обработки ситуаций, когда сервер внезапно перестает работать и клиент все еще работает
            try:
                # получение сообщения в byte формате с сервера
                self.data = self.clientsoc.recv(1024)
                # конвертирование из формата byte в список python
                # первый индекс - юзернейм отправителя, второй индекс - сообщение
                self.data = pickle.loads(self.data)
                # если data not None
                if self.data:        
                    # print юзернейм и сообщение
                    print(str(self.data[0] + ": " +self.data[1]))
                    # не показывать сообщение, если вы отправитель
                    if self.data[0] != self.nickname:
                        #self.emit(QtCore.SIGNAL("MESSAGES", self.message))
                        self.textbrowser.append(str(self.data[0] + ": " +self.data[1]))
            except:
                #сделаем всплывающее окно, чтобы показать ошибку
                print("Error receiving")       # print error
                self.stop()                    # вызываем метод для закрытия соединения с сервером
                sys.exit()                     # останавливаем программу

    # метод отправки сообщения, находящегося в данный момент в поле редактирования сообщения, на сервер
    def send(self):
        self.message = self.sendmsg.text()         # текст находящийся в настоящее время в поле редактирования сообщения
        self.textbrowser.append("You: " + self.message)         # показать сообщение в истории сообщений
        self.sendmsg.setText("")                    # установить текст в поле редактирования сообщений на пустую строку
        self.data = (self.nickname, self.message)               # связываем имя отправителя и сообщение в список
        self.data = pickle.dumps(self.data)                     # конвертируем список в byte формат

        # try except блок потому что сервер может не работать
        try:
            self.clientsoc.send(self.data)          # отправить сообщение на сервер
        except:
            print("Error sending message")

    # метод закрытия соединения с сервером
    def stop(self):
        self.running = False        # set flag to false
        self.clientsoc.close()      # close the socket


Сборка


Конвертируем .ui в .py

pyuic5 имя файла.ui -o имя файла.py

Собираем сервер

сервер
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QMessageBox
import serverMain
import serverSetup
import server

class main(QMainWindow, serverMain.Ui_MainWindow):
    def __init__(self, parent=None):
        super(main, self).__init__(parent)
        self.setupUi(self)

        # создаем экземпляры форм
        self.userInput = serverSetup.Ui_Form()
        self.client = self.clientWidget
        self.debug = self.whatsHap

        # создаем виджеты
        self.inputWidget = QWidget(self)  # виджет для отображения ввода номера порта


        # добавляем формы в виджеты
        self.userInput.setupUi(self.inputWidget)

        # show inputWidget, остальное скрываем
        self.inputWidget.setVisible(True)
        self.client.setHidden(True)
        self.debug.setHidden(True)
        self.sendbtn.setHidden(True)
        self.stopbtn.setHidden(True)
        self.globalMsg.setHidden(True)
        self.label.setHidden(True)

        # подключаем метод start_server
        self.userInput.pushButton.clicked.connect(self.server_start)  # при нажатии на кнопку

        self.userInput.lineEdit.returnPressed.connect(self.server_start)  # при нажатии Enter
        # подключаем метод server_stop
        self.stopbtn.clicked.connect(self.server_stop)  # при нажатии на кнопку

        self.userInput.pushButton_2.clicked.connect(self.close) # поключаем метод closeEvent

        self.show()

        # устанавливаем значения по умолчанию
        self.hostname = "127.0.0.1"
        self.port = 8080

    def server_start(self):
        # если номер порта введен, иначе используйте номер порта по умолчанию
        if self.userInput.lineEdit.text() != "":
            self.port = self.userInput.lineEdit.text()

        if self.userInput.pushButton.text() == "CONNECT":
            self.label.setText("CLIENTS")
            self.client.setVisible(True)
            self.debug.setVisible(True)
            self.inputWidget.setHidden(True)
            self.sendbtn.setVisible(True)
            self.globalMsg.setVisible(True)
            self.stopbtn.setVisible(True)
            self.label.setVisible(True)

            self.server = server.Server(self.hostname, self.port, self.client, self.debug)
            self.server.start()
        else:
            self.inputWidget.setVisible(True)
            self.debug.setHidden(True)
            self.client.setHidden(True)
            self.sendbtn.setHidden(True)
            self.stopbtn.setHidden(True)
            self.label.setHidden(True)
            self.globalMsg.setHidden(True)

    def server_stop(self):
        self.inputWidget.setVisible(True)
        self.debug.setHidden(True)
        self.client.setHidden(True)
        self.sendbtn.setHidden(True)
        self.stopbtn.setHidden(True)
        self.label.setHidden(True)
        self.globalMsg.setHidden(True)

    def closeEvent(self, event):
        reply = QMessageBox.question(self, 'Message',
                                     "Are you sure to quit?", QMessageBox.Yes |
                                     QMessageBox.No)

        if reply == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()



if __name__ == "__main__":
    app = QApplication(sys.argv)
    mainWindow = main()
    mainWindow.show()
    sys.exit(app.exec())


вот что будет если setHidden(False)


Клиент
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QMessageBox
import clientMain
import clientSetup
import client
import random

class main(QMainWindow, clientMain.Ui_MainWindow):
    def __init__(self, parent=None):
        super(main, self).__init__(parent)
        self.setupUi(self)
        self.setupWidget = QWidget(self)
        self.config = clientSetup.Ui_Form()
        self.config.setupUi(self.setupWidget)
        self.show()

        # скрываем centralwidget
        self.centralwidget.setHidden(True)

        # устанавливаем значения по умолчанию
        self.hostname = "127.0.0.1"
        self.port = 8080
        self.nickname = "User " + str(random.randint(1, 1000))      # генерируем рандомный юзернейм

        # подключаем метод "self.start" этими сигналами
        self.config.serverf.returnPressed.connect(self.start)    # вызываем метод start при нажатии Enter в поле server
        self.config.portf.returnPressed.connect(self.start)      # вызываем метод start при нажатии Enter в поле port
        self.config.usernamef.returnPressed.connect(self.start)  # вызываем метод start при нажатии Enter в поле username
        self.config.connbtn.clicked.connect(self.start)          # вызываем метод start при нажатии кнопки "CONNECT"
        # подключаем метод closeEvent
        self.config.exitbtn.clicked.connect(self.close)           # Вызов close() отправляет событие close,
                                                                  # которое впоследствии будет доставлено в обработчик
                                                                  # closeEvent окна.

    def start(self):
        # проверяем пользовательский ввод, чтобы определить, использовать ли значения по умолчанию или нет
        if self.config.serverf.text() != "":
            # если пользователь вводит имя хоста, устанавливаем для имени хоста то, что пользователь ввел
            self.hostname = self.config.serverf.text()
        if self.config.portf.text() != "":
            # если пользователь вводит номер порта, устанавливаем номер порта на тот, что пользователь ввел
            self.port = self.config.portf.text()
        if self.config.usernamef.text() != "":
            # если пользователь вводит юзернейм, устанавливаем введенный пользователем юзернейм
            self.nickname = self.config.usernamef.text()

        #self.setWindowTitle(self.nickname + "'s chat")      # тут можно изменить WindowTitle(сотрите '#')
        self.textBrowser.append("Welcome " + self.nickname)       # при входе показывает юзернейм

        print("Nickname ----:", self.nickname)      # print nickname
        print("Hostname: ---:", self.hostname)      # print hostname
        print("Port number -:", self.port)          # print port number

        # создаем экземпляр клиента
        self.client = client.Client(self.hostname, self.port, self.sendbtn, self.nickname, self.sendmsg, self.textBrowser)
        self.client.start()     # вызов метода, унаследованного от QThread, чтобы запустить метод в клиенте

        # скрываем setupWidget после того как пользователь нажал на Enter или кнопку "CONNECT"
        self.setupWidget.setHidden(True)
        # show centralwidget
        self.centralwidget.setVisible(True)

    def closeEvent(self, event):
        reply = QMessageBox.question(self, 'Message',
                                     "Are you sure to quit?", QMessageBox.Yes |
                                     QMessageBox.No)

        if reply == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()

if __name__=="__main__":
    app = QApplication(sys.argv)
    mainWindow = main()         # создаем экземпляр main
    sys.exit(app.exec())        # останавливаем скрипт при выходе из графического интерфейса


Приложение готово к работе!

Небольшое отступление. Я только учусь, и начал изучать python и программирование в целом, примерно 2 месяца назад. В основном ищу уже готовые решения и стараюсь их анализировать, и уже после пытаюсь добавить в них что-то свое. В данном приложении не работает: список пользователей клиентской части, поле для ввода и редактирования серверной части для рассылки глобальных сообщений и кнопка. Но я обязательно доделаю этот функционал.

Буду рад конструктивной критике. Благодарю за потраченное на прочтение время.




К сожалению, не доступен сервер mySQL