背景
最近接觸到用 Twisted 來寫個 RPC 服務器,對高并發、性能和大量長連接時的穩定性方面有要求,所以應該在 Twisted 的基礎上再造些輪子,最后考慮用 Twisted + gevent 來實現 「異步+協程」的部分。
分別簡要介紹下 Twisted 和 gevent。
Twisted
Twisted是用 Python 實現的基于事件驅動的異步的網絡引擎框架。它封裝了大部分主流的網絡協議(傳輸層或應用層),如 TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC以及FTP等,在這我主要會用到 TCP 協議。
使用 Twisted 的好處在于,它是以事件驅動編程實現的,所以提供了事件注冊的回調函數的接口,每次接受到請求,獲得了事件通知,就調用事件所注冊的回調函數( Node.js 程序員可能比較熟悉)。這讓我不必去操心服務器事件驅動的編寫。
并且,在網絡引擎方面,有心跳包和粘包的三方庫,非常完善。
然而,Twisted 有一個缺陷,它的異步有點問題,單個連接建立后是一個進程,在進程里用多線程實現并發,但多個連接建立后仍然會出現同步阻塞的情況,所以這就要引入 gevent 來填充其性能上的缺陷。
gevent
gevent 是一種基于協程的 Python 網絡庫,它用到 greenlet 提供的,封裝了 libevent 事件循環的高層同步API。
如果你不知道什么是協程,那么可以簡單這么理解:
協程就是由程序員自己編碼實現調度的多線程。
而 gevent 對 greenlet 協程進行了封裝,同時 gevent 提供了看上去非常像傳統的基于線程模型編程的接口,但是在隱藏在下面做的是異步 I/O ,所以它以同步的編碼實現了異步的功能。
開搞
Step 1 完成基礎框架
首先由于我要編寫一個 RPC 服務器(使用 TCP 協議),所以需要先實現一個 TCP 服務器。
# server.py
from twisted.internet.protocol import ServerFactory, ProcessProtocol
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor
PORT = 5354
class CmdProtocol(LineReceiver):
client_ip = ''
# 連接建立接口
def connectionMade(self):
# 獲得連接對端 ip
self.client_ip = self.transport.getPeer().host
print("Client connection from %s" % self.client_ip)
# 連接斷開接口
def connectionLost(self, reason):
print('Lost client connection. Reason: %s' % reason)
# 數據接收接口
def dataReceived(self, data):
print('Cmd received from %s : %s' % (self.client_ip, data))
class RPCFactory(ServerFactory):
# 使用 CmdProtocol 與客戶端通信
protocol = CmdProtocol
# 啟動服務器
if __name__ == "__main__":
reactor.listenTCP(PORT, RPCFactory())
reactor.run()
Twisted 提供3個非?;A的接口使程序員進行重寫:
- connectionMade() 連接建立后執行操作
- connectionLost() 連接斷開后執行操作
- dataReceived() 接收到數據后觸發操作
這3個接口通常來說是必須的,以此基礎上進行完善,可以看到我只是先輸出了友好信息。
這樣簡單完成了一個 TCP 服務器,可以看出 Twisted 網絡引擎的架構如下:
- 先由程序員來制定一個或多個協議(該協議可以繼承各種底層網絡協議)。
- 接著指定唯一一個工廠,這個工廠必須聲明使用的協議對象。
- 使用 reactor 選擇監聽模式、監聽工廠和端口,開啟服務器。
Step 2 完善基礎框架
顯然,這個 TCP 服務器基礎框架顯得有些單薄,我首先想到的是需要進行多客戶端的控制及 ip 記錄,故應有個隊列來實時更新連接入服務器的 ip。
并且,最近有好幾部電影在豆瓣我標記了,我想和高圓圓一起去看,所以不能一直盯著屏幕來觀察反饋,所以需要一個日志系統來記錄反饋信息。
故增加一個 log.py 日志系統文件:
# log.py
import os
import logging
import logging.handlers
from twisted.python import log
#當前執行文件所在地址
CURRENT_PATH = os.getcwd()
#日志文件路徑
LOG_FILE = CURRENT_PATH + '/rpcserver.log'
# 全局日志模塊
gl_logger = None
class log(Protocol):
def init_log():
global gl_logger
try:
os.makedirs(os.path.dirname(LOG_FILE))
except:
pass
# 實例化handler
handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1024 * 1024, backupCount=1)
fmt = '[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d:%(funcName)s] - %(message)s'
# 實例化formatter
formatter = logging.Formatter(fmt)
# 為handler添加formatter
handler.setFormatter(formatter)
# 獲取名為rpcserver的logger
gl_logger = logging.getLogger('rpcserver')
# 為logger添加handler
loggergl_logger.addHandler(handler)
handlergl_logger.setLevel(logging.DEBUG)
gl_logger.info("----------------------------------")
并在 server.py 中添加如下代碼:
(添加多連接控制,把 print 替換為 log.msg 來打印日志)
# server.py
from twisted.internet.protocol import ServerFactory, ProcessProtocol
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor
from twisted.python import log
PORT = 5354
class CmdProtocol(LineReceiver):
client_ip = ''
# 連接建立接口
def connectionMade(self):
# 獲得連接對端 ip
self.client_ip = self.transport.getPeer().host
log.msg("Client connection from %s" % self.client_ip)
# 進行多連接控制
if len(self.factory.clients) >= self.factory.clients_max:
log.msg("Too many connections. Disconnect!")
self.client_ip = None
self.transport.loseConnection()
else:
self.factory.clients.append(self.client_ip)
# 連接斷開接口
def connectionLost(self, reason):
log.msg('Lost client connection. Reason: %s' % reason)
if self.client_ip:
self.factory.clients.remove(self.client_ip)
# 數據接收接口
def dataReceived(self, data):
log.msg('Cmd received from %s : %s' % (self.client_ip, data))
class RPCFactory(ServerFactory):
# 使用 CmdProtocol 與客戶端通信
protocol = CmdProtocol
# 設置最大連接數
def __init__(self, clients_max=10):
self.clients_max = clients_max
self.clients = []
# 啟動服務器
if __name__ == "__main__":
log.startLogging(sys.stdout)
reactor.listenTCP(PORT, RPCFactory())
reactor.run()
Step 3 增加 rpc 實例
既然是 rpc 服務器,辣么接下來就要實現一個簡單的遠程命令調用,既然之前寫了日志模塊,那就寫一個對應的遠程日志查看調用吧!
對了,寫到這里,已經是 02:53 了,我不知道為什么開始胡思亂想起來。
我想大概是因為越是無端的,越是會心念著...

嗷,跑題了...
遠程調用呢, Twisted 提供了一個敲好用的子進程父類 ProcessProtocol
這個類提供了2個接口:
- outReceived 用來接收和外發數據
- processEnded 進程結束回調
于是,我在 server.py 中加入以下代碼:
# 打印日志
class TailProtocol(ProcessProtocol):
def __init__(self, write_callback):
self.write = write_callback
def outReceived(self, data):
self.write("Begin logger\n")
data = [line for line in data.split('\n') if not line.startswith('==')]
for d in data:
self.write(d + '\n')
self.write("End logger\n")
def processEnded(self, reason):
if reason.value.exitCode != 0:
log.msg(reason)
循環讀取日志文件中每一行并輸出信息。
接著在 CmdProtocol 類中加入以下函數:
# 根據 cmd 執行相應操作
def processCmd(self, line):
if line.startwith('getlog'):
tailProtocol = TailProtocol(self.transport.write)
# 打印rpcserver.log日志
reactor.spawnProcess(tailProtocol, '/usr/bin/tail', args=['/usr/bin/tail', '-10', '/var/log/rpcserver.log'])
通過獲取遠程發送來的命令 「getlog」 觸發了以下事件 tailProtocol ,并調用 TailProtocol 類中的回調函數 outReceived 來循環讀取日志文件中每一行并輸出日志信息,返回給客戶端。
同理,其余 RPC 遠程調用實例也可類似的編寫。
注意,這里使用了 Twisted 自帶的 spawnProcess()
來處理事件回調,并新建一個線程來執行函數,這就是單個連接中并發的實現。
Step 4 加入 gevent 協程部分
首先我考慮的是使用一個隊列來儲存每次接收到事件觸發的鉤子后,把鉤子接收的參數存入隊列中,再用 gevent 的協程來進行任務的分發。
直接上代碼:
# server.py
import geventfrom gevent.queue
import Queue
# 任務隊列
tasks = Queue()
class CmdProtocol(LineReceiver):
def worker(self, target):
while not tasks.empty():
task = tasks.get()
log.msg('User %s got task %s' % (target, task))
self.processCmd(task)
gevent.sleep(0)
def dispatch(self, data):
tasks.put_nowait(data)
def dataReceived(self, data):
log.msg('Cmd received from %s : %s' % (self.client_ip, data))
gevent.spawn(self.dispatch, data).join()
gevent.spawn(self.worker, self.client_ip)
首先, gevent 的隊列 Queue 有兩個主要的方法 get()
和 put()
來對隊列中的元素進行讀和寫。put_nowait()
相當于 put()
的無阻塞模式。
在 dispatch()
中,我把每個收到的 data 的 trigger 放入任務隊列中,使其進入等待分發的狀態。
接著,協程會執行下一步 worker()
從任務隊列中取出相應的 trigger ,傳入 processCmd
中觸發回調,執行相應的函數。
執行完后,協程會回到上一步 dispatch()
接著再到 worker()
這樣交替輪循,直到任務列表里的任務全部執行完為止,這個過程中,各個任務執行是獨立的,不會造成阻塞,吊!
歐勒!
就醬,我們擼出了一個高性能的、協程的、異步的 RPC 服務器!