Twisted

情境

前陣子 JS 碰久了,自然就好奇 Python 的異步如何開發,所以研究了一下 Twisted。

環境設定

1
2
3
4
5
(venv)$ pip install twisted 

# 測試方式
>>> import twisted
>>> twisted.__version__

如果想要使用 Twisted 的 SSL 功能,需要安裝:

1
2
3
4
5
6
7
(venv)$ pip install pyopenssl
(venv)$ pip install service_identity

# 測試方式
>>> import OpenSSL
>>> import twisted.internet.ssl
>>> twisted.internet.ssl.SSL

如果沒有安裝 service_identity,應該會看這個警告:

1
2
>>> import twisted.internet.ssl
:0: UserWarning: You do not have a working installation of the service_identity module: 'No module named service_identity'. Please install it from <https://pypi.python.org/pypi/service_identity> and make sure all of its dependencies are satisfied. Without the service_identity module, Twisted can perform only rudimentary TLS client hostname verification. Many valid certificate/hostname mappings may be rejected.

如果想要使用 Twisted 的 SSH 功能,需要安裝:

1
2
3
4
5
6
(venv)$ pip install pycrypto

# 測試方式
>>> import Crypto
>>> import twisted.conch.ssh.transport
>>> twisted.conch.ssh.transport.md5

開發資源

直接從官方網站的文件下手應該是最好的。The complete developer guide 這份文件又依照各功能分門別類,還提供範例程式,是熟悉上手的好選擇。

echo_server & echo_client

echo_server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
def dataReceived(self, data):
self.transport.write('ping {}'.format(data))

class EchoFactory(protocol.Factory):
def buildProtocol(self, addr):
return Echo()

reactor.listenTCP(8000, EchoFactory())
reactor.run()

echo_client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
from twisted.internet import reactor, protocol

class EchoClient(protocol.Protocol):
def connectionMade(self):
self.transport.write("Hello, WWW.")

def dataReceived(self, data):
print( 'Server said: {}'.format(data) )
time.sleep(1)
self.transport.write("pong {}.".format(time.time()))
#self.transport.loseConnection()


class EchoFactory(protocol.ClientFactory):
def buildProtocol(self, addr):
return EchoClient()

def clientConnectionFailed(self, connector, reason):
print( 'connnection failed: {}'.format(reason) )

def clientConnectionLost(self, connector, reason):
print( 'connection lost: {}'.format(reason) )
reactor.stop()

reactor.connectTCP("localhost", 8000, EchoFactory())
reactor.run()

從 echo_server 和 echo_client 的例子,可以開始最簡單的異步程式。要小心繼承來的方法名稱不要打錯字,否則什麼事情都不會發生,而且還可能不會報錯。我就不小心把 echo_client 的 connectionMade 打錯了,中間沒有任何事情發生,包括錯誤,就只能靠經驗除錯。從這點可以很清楚看到 Python 在繼承上的不足,JAVA 早期也是有這樣的問題,後來透過 annotation 解決了。查了資料,有些人會自製 decorator 來避免這樣的問題。

Twisted 基本觀念

Reactor

Reactor 是 Twisted 的核心,負責與底層實作的中間溝通部分,讓上層呼叫只要透過統一的介面即可使用。他等待並且解析 network、file system、timer 事件,將這些事件分配到對應的事件處理器。

基本上做了這樣的事情:

1
2
3
4
5
while True:
# 取得接下來要到期事件
# 取得現在已經可以處理的事件

# 處理這些事件

ReactorlistenTCP 和 connectTCP 是用來註冊要在 TCP Socket 可以讀取資料時,用來接獲通知的 callback 。reactor.run 用來啟動事件迴圈,除非呼叫 react.stop,否則事件迴圈就會一直處理下去。

Transports

Transport 代表的是透過網路溝通兩端點之間的連線。Transport 代表的是 TCP、UDP、Unix sockets、serial ports。Transport 實作 ITransport ,有這些方法:

  • write: 使用 nonblocking 方法將資料寫至實體連線。
  • writeSequence: 在 line-oriented protocols 時,可以一次寫多筆的字串到實體連線。
  • loseConnection: 把所有資料寫出,然後關閉連線。
  • getPeer: 取得遠端端點的位址。
  • getHost: 取得本機端點的位址。

Protocols

Protocol 決定如何異步處理網路事件。Protocol 實作了 IProtocol

  • makeConnection: 用來建立連線。
  • connectionMade: 連線建立時,會被呼叫。
  • dataReceived: 收到資料時,會被呼叫。
  • connectionLost: 斷線時會被呼叫。

Protocol Factories

Protocol 實體在每條 connection 建立時初始化,每條 connection 斷線時就跟著結束。這代表用來連線的永久(persistent)設定資訊,不會存放在 Protocol 實體中。所以有一個 Factory 這個角色負責維護超越多個 Protocol 實體的設定資訊是必然的。每個 Factory 的 buildProtocol 用來為每條 connection 創建新的 Protocol,之後就將這個 Protocol 丟到 Reactor 去註冊 callback。Server/Client 分別使用 protocol.Factory/protocol.ClientFactory 的子類。這邊使用了 Factory Method Pattern ,算是一定規模以上的專案常見的手法。