Twisted + gevent 異步+協程服務器開發

背景

最近接觸到用 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 網絡引擎的架構如下:

  1. 先由程序員來制定一個或多個協議(該協議可以繼承各種底層網絡協議)。
  2. 接著指定唯一一個工廠,這個工廠必須聲明使用的協議對象。
  3. 使用 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 服務器!

rpcserver
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容