我正在通过编写聊天客户端来练习 PyQt+Twisted。这需要让两个事件循环发挥得很好。我想弄清楚如何在没有 Twisted QTReactor 的情况下做到这一点。我想要实现的模式是
- Qt 正常运行,Twisted 在另一个线程中运行(比如 QThread)。
- Qt 函数/方法使用
callFromThread
. - 我们连接了 Twisted 给我们的延迟,这样它们就可以发射 Qt 信号。这样Twisted 线程就可以回调Qt 线程。
我实际上已经让那部分工作了。解决方案的核心是这个功能
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback
func is the Twisted function you want to invoke. It probably returns a
deferred. successSignal is the signal you want to emit once the asynchronous
call finishes. This signal will be emitted with the result of func as an
argument
"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
问题是当我得到“未要求的”数据时,我不知道该怎么做。例如,其他人可能会发送聊天消息。我需要能够检测到 Twisted 线程中数据的接收,并以某种方式将该数据输入 Qt 线程。这似乎很棘手,因为在这种情况下,我没有为程序的 Twisted 部分提供用作回调的信号。我怎样才能做到这一点?
下面请找到完整的示例代码来运行我几乎可以工作的聊天客户端和服务器。要使用这些程序,首先将服务器和客户端复制为 .py 文件,并将 ui 规范复制为 .ui 文件。运行服务器,然后运行客户端。客户端窗口打开后,单击“连接”按钮。您应该会在服务器上看到一条消息,指示新连接。然后尝试输入行编辑并单击“发送”按钮。您会看到数据到达服务器并返回,但它没有显示在客户端的 QTextEdit 框中。这是因为我不知道如何将数据从 ChatProtocol 获取到程序的 Qt 部分。我们应该怎么做?
服务器(异步)
import asyncore
import socket
import constants as C
HOST = 'localhost'
PORT = 12344
class ChatServer(asyncore.dispatcher):
"""Receive and forward chat messages
When a new connection is made we spawn a dispatcher for that
connection.
"""
ADDRESS_FAMILY = socket.AF_INET
SOCKET_TYPE = socket.SOCK_STREAM
def __init__(self, host, port):
self.map = {}
self.address = (host,port)
self.clients = []
asyncore.dispatcher.__init__(self, map=self.map)
def serve(self):
"""Bind to socket and start asynchronous loop"""
self.create_socket(self.ADDRESS_FAMILY, self.SOCKET_TYPE)
self.bind(self.address)
print("ChatServer bound to %s %s"%self.address)
self.listen(1)
asyncore.loop(map=self.map)
def writable(self):
return False
def readable(self):
return True
def newMessage(self, data, fromWho):
"""Put data in all clients' buffers"""
print("new data: %s"%data)
for client in self.clients:
client.buffer = client.buffer + data
def handle_accept(self):
"""Deal with newly accepted connection"""
(connSock, clientAddress) = self.accept()
print("New connection accepted from %s %s"%clientAddress)
self.clients.append(ChatHandler(connSock, self.map, self))
class ChatHandler(asyncore.dispatcher):
def __init__(self, sock, map, server):
self.server = server
self.buffer = ''
asyncore.dispatcher.__init__(self, sock, map)
def writable(self):
return len(self.buffer) > 0
def readable(self):
return True
def handle_read(self):
"""Notify server of any new incoming data"""
data = self.recv(4096)
if data:
self.server.newMessage(data, self)
def handle_write(self):
"""send some amount of buffer"""
sent = self.send(self.buffer)
self.buffer = self.buffer[sent:]
if __name__=='__main__':
if HOST is None:
HOST = raw_input('Host: ')
if PORT is None:
PORT = int(raw_input('Port: '))
s = ChatServer(HOST, PORT)
s.serve()
客户端(PyQt + Twisted)
import sys
import PyQt4.QtGui as QtGui
import PyQt4.QtCore as QtCore
import PyQt4.uic as uic
import twisted.internet.reactor as reactor
import twisted.internet.defer as defer
import twisted.internet.protocol as protocol
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.networkThread = NetworkThread()
self.networkThread.start()
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
#Make connections from network thread signals to our slots
self.connect(self.networkThread,
self.networkThread.sigConnected,
self.onConnected)
self.connect(self.networkThread,
self.networkThread.sigNewData,
self.onNewData)
self.ui.show()
def getNetworkConnection(self):
factory = protocol.ClientCreator(reactor, ChatProtocol)
self.networkThread.callFromMain(factory.connectTCP,
self.networkThread.sigConnected,
'localhost', 12344)
def onConnected(self, p):
print("Got a protocol!")
self.cxn = p
def onNewData(self, data):
self.ui.outputBox.append('\r\n'+data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.networkThread.callFromMain(self.cxn.send, None, message)
class NetworkThread(QtCore.QThread):
"""Run the twisted reactor in its own thread"""
def __init__(self):
QtCore.QThread.__init__(self)
self.sigConnected = QtCore.SIGNAL("sigConnected")
self.sigNewData = QtCore.SIGNAL("sigNewData")
def run(self):
reactor.run(installSignalHandlers=0)
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
class ChatProtocol(protocol.Protocol):
def dataReceived(self, data):
print("Got data: %s"%data)
print("...but I don't know how to pass it to Qt :(")
def send(self, data):
self.transport.write(data)
class ChatFactory(protocol.ClientFactory):
protocol = ChatProtocol
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
界面文件
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTextEdit" name="outputBox"/>
</item>
<item>
<widget class="QLineEdit" name="inputBox"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="text">
<string>connect</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>