聊一聊遊戲的壓測

不愧是暗影大人發表於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的內容就略略略吧,最近已經想換掉它了。所以,有啥好用的替代方案嗎?

相關文章