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