最近看到一论坛推广自己写的网页MUD游戏,试玩一下对开发这样的游戏感到了兴趣,顺道研究一下客户端与服务器通讯上是怎么优化的。
首先从 登录页面 开始,通过 Chrome 开发者工具对数据进行抓包,输入错误密码,检查得到是使用 ws 通讯的。
包结构如下:
这是登录时产生的五个数据包,通过这几个包,我们可以看到最前面有几(4)个字节是未知的,然后后面跟着一串JSON(字符串)数据。
既然是字符串和二进制数据混合,那么可以假设前面的二进制包含一个字符串长度的字段。
以握手包为例,(注意,这里 0100 0064
是 二进制数据的 16进制表示,而后面的JSON字符串由于已知,就直接显示原字符串了,一下类同,空格是为了方便查看添加的)
0100 0064 {"sys":{"type":"js-websocket","version":"0.0.1","rsa":{},"protoVersion":"2zWPnSlRsRxjqgcU215Uxg=="}}
数了一下,除了开头4个字节(0100 0064
)外,后面的JSON字符串长度为100字符,换算16进制为 0x64,正好对应了第4个字节64。虽然不确定,但是暂时可以猜测应该是这个(后面的其他数据包可以印证)
好,继续看其他的数据包有什么规律没。
握手返回包 JSON字符串36个字符,0x24。
0100 0024 {"code":200,"sys":{"useProto":true}}
空白包,疑似确认
0200 0000
登录包 假如 0x66 是数据包长度,那么这里 JSON 也应该是 102 个字符,可是这里的 JSON 只有 72 个字符。
继续往前加,签名那串字符串也才 27 个字符,加起来才 99 个字节,还差三个,但是加上 0001 1b
就正好了,所以 0001 1b
可能也属于后面的内容。
0400 0066 0001 1b gate.gateHandler.queryEntry{"login_email":"Test1","login_pwd":"123451","code":"fdfdd","is_r":false}
自此,可以猜测前面4个字节中至少有一个字节是表示后面内容长度的,但是考虑内容长度很容易就超过255字节,所以这个长度应该不止一个字节,但是是往前拓展还是往后拓展,暂时不确定。
由于数据包太少不够参考,我们登陆成功看下后续的数据包。
登陆成功时的数据包
0400 0125 0401 {"code":200,"host":"127.0.0.1","port":13053,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IlRlc3QxIiwibWFwIjo5LCJpZCI6IjVmMDg3MTE0MGRmNDJmMDZmYTQ3MTBiYSIsIm5pY2tuYW1lIjoiVGVzdDEiLCJpYXQiOjE1OTQ2MTk4NjAsImV4cCI6MTU5NDYxOTkyMH0.09FxTtK2i7WOgYnPGeLFIPL6xrMZTxFmAUwUHNfHiII","mid":9}
登陆成功后,会断开 ws 链接重新建立一个新端口的链接,所以应该是登陆服务器和游戏服务器在不同的服务器/端口上。
新的链接依然会有一个握手包和上面的一样,但握手返回的包不一样。
握手返回包
0100 2339 {"code":200,"sys":{"heartbeat":3,"dict":{"chat.chatHandler.send":1,"connector.entryHandler.enter":2,...
握手确认包
0200 0000
登录包
0400 0104 0101 0011 {"email":"Test1","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IlRlc3QxIiwibWFwIjo5LCJpZCI6IjVmMDg3MTE0MGRmNDJmMDZmYTQ3MTBiYSIsIm5pY2tuYW1lIjoiVGVzdDEiLCJpYXQiOjE1OTQ2MTk4NjAsImV4cCI6MTU5NDYxOTkyMH0.09FxTtK2i7WOgYnPGeLFIPL6xrMZTxFmAUwUHNfHiII"}
收到空白包
0300 0000
收到推送包
0400 2f30 0401 {"code":200,"msg":"success","data":{"userTasks":...
由于返回的内容比较长,就没全部显示,握手返回包与收到推送包是一个纯 JSON 字符串,长度为 9017(0x2339) 字符,不用猜了,第3-4字节一定是内容长度。而且怀疑 0100
中 01
应该是包的类型,或者seq,因为发 0100
, 收 0100
,接着发 0200 0000
但没有返回。
但是接下来发的包就不是 0300 了,而是 0400,而且后续的几乎所有包,都是 0300
或 0400
开头,所以猜测为数据包类型。
通过查看后续大量的数据包,总结规律,0300 0000
应该是心跳包,固定收发相同内容。
然后有内容的数据包都是 0400
开头,既然如此,我们暂时可以推测数据包格式如下。
{Type}00 {BodyLength} {Body}
- Type 一个字节 01=握手 02=握手确认(典型TCP三次确认) 03=心跳 04=数据包
- BodyLength 2个字节 表示 Body 的内容长度
- Body 正文
而 {Body} 中有时候前面还会有一段未知数据段 0401
、0101 0011
、0001 1b gate.gateHandler.queryEntry
这三个个还不一样,但是有一个有字符串,依然猜测需要有地方来表示字符串长度,而 gate.gateHandler.queryEntry
长度为 27 (0x1b),那就不用猜了,就是这个位置了,而这个字符串应该就是概念意义上的路由了。
之前返回了字典,这个字符串也出现在了字典里,合理怀疑字典就是路由的map,用来减少数据包长度的。但位置未知。
继续分析后面的大量数据包,发现前面第二位在逐渐递增,合理怀疑为SEQ,这里讲解一下为什么会有SEQ这个东西,
0400 003d 0607 onLeave{"route":"onLeave","uid":"5f02ad4dd6827e0184c1316c"}
0400 00b6 0609 onChatMsg{"msg":"恭喜 <span style='color:red;'>lll </span> 捕捉<span style='color:red;'> <span style='color:purple;'>粉红海兔</span></span> -> 成功","type":1,"channel":0}
0400 000d 0102 0024 {"mid":9}
0401 614e 0402 {"data":{"...
在我们普通 http 中,可能没这个概念,因为我们的 http 请求某种意义上来说是阻塞的,就是你发送一个请求以后会一直等待这个链接返回数据,在返回之前,你无法继续用这个链接发送其他数据。所以我们不会有数据包弄混的情况。
但是在 TCP 或者 ws 中,数据都是异步的,所以如果同时向服务器发送多个数据包,然后服务器又返回好几个,怎么和原来的请求对应上呢?所以这里就有一个 SEQ 的概念,全称叫做“Sequence Number”,感兴趣的可以查看一下相关资料,这也是 TCP 中,或者异步通讯中经常出现的一个概念。具体我也只是知道,不能说的太多。
然后 Body 的第一个字节,总是有一定的规律,和后面的格式有一定关联,所以猜测应该是标记后面的格式或者类型的。
至于 Type = 01 时 后面两个字节,猜测和 Dict 有关系,和几个事件对了一下,和 Dict 中描述的是一致的,所以应该是压缩字典。
所以目前猜出的 Body 包中格式大概如下:
{Type} {SeqId} {TypeArgs} {Data}
其中格式如下
Type 1字节 对应 TypeArgs 的结构
00
发送{TypeLength} {TypeString}
01
发送{DictID}
04
返回 无 TypeArgs06
返回{TypeLength} {TypeString}
- SeqId 1字节
- Data 数据,为 JSON 字符串。
至此,分析结果已经明了。
这次分析也看到,一些框架为了减少数据流大小,对Route也是尽可能压缩,通过字典来压缩。
而作者将登陆鉴权与游戏分成了两个部分,而且游戏服务器的端口也登录后返回的,猜测是为了以后把服务器分开而设计的。
具体分析结果见 Github
请问一下,二进制头部那几个我用了好几种解码都解不出来是啥,你是如何解出来是这些数据的,还有登陆成功时的数据包0401{"code":200,"host":"127.0.0.1","port":13053,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IlRlc3QxIiwibWFwIjo5LCJpZCI6IjVmMDg3MTE0MGRmNDJmMDZmYTQ3MTBiYSIsIm5pY2tuYW1lIjoiVGVzdDEiLCJpYXQiOjE1OTQ2MTk4NjAsImV4cCI6MTU5NDYxOTkyMH0.09FxTtK2i7WOgYnPGeLFIPL6xrMZTxFmAUwUHNfHiII","mid":9}16进制长度应该为127,你写的的125是不是弄错了?还是我的方式有错?
看来是你弄错了
0401 是两个字节 不是字符串