聊一聊遊戲的壓測

不愧是暗影大人發表於2020-04-18

目前在做的業務,需要為很多的遊戲專案寫壓測指令碼。因為沒有專案願意直接提供客戶端的網路協議程式碼,大部分還是願意提供部分的協議檔案的。所以基本上需要從這部分協議檔案憑空變出一份壓測指令碼。

目前的情況是,基於 locustio 做了一個壓測客戶端。當然 locustio 也只提供了 http 的通訊,socket 通訊這塊還是得自己寫。基本上用到 locustio 本身功能的地方就是 taskset 的建立、管理,還有內建的事件機制,用來統計一些訊息的收發間隔時間。

socket 通訊

基本的需求是非同步 IO,否則併發量上沒法保證。用 python2 比較順手,所以這塊兒就得靠老夥計 gevent 了。

gevent.monkey.patch_all()

monkey_patch 一下,基本的非同步 IO 就完事兒了。至少 send、recv 不阻塞,但是咱是需要同時建立一堆 socket 連線互相無阻塞的收發訊息的。反正傳送是靠 locustio 的 taskset 去往下呼叫的,這塊 locustio 原始功能已經夠用了。剩下接收就不能靠 locustio 來解決,只能自己想辦法了。

_watcher = gevent.get_hub().loop.io(conn.fileno(), 1)

建立一個監聽 socket 讀事件的監聽器,參考 API:
io(fd, events, ref=True, priority=None)¶
Create and return a new IO watcher for the given fd. events is a bitmask specifying which events to watch for. 1 means read, and 2 means write.

每一個 socket 連線成功後,都往 gevent 的主迴圈裡註冊這麼一個監聽器。等有訊息傳送過來的時候,它就能呼叫你註冊的回撥函式。這時候就該你調 conn.recv,收資料包啦。

題外話,因為每一個遊戲都有自己的資料包封包方式,所以回撥函式這塊我都是單獨一個檔案存放的。來活兒了之後,開個分支,把封包、拆包函式寫好扔進去。單獨的檔案比較好管理。

tcp 資料包

一般的資料包都是固定長度的包頭,然後裡面帶有資料包長度。我這邊的習慣是分成兩次去讀,反正需要注意接收到的資料檢查一下長度就行了。如果讀的資料少了一截,那基本上在壓測中算比較常見了,先扔 buffer 裡,等下次再讀拼接上去就是了。

資料包接收完畢,就需要解析資料了。得先找到對應的協議定義,一般包頭裡會帶協議號。那就要求咱維護一份協議號跟協議定義的對映表啦。

又該體外話了,該吐槽吐槽有些時候協議號的定義方式千奇百怪。其中最好處理的還是用 protobuf 的,把協議號寫在 message 的第一個 option 欄位裡。其他的有些寫在單獨的檔案裡,有些放在註釋裡。朋友們,最完犢子的是協議號定義檔案裡寫的不是協議類名而是函式名的,函式當然是跟協議定義對得上的啊。可,我只能靠推理得出函式跟協議類的對應關係。

有了對映表,你就可以正常解析資料包了。通常,還需要寫一個 dispatch 函式,用來按照協議號分發給不同的回撥函式。

封包也差不多,不過是先建立協議物件,然後把協議號扔到包頭裡去。

事件分發

都是為了寫收協議的回撥函式方便,所以統一把接收的協議透過 dispatch 函式按協議號分發出去。

@tcp_callback(RSPID)
def _callback_RSPID(self, response):
    pass

所以寫回撥的時候,用個裝飾器,函式定義完直接註冊進去監聽對應協議號的事件。

同步請求

壓測還需要統計協議收發間隔時間呢,除了協議內容帶有時間戳的情況外。一般都需要在傳送的時候看下錶,然後接收的時候再看下錶,然後在紙上算出時間......才怪。

如果不想每次接收訊息的時候,都去查上一條協議是啥時候發的,就只能採用非同步改同步的寫法了。gevent 的寫法是透過 AsyncResult。

self.async_results[MSGID] = gevent.event.AsyncResult()

當然懶得去打理這個建立的過程,就統統寫在裝飾器內部咯。回撥函式 return 什麼值,就用這個值去 set 對應的 AsyncResult。

self.async_results[MSGID].set(value)

當然是寫在裝飾器內部啦。

def login(self, account, password):
    self.entity_msg(account, password)
    t = time.time()
    response = self.async_results[RSPID].get(block=True, timeout=5)
    self.async_results[RSPID].clear()
    locust.events.request_success.fire(request_type="TCP", name='login', response_time=time.time()-t, response_length=len(response))

@tcp_callback(RSPID)
def _callback_login(self, response):
    return response

上面的 self 當然就是咱的機器人物件啦。機器人類的其他部分略略略咯,也就是在init函式里寫一些例項化管理 Socket 的類的操作。其他部分跟上面這段都差不多的。

這樣的寫法還算是可以接受吧。嗯,其實 login 函式的後三行,都是封裝了函式的,所以寫起來還要更簡短一些。

Locust

嗯,有關 locust 的內容就略略略吧,最近已經想換掉它了。所以,有啥好用的替代方案嗎?

相關文章