聊一聊遊戲的壓測
目前在做的業務,需要為很多的遊戲專案寫壓測指令碼。因為沒有專案願意直接提供客戶端的網路協議程式碼,大部分還是願意提供部分的協議檔案的。所以基本上需要從這部分協議檔案憑空變出一份壓測指令碼。
目前的情況是,基於 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 的內容就略略略吧,最近已經想換掉它了。所以,有啥好用的替代方案嗎?
相關文章
- 聊一聊遊戲版本運營遊戲
- 聊一聊遊戲分級制度的前世今生遊戲
- 聊一聊解謎遊戲的設計(一):解密遊戲的三個維度遊戲解密
- 聊一聊測試流程
- 聊一聊解謎遊戲的設計(六):劇情點綴“純”解謎遊戲遊戲
- 聊一聊解謎遊戲的設計(三):機制與劇情遊戲
- 聊一聊解謎遊戲的設計(二):機制與關卡遊戲
- 聊一聊我對測試開發的看法
- 聊一聊解謎遊戲的設計(四):事件鎖鏈和劇情遊戲事件
- 聊一聊 JVM 的 GCJVMGC
- 聊一聊 RestTemplateREST
- 和手遊開發者聊一聊 iPhoneiPhone
- 聊一聊cc的變化偵測和hook實現Hook
- 聊一聊 Javascript 中的 ASTJavaScriptAST
- 聊一聊 TLS/SSLTLS
- 聊一聊資料庫基準測試那些事資料庫
- 聊一聊Javascript中的Promise物件JavaScriptPromise物件
- 簡單聊一聊Vuex的原理Vue
- 聊一聊Java的列舉enumJava
- 聊一聊MySQL的字符集MySql
- 聊一聊MySQL的儲存引擎MySql儲存引擎
- 聊一聊MySQL的直方圖MySql直方圖
- 聊一聊Redis的離線分析Redis
- 聊一聊Jmeter的引數化JMeter
- 聊一聊橋接(JSBridge)的原理橋接JS
- 聊一聊手遊付費史中月卡的變遷
- 聊一聊前端換膚前端
- 聊一聊session和cookieSessionCookie
- 聊一聊Greenplum與PostgreSQLSQL
- 聊一聊模板方法模式模式
- 聊一聊Iterable與Iterator的那些事!
- 聊一聊泛型的可空性(kotlin)泛型Kotlin
- 聊一聊Spring Bean 的生命週期SpringBean
- 聊一聊RocketMQ的註冊中心NameServerMQServer
- 聊一聊MySQL索引失效的問題MySql索引
- [gRPC]來聊一聊gRPC的認證RPC
- 聊一聊 SQLSERVER 的行不能跨頁SQLServer
- 2019,聊一聊國產遊戲研發圈裡目前主要存在的幾個問題吧遊戲