• Eli Bendersky's website » Code sample – socket client based on Twisted with PyQt


    Eli Bendersky's website » Code sample – socket client based on Twisted with PyQt

    Code sample – socket client based on Twisted with PyQt

    May 26th, 2011 at 5:28 am

    In an earlier post, I discussed one way of combining blocking socket I/O with a GUI, by means of a separate thread in which the I/O runs. I’ve also mentioned that one alternative is using asynchronous I/O with callbacks integrated into the GUI event loop. Here I want to present some sample code for this alternative.

    One of the first things Python programmers will think about then considering asynchronous I/O is Twisted, which is an event-driven networking library written in pure Python. Twisted is a huge library, and it’s quite a job to learn how to use it properly. Here I’ll demonstrate just enough to accomplish the task at hand – re-create the simple socket client from the previous post that works as part of a PyQt GUI.

    The full code sample is available for download – socket_client_twisted_pyqt.zip. Here is the socket client class using Twisted:

    import struct
    
    from twisted.internet.protocol import Protocol, ClientFactory
    from twisted.protocols.basic import IntNStringReceiver
    
    
    class SocketClientProtocol(IntNStringReceiver):
        """ The protocol is based on twisted.protocols.basic
            IntNStringReceiver, with little-endian 32-bit
            length prefix.
        """
        structFormat = "<L"
        prefixLength = struct.calcsize(structFormat)
    
        def stringReceived(self, s):
            self.factory.got_msg(s)
    
        def connectionMade(self):
            self.factory.clientReady(self)
    
    
    class SocketClientFactory(ClientFactory):
        """ Created with callbacks for connection and receiving.
            send_msg can be used to send messages when connected.
        """
        protocol = SocketClientProtocol
    
        def __init__(
                self,
                connect_success_callback,
                connect_fail_callback,
                recv_callback):
            self.connect_success_callback = connect_success_callback
            self.connect_fail_callback = connect_fail_callback
            self.recv_callback = recv_callback
            self.client = None
    
        def clientConnectionFailed(self, connector, reason):
            self.connect_fail_callback(reason)
    
        def clientReady(self, client):
            self.client = client
            self.connect_success_callback()
    
        def got_msg(self, msg):
            self.recv_callback(msg)
    
        def send_msg(self, msg):
            if self.client:
                self.client.sendString(msg)
    

    A couple of notes:

    • Since I want to re-create the same length-prefixed protocol from the previous post, the IntNStringReceiver protocol class from twisted.protocols.basic comes in handy – it’s designed especially for strings prefixed by integer length headers. In our case, the prefix is a 4-byte little-endian unsigned integer, which I specify with the structFormat and prefixLength attributes. In addition I implement a couple of callbacks of the IProtocol interface.
    • SocketClientFactory is a straightforward subclass of Twisted’s ClientFactory, implementing the callbacks we’re interested in here.

    This is all quite simple. The bigger problem was finding how to interface Twisted with PyQt. Since Twisted and a typical GUI library are both event-loop based, to make them work together we should use a custom reactor. Unfortunately, due to licensing issues, Twisted doesn’t come with a reactor for PyQt pre-packaged, and it should be obtained separately. Even more unfortunately, the PyQt reactor (qt4reactor.py) doesn’t appear to have a single well-defined address on the web – several slightly different versions of it can be found floating online. In my code sample I’ve included a version of qt4reactor.py which I found to work for my needs.

    So, back to the code. This is the implementation of the PyQt client GUI that uses Twisted. Similarly to the thread-based sample, it keeps drawing a nice circle animation to demonstrate it’s never blocked. The full code is in the zip archive, here is only the interesting part:

    class SampleGUIClientWindow(QMainWindow):
        def __init__(self, reactor, parent=None):
            super(SampleGUIClientWindow, self).__init__(parent)
            self.reactor = reactor
    
            self.create_main_frame()
            self.create_client()
            self.create_timer()
    
        def create_main_frame(self):
            self.circle_widget = CircleWidget()
            self.doit_button = QPushButton('Do it!')
            self.doit_button.clicked.connect(self.on_doit)
            self.log_widget = LogWidget()
    
            hbox = QHBoxLayout()
            hbox.addWidget(self.circle_widget)
            hbox.addWidget(self.doit_button)
            hbox.addWidget(self.log_widget)
    
            main_frame = QWidget()
            main_frame.setLayout(hbox)
    
            self.setCentralWidget(main_frame)
    
        def create_timer(self):
            self.circle_timer = QTimer(self)
            self.circle_timer.timeout.connect(self.circle_widget.next)
            self.circle_timer.start(25)
    
        def create_client(self):
            self.client = SocketClientFactory(
                            self.on_client_connect_success,
                            self.on_client_connect_fail,
                            self.on_client_receive)
    
        def on_doit(self):
            self.log('Connecting...')
            # When the connection is made, self.client calls the on_client_connect
            # callback.
            #
            self.connection = self.reactor.connectTCP(SERVER_HOST, SERVER_PORT, self.client)
    
        def on_client_connect_success(self):
            self.log('Connected to server. Sending...')
            self.client.send_msg('hello')
    
        def on_client_connect_fail(self, reason):
            # reason is a twisted.python.failure.Failure  object
            self.log('Connection failed: %s' % reason.getErrorMessage())
    
        def on_client_receive(self, msg):
            self.log('Client reply: %s' % msg)
            self.log('Disconnecting...')
            self.connection.disconnect()
    
        def log(self, msg):
            timestamp = '[%010.3f]' % time.clock()
            self.log_widget.append(timestamp + ' ' + str(msg))
    
        def closeEvent(self, e):
            self.reactor.stop()
    
    
    #-------------------------------------------------------------------------------
    if __name__ == "__main__":
        app = QApplication(sys.argv)
    
        try:
            import qt4reactor
        except ImportError:
            # Maybe qt4reactor is placed inside twisted.internet in site-packages?
            from twisted.internet import qt4reactor
        qt4reactor.install()
    
        from twisted.internet import reactor
        mainwindow = SampleGUIClientWindow(reactor)
        mainwindow.show()
    
        reactor.run()
    

    The most important part of this code is in the last section, where the Twisted reactor and PyQt application are set up. A few steps have to be performed in a careful order:

    • An QApplication is created
    • qt4reactor is imported and installed into Twisted
    • The main window is created
    • Finally, the singleton Twisted reactor (which is actually a qt4reactor, since that’s the one we’ve installed) is run

    Note that there’s no app.exec_() here, contrary to what you’d expect from a PyQt program. Since both PyQt and Twisted are based on event loops (in app.exec_() and reactor.run(), respectively), one of them should drive the other. The Twisted way is to let the reactor drive (hence we only call reactor.run() here). Inside its implementation, qt4reactor takes care of running an event loop in a way that dispatches events to both Twisted and PyQt.

    Some additional notes:

    • The PyQt main window no longer needs a timer to query results from the client thread. When SocketClientFactory is created in create_client, some methods are passed to it as callbacks. These callbacks will be invoked when interesting events happen.
    • Even though Twisted’s reactor is a global singleton object, it’s good practice to pass it around in the application, instead of importing it from Twisted in multiple places. Here, SampleGUIClientWindow accepts the reactor object in its constructor and uses it later.
    • Twisted’s reactor keeps running until explicitly stopped. The user of a GUI expects the program to exit then the GUI is closed, so I call reactor.stop() in the main window’s closeEvent.

    This is it. Once set up, Twisted integrates quite nicely with PyQt. Since both the GUI framework and Twisted are based on the concept of an event loop with callbacks, this is a natural symbiosis.

    A final note: I’m very far from being a Twisted expert. In fact, this is the first time I really use it, so may be doing things not in the most idiomatic or optimal way. If you can recommend a better way to implement some parts of this sample, I’ll be very happy to hear about it.

    Related posts:

    1. Code sample – socket client thread in Python
    2. Sample using QScintilla with PyQt
    3. New-style signal-slot connection mechanism in PyQt
    4. Boost.Asio with Protocol Buffers code sample
    5. Passing extra arguments to PyQt slots
  • 相关阅读:
    ES6常用语法简介
    webpack核心概念
    前端模块化规范详解
    使用Node.js原生代码实现静态服务器
    Node.js脚手架express与前段通信【socket】
    临门一脚- Node.js
    redis缓存穿透和雪崩
    redis哨兵模式
    redis主从复制
    redis发布订阅
  • 原文地址:https://www.cnblogs.com/lexus/p/2852630.html
Copyright © 2020-2023  润新知