尊重原创,原文链接
关于PyQt5+Python3开发环境安装可以参考上篇。这里实现的是一种群聊工具,socket类使用的是Qt的TcpSocket, TcpServer 类;线程类使用的QTread;收发数据使用的QDataStream(当然也可以用其他的方式,不唯一)
先看服务器端, 服务端界面设计,下面标注红色的是后面代码中要使用的控件,其它的可要可不要 # Ui_server.py
-
# -*- coding: utf-8 -*-
-
#
-
# Created by: PyQt5 UI code generator 5.9.2
-
#
-
# WARNING! All changes made in this file will be lost!
-
-
from PyQt5 import QtCore, QtGui, QtWidgets
-
class Ui_Form(object):
-
def setupUi(self, Form):
-
Form.setObjectName( "Form")
-
Form.resize( 541, 355)
-
self.splitter_2 = QtWidgets.QSplitter(Form)
-
self.splitter_2.setGeometry(QtCore.QRect( 10, 10, 516, 331))
-
self.splitter_2.setOrientation(QtCore.Qt.Vertical)
-
self.splitter_2.setObjectName( "splitter_2")
-
self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
-
self.layoutWidget.setObjectName( "layoutWidget")
-
self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget)
-
self.horizontalLayout.setContentsMargins( 0, 0, 0, 0)
-
self.horizontalLayout.setObjectName( "horizontalLayout")
-
self.label = QtWidgets.QLabel(self.layoutWidget)
-
self.label.setObjectName( "label")
-
self.horizontalLayout.addWidget(self.label)
-
self.txtPort = QtWidgets.QLineEdit(self.layoutWidget)
-
self.txtPort.setObjectName( "txtPort")
-
self.horizontalLayout.addWidget(self.txtPort)
-
self.btnCreate = QtWidgets.QPushButton(self.layoutWidget)
-
self.btnCreate.setObjectName( "btnCreate")
-
self.horizontalLayout.addWidget(self.btnCreate)
-
self.splitter = QtWidgets.QSplitter(self.splitter_2)
-
self.splitter.setOrientation(QtCore.Qt.Horizontal)
-
self.splitter.setObjectName( "splitter")
-
self.layoutWidget1 = QtWidgets.QWidget(self.splitter)
-
self.layoutWidget1.setObjectName( "layoutWidget1")
-
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget1)
-
self.verticalLayout.setContentsMargins( 0, 0, 0, 0)
-
self.verticalLayout.setObjectName( "verticalLayout")
-
self.lbUserInfo = QtWidgets.QLabel(self.layoutWidget1)
-
self.lbUserInfo.setObjectName( "lbUserInfo")
-
self.verticalLayout.addWidget(self.lbUserInfo)
-
self.listUser = QtWidgets.QListWidget(self.layoutWidget1)
-
self.listUser.setObjectName( "listUser")
-
self.verticalLayout.addWidget(self.listUser)
-
self.layoutWidget2 = QtWidgets.QWidget(self.splitter)
-
self.layoutWidget2.setObjectName( "layoutWidget2")
-
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget2)
-
self.verticalLayout_2.setContentsMargins( 0, 0, 0, 0)
-
self.verticalLayout_2.setObjectName( "verticalLayout_2")
-
self.chatInfo = QtWidgets.QLabel(self.layoutWidget2)
-
self.chatInfo.setObjectName( "chatInfo")
-
self.verticalLayout_2.addWidget(self.chatInfo)
-
self.browerChat = QtWidgets.QTextBrowser(self.layoutWidget2)
-
self.browerChat.setObjectName("browerChat")
-
self.verticalLayout_2.addWidget(self.browerChat)
-
-
self.retranslateUi(Form)
-
QtCore.QMetaObject.connectSlotsByName(Form)
-
-
def retranslateUi(self, Form):
-
_translate = QtCore.QCoreApplication.translate
-
Form.setWindowTitle(_translate( "Form", "Form"))
-
self.label.setText(_translate( "Form", "ADDRESS:PORT:"))
-
self.btnCreate.setText(_translate( "Form", "create"))
-
self.lbUserInfo.setText(_translate( "Form", "用户列表"))
-
self.chatInfo.setText(_translate( "Form", "聊天信息"))
-
-
if __name__ == "__main__":
-
import sys
-
app = QtWidgets.QApplication(sys.argv)
-
Form = QtWidgets.QWidget()
-
ui = Ui_Form()
-
ui.setupUi(Form)
-
Form.show()
-
sys.exit(app.exec_())
在服务器端,使用QTcpServer类监听接入客户端,对于QTcpServer类,一旦有用户接入,就调用
其incomingConnection函数 。要使用多线程的话,就要在incomingConnection中调用线程类QThread,生成新的线程,
然后再新的线程run函数中调用QTcpSocket实例,进行数据的收发,在这里我也把QTcpSocket类重新实现了,也可以就在
重载的Thread实现QTcpSocket收发的代码,代码如下:
-
# -*- coding: utf-8 -*-
-
-
"""
-
Module implementing Server.
-
"""
-
from PyQt5.QtCore import *
-
from PyQt5.QtWidgets import *
-
from PyQt5.QtNetwork import *
-
from Ui_server import Ui_Form
-
import sys
-
-
PORT = 26711
-
SIZEOF_UINT16 = 2
-
-
class TcpSocket(QTcpSocket):
-
'''
-
实现了客户端读取信息与发送信息到客户端的功能
-
'''
-
#信号用于Thread类中,信号在接受客户端信息后发送,一直发送到Server更新其界面
-
#上级为Thead, 先将其交付给连接Thread中的slotRecv
-
signRecv= pyqtSignal(str, str)
-
def __init__(self, socketId, parent=None):
-
super(TcpSocket, self).__init__(parent)
-
self.socketId = socketId
-
#客户端发送信息就接受
-
self.readyRead.connect(self.slotRecv)
-
-
#接受信息槽函数, client-> slotRecv -> Server
-
def slotRecv(self):
-
#使用QDataStream接受信息,也可以选择其他接受方式
-
while self.state() == QAbstractSocket.ConnectedState:
-
nextBlockSize = 0
-
stream = QDataStream(self)
-
if self.bytesAvailable() >= SIZEOF_UINT16:
-
nextBlockSize = stream.readUInt16()
-
else:
-
#print('cannot read client')
-
break
-
if self.bytesAvailable() < nextBlockSize:
-
break
-
action = ''
-
msg = ''
-
action = stream.readQString()
-
msg = stream.readQString()
-
clientAddress = self.peerAddress().toString()
-
clientPort = str(self.peerPort())
-
msg= clientAddress+ ':'+clientPort + ' '+ msg+ '\n'
-
-
#发射给上级Thread
-
self.signRecv.emit(action, msg)
-
-
#发送信息槽函数,其变量数据来源于上级 Server -> slotSend -> client
-
def slotSend(self, action, msg, id):
-
#print(id)
-
#print(int(self.socketId))
-
if id == int(self.socketId):
-
reply = QByteArray()
-
stream = QDataStream(reply, QIODevice.WriteOnly)
-
stream.writeUInt16( 0)
-
stream.writeQString(action)
-
stream.writeQString(msg)
-
stream.device().seek( 0)
-
stream.writeUInt16(reply.size() - SIZEOF_UINT16)
-
self.write(reply)
-
-
class Thread(QThread):
-
'''
-
线程类,在run中创建socket变量, 然后充当中介,向下交付slotSend中参数,
-
向上交付slotRecv中得到的参数
-
'''
-
#被TcpServer 使用,在类中发射
-
signRecv = pyqtSignal(str, str)
-
#在类中使用, 连接socket类中的slotSend
-
signSend = pyqtSignal(str, str, int)
-
-
-
def __init__(self, socketId, parent):
-
super(Thread, self).__init__(parent)
-
self.socketId = socketId
-
-
def run(self):
-
socket = TcpSocket(self.socketId)
-
if not socket.setSocketDescriptor(self.socketId):
-
return
-
#socket类中的signRecv信号在此连接,把socket接受到的信息作为参数传递给Thread的slotSend
-
socket.signRecv.connect(self.slotRecv)
-
#Thread类中的signSend连接socket中slotSend, 将从上级得到的信息传递给socket,最后发送到客户端
-
self.signSend.connect(socket.slotSend)
-
-
#循环,不加这一句会出问题
-
self.exec_()
-
-
-
def slotSend(self, action, msg, id):
-
if action == '':
-
return
-
#发射到socket中
-
self.signSend.emit(action, msg, id)
-
-
def slotRecv(self, action, msg):
-
#发射到TcpServer
-
self.signRecv.emit(action, msg)
-
-
class TcpServer(QTcpServer):
-
signRecv= pyqtSignal(str, str)
-
signSend = pyqtSignal(str, str, int)
-
#存放客户端socket的socketId
-
socketList = []
-
def __init__(self, parent=None):
-
super(TcpServer, self).__init__(parent)
-
-
-
def incomingConnection(self, socketId):
-
#一有客户端进来就加入列表,当然在此要判断列表是否存在该客户端
-
self.socketList.append(socketId)
-
-
#创建线程
-
thread = Thread(socketId, self)
-
#thread发射后,调用tcpserver的slotRecv ,然后其又发射自身的signRecv信号,该信号在Server中连接Server的slotRecv
-
thread.signRecv.connect(self.slotRecv)
-
#连接thread的slotSend
-
self.signSend.connect(thread.slotSend)
-
thread.finished.connect(thread.deleteLater)
-
thread.start()
-
-
def slotRecv(self, action, msg):
-
self.signRecv.emit(action, msg)
-
-
-
-
class Server(QWidget, Ui_Form):
-
"""
-
Class documentation goes here.
-
"""
-
signSend = pyqtSignal(str, str, int)
-
def __init__(self, parent=None):
-
"""
-
Constructor
-
-
@param parent reference to the parent widget
-
@type QWidget
-
"""
-
super(Server, self).__init__(parent)
-
self.setupUi(self)
-
-
self.server= TcpServer(self)
-
self.server.listen(QHostAddress( "0.0.0.0"), PORT)
-
#tcoServer中的signRecv信号,连接自身的slotRecv
-
self.server.signRecv.connect(self.slotRecv)
-
-
-
def slotRecv(self, action, msg):
-
self.updateServer(action, msg)
-
for id in self.server.socketList:
-
self.server.signSend.emit(action, msg, id)
-
-
def showConnection(self):
-
print(self.server.socketList)
-
-
#def slotSend(self, action, msg):
-
-
-
def updateServer(self, action, msg):
-
if action == 'USER':
-
self.listUser.addItem( '*user*: '+ msg)
-
if action == 'MSG':
-
self.browerChat.append(msg)
-
-
-
def on_btnCreate_clicked(self):
-
"""
-
Slot documentation goes here.
-
"""
-
# TODO: not implemented yet
-
-
if __name__ == '__main__':
-
app = QApplication(sys.argv)
-
serverDlg = Server()
-
serverDlg.show()
-
app.exec_()
客户端实现, 客户端类似服务器端,只不过多了一个发送消息的LineEdit, 取名为txtInput, 发送按钮btnSend
代码(Ui_client.py):
-
# -*- coding: utf-8 -*-
-
-
#
-
# Created by: PyQt5 UI code generator 5.9.2
-
#
-
# WARNING! All changes made in this file will be lost!
-
-
from PyQt5 import QtCore, QtGui, QtWidgets
-
-
class Ui_Form(object):
-
def setupUi(self, Form):
-
Form.setObjectName( "Form")
-
Form.resize( 455, 366)
-
self.listUser = QtWidgets.QListWidget(Form)
-
self.listUser.setGeometry(QtCore.QRect( 0, 10, 161, 341))
-
self.listUser.setObjectName( "listUser")
-
self.splitter_2 = QtWidgets.QSplitter(Form)
-
self.splitter_2.setGeometry(QtCore.QRect( 180, 10, 256, 341))
-
self.splitter_2.setOrientation(QtCore.Qt.Vertical)
-
self.splitter_2.setObjectName( "splitter_2")
-
self.setNameWidget = QtWidgets.QWidget(self.splitter_2)
-
self.setNameWidget.setObjectName( "setNameWidget")
-
self.horizontalLayout = QtWidgets.QHBoxLayout(self.setNameWidget)
-
self.horizontalLayout.setContentsMargins( 0, 0, 0, 0)
-
self.horizontalLayout.setObjectName( "horizontalLayout")
-
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
-
self.horizontalLayout_3.setObjectName( "horizontalLayout_3")
-
self.lbName = QtWidgets.QLabel(self.setNameWidget)
-
self.lbName.setObjectName( "lbName")
-
self.horizontalLayout_3.addWidget(self.lbName)
-
self.txtName = QtWidgets.QLineEdit(self.setNameWidget)
-
self.txtName.setObjectName( "txtName")
-
self.horizontalLayout_3.addWidget(self.txtName)
-
self.btnSet = QtWidgets.QPushButton(self.setNameWidget)
-
self.btnSet.setObjectName( "btnSet")
-
self.horizontalLayout_3.addWidget(self.btnSet)
-
self.horizontalLayout.addLayout(self.horizontalLayout_3)
-
self.browerChat = QtWidgets.QTextBrowser(self.splitter_2)
-
self.browerChat.setObjectName( "browerChat")
-
self.layoutWidget = QtWidgets.QWidget(self.splitter_2)
-
self.layoutWidget.setObjectName( "layoutWidget")
-
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget)
-
self.horizontalLayout_2.setContentsMargins( 0, 0, 0, 0)
-
self.horizontalLayout_2.setObjectName( "horizontalLayout_2")
-
self.txtInput = QtWidgets.QLineEdit(self.layoutWidget)
-
self.txtInput.setObjectName( "txtInput")
-
self.horizontalLayout_2.addWidget(self.txtInput)
-
self.btnSend = QtWidgets.QPushButton(self.layoutWidget)
-
self.btnSend.setObjectName( "btnSend")
-
self.horizontalLayout_2.addWidget(self.btnSend)
-
-
self.retranslateUi(Form)
-
QtCore.QMetaObject.connectSlotsByName(Form)
-
-
def retranslateUi(self, Form):
-
_translate = QtCore.QCoreApplication.translate
-
Form.setWindowTitle(_translate( "Form", "Form"))
-
self.lbName.setText(_translate( "Form", "name"))
-
self.txtName.setText(_translate( "Form", "deafault"))
-
self.btnSet.setText(_translate( "Form", "SetConnection"))
-
self.btnSend.setText(_translate( "Form", "Send"))
-
-
-
if __name__ == "__main__":
-
import sys
-
app = QtWidgets.QApplication(sys.argv)
-
Form = QtWidgets.QWidget()
-
ui = Ui_Form()
-
ui.setupUi(Form)
-
Form.show()
-
sys.exit(app.exec_())
-
# -*- coding: utf-8 -*-
-
-
"""
-
Module implementing Client.
-
"""
-
-
from PyQt5.QtCore import *
-
from PyQt5.QtWidgets import *
-
from PyQt5.QtNetwork import *
-
-
from Ui_client import Ui_Form
-
import sys
-
import time
-
-
PORT = 26711
-
SIZEOF_UINT16 = 2
-
-
class Client(QWidget, Ui_Form):
-
"""
-
Class documentation goes here.
-
"""
-
def __init__(self, parent=None):
-
"""
-
Constructor
-
-
@param parent reference to the parent widget
-
@type QWidget
-
"""
-
super(Client, self).__init__(parent)
-
self.setupUi(self)
-
-
-
self.socket = QTcpSocket()
-
-
self.request = None
-
#self.nextBlockSize = 0
-
self.name = ''
-
-
#信号
-
#self.socket.connected.connected.connect(self.sendRequest)
-
self.socket.readyRead.connect(self.readResponse)
-
self.socket.disconnected.connect(self.serverHasStopped)
-
self.socket.error.connect(self.serverHasError)
-
#if self.socket.isOpen() == False:
-
#self.socket.connectToHost("localhost", PORT)
-
-
-
-
def on_btnSend_clicked(self):
-
"""
-
Slot documentation goes here.
-
"""
-
# TODO: not implemented yet
-
if self.socket.isOpen() :
-
#self.socket.disconnected.emit
-
self.socket.close()
-
self.socket.connectToHost( "localhost", PORT)
-
msg =self.name + ' '+\
-
time.strftime( '%Y-%m-%d %H:%M:%S',time.localtime(time.time()))+ '\n'+\
-
self.txtInput.text()
-
self.issueRequest( 'MSG', msg)
-
self.txtInput.setText( '')
-
-
-
def on_txtInput_returnPressed(self):
-
"""
-
Slot documentation goes here.
-
"""
-
# TODO: not implemented yet
-
pass
-
-
-
def on_btnSet_clicked(self):
-
"""
-
Slot documentation goes here.
-
"""
-
if self.socket.isOpen() :
-
self.socket.close()
-
self.socket.connectToHost( "localhost", PORT)
-
# TODO: not implemented yet
-
self.name = self.txtName.text()
-
self.issueRequest( 'USER', self.name)
-
-
def issueRequest(self, action, msg):
-
print( 'sendRequest')
-
self.request= QByteArray()
-
stream = QDataStream(self.request, QIODevice.WriteOnly)
-
stream.writeUInt16( 0)
-
stream.writeQString(action)
-
stream.writeQString(msg)
-
stream.device().seek( 0)
-
stream.writeUInt16(self.request.size() - SIZEOF_UINT16) #overwrite seek(0)
-
-
self.socket.write(self.request)
-
self.nextBlockSize = 0
-
self.requst = None
-
-
def readResponse(self):
-
nextBlockSize = 0
-
stream = QDataStream(self.socket)
-
while 1:
-
if nextBlockSize == 0 :
-
if self.socket.bytesAvailable() <SIZEOF_UINT16:
-
break
-
nextBlockSize = stream.readUInt16()
-
if self.socket.bytesAvailable() < nextBlockSize:
-
break
-
action = ''
-
msg = ''
-
action = stream.readQString()
-
msg = stream.readQString()
-
print(msg)
-
if action == 'MSG':
-
self.browerChat.append(msg)
-
if action == 'USER':
-
self.listUser.addItem( '*user*: '+ msg)
-
-
-
def serverHasStopped(self):
-
#self.issueRequest('CLOSE', self.name)
-
#self.socket.close()
-
print( 'socket is close')
-
-
def serverHasError(self, error):
-
-
self.socket.close()
-
-
-
if __name__ == '__main__':
-
app = QApplication(sys.argv)
-
dlg = Client()
-
dlg.show()
-
sys.exit(app.exec_())
之前的设计中经常出现Cannot create children for a parent that is in a different thread
Socket notifiers cannot be enabled or disabled from another thread等问题
但是以上代码是都解决了这些问题,当然还有很多不足之处,比如说用户列表那一块,懒得搞了,以后再说吧