目前在做的业务,需要为很多的游戏项目写压测脚本。因为没有项目愿意直接提供客户端的网络协议代码,大部分还是愿意提供部分的协议文件的。所以基本上需要从这部分协议文件凭空变出一份压测脚本。

目前的情况是,基于 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 的内容就略略略吧,最近已经想换掉它了。所以,有啥好用的替代方案吗?


↙↙↙阅读原文可查看相关链接,并与作者交流