osx平台上lol英雄联盟launcher启动器的分析实现 我算个LOL玩家吧,虽然是郊区白银段位的,虽然能1打5个电脑,但瘾不小。每次想打LOL,都找不到PC电脑,看着OSX却不能玩国服,只能玩美服、韩服。而外服的网络延迟比较大,根本没法打,跳帧严重,野怪都打不过。每次瘾来了时,总是想在OSX上也能玩,次数多了,决定试试,看看能否在OSX上玩国服。 15年10月份对LOL英雄联盟录像做过解析器在mac osx上看lol国服ob录像的技术分析,文中最后提到过关于LOL客户端架构的组成,以及在OSX上打LOL国服还是有可能的。 现状分析,League of legends有mac版本,在riot拳头运营的地区中,有韩服、美服、欧服之类国外服务器。而腾讯运营国服版,却没有OSX版本。上次的录像分析中,是腾讯运营的windows版本产生的录像文件,可以在OSX版本的美服上解析、渲染、播放,也就以为着协议是相通的,也就意味着用riot运营的OSX客户端,连接腾讯运营的服务器,应该可以正常通讯,正常游戏的。 先来看下国服League of legends英雄联盟的目录结构 Air目录为LolClient.exe所在目录,即游戏大厅目录,air的,也就是flash as的,意味着反编译比较简单... Config目录确定 Cross目录为腾讯游戏必装的各种乱七八糟工具包目录 Game目录为game进程League of legends.exe所在目录 log目录,我也不知道这是记录啥日志的,一直是空的 logs目录,这是launcher启动器记录通讯数据的目录 TCLS目录是腾讯登录SSO的客户端程序目录 TQM目录,腾讯自己的,跟游戏无关 先来对比Windows跟osx上区别,riot运营的(以下以美服代表)游戏,是先选地区,再进入登录界面,再进入游戏。而腾讯运营的是先登录,再选择大区,再进入游戏。显然,腾讯运营的选大区,可以理解为riot运营的选地区,之后再进入游戏。区别是riot先选区,再登录;腾讯(以下简称国服)的先登录,再选区。先看国服在windows上流程,快捷方式打开的是TCLS\Client.exe,内嵌登录的DLL,登录之后,选大区,直接开启LolClient.exe进入对应大区游戏。而美服的登录窗口是在LolClient.exe中完成的,不难看出区别是美服手动登录,国服自动登录。显然,是LolClient.exe启动时,CLI参数包含了一些KEY,来实现了自动登录。 也就是说,问题在登录认证部分,只要我在OSX上能实现LolClient.exe的自动登录就可以...当然要在windows上获取登录的Game Signature Key,先来看下win上的进程启动信息: 果然是CLI中参数带有signature key,参数如下: 可以看出,除了Air运行时参数外,还有8393数字,认证的KEY,当前大区服务器域名,xmpp聊天服务器地址,登录服务器地址之类。另外,在看下LolClient.exe的进程树: 如上图,游戏大厅进程LolClient.exe跟game进程League of legends.exe都是lol.launcher_tencent.exe的子进程。 这里的8393数字其实是LolClient.exe要连接的TCP端口,也就是lol.launcher_tencent.exe进程监听的端口, lol.launcher_tencent.exe进程再fork起LolClient.exe进程,进入游戏大厅。 游戏开局匹配时,都是在LolClient.exe中进行的,匹配完成之后 ,lol.launcher_tencent.exe又createprocess出Game\League of Legends.exe进入游戏。 那么,只要我在osx上实现一个launcher的功能,就能在osx上玩League of legends英雄联盟的国服了,感觉很兴奋的样子,继续往下看...... LolClient.exe进程进入游戏后,lol.launcher_tencent.exe进程是如何知道的呢?它又是如何决定启动League of Legends.exe进程的?在上篇在mac osx上看lol国服ob录像的技术分析中,提到过,game进程启动时,命令行参数有个端口8394,先查看进程监听列表 从结果中,可以看到进程lol.launcher_tencent.exe监听了本地的8393、8394、8395端口,而且,LolClient.exe已经连接了8393端口,也就是其启动时命令行参数中的对应数值。那么另外两个端口8394、8395也应该是其他进程来连接的咯。抓本地数据包,看看他们之间有没有通讯,如看看他们通讯了什么内容:(在windows上,wireshark抓不到回环地址通讯包,可以用rawcap来抓包) 之后,wireshark打开这个文件,可以看到如下TCP数据包: 从协议包结果上来看,对比发送、接收的4个包作为例子,不难看出,他们的数据包格式拆分为前8bytes10 00 00 00 01 00 00 00为固定值(暂且认为是固定值);后面4bytes04 00 00 00、05 00 00 00为一段;再后面的4bytes00 00 00 00、34 00 00 00为另一段;最后一部分的Nbytes长度31 34 2e 31 37 2e 32 33 2e 39 34 20 35 31 35 36 20 51 62 77 41 34 73 6f 42 38 4a 71 37 43 4f 45 51 2b 31 78 63 34 67 3d 3d 20 34 30 30 36 33 31 39 32 33 32为最后一段;以第4个通讯包来看,第三段的对应数值是0x34,显然是LittleEndian小端字节序,显然后面的00000020 - 00000054部分数据长度也是0x34个,也就是说,协议通讯格式的第三段表示后面消息包长度的含义,第四段就是主体消息正文。第一段、第二段都是消息头的部分。第二段多数为command指令。暂且这么假设,那么通讯协议包格式确定了,接下来就要确认下每个command对应的业务含义了。先看下LolClient.exe进程发给launcher,跟League of legends.exe发给launcher进程的TCP数据包,初步确认下我对协议包格式的判断是否正确 如何查找command的类型一共有多少种?如何确认每种command的意义?记得最初LOL目录结构里提到大厅客户端LolClient.exe是flash air的,反编译比较方便,入口文件是LolClient.swf,跟下去是mod/win/ClientWindow.dat,然后加载了&^*($#@#,as代码太乱了,而且,都分布在各个小的flash中,目录还不统一,拓展名还都是dat的,还得手动改成swf,反编译工具才能选它,as代码也不好理解,检索麻烦,索性直接反编译lol.launcher_tencent.exe进程吧。 IDA的导入lol.launcher_tencent.exe进程后,在imports中看到 Maestro_Remove、...等来自RiotLauncher这个library 再静态分析RiotLauncher.dll,在Exports里看到Maestro_MessageTypeToString函数跟进,看到MAESTROMESSAGETYPE_GAMECLIENT_CREATE等字样 ,F5导出 至此,可以看到lol launcher的协议指令类型都在这里。再看看哪里调用了Maestro_MessageTypeToString函数: 再看看MaestroMessageAgent_SendMessage的调用处: 从MaestroGameController_SendGameConnectedToServerMessage函数名的字面含义上理解,此函数为『当游戏连接到服务器时,发送消息』,继续跟进...可是,我静态分析水平比较弱,越是继续跟进,我的思路越混乱,决定另辟蹊径。 在文中最初的LOL目录结构中,有个Logs目录,这个目录下Maestro Logs目录中的maestro-server.log、maestro-game_client.log中有一些日志,是开发人员用来排查调试程序执行结果的信息,大约如下: 这里确实可以对应着launcher协议中,某些command被接收后,会被记录到这里。如何确认每个日志记录指令对应是哪个TCP包发送后的结果呢?如何利用这些日志信息呢?如果windows上有类似linux的strace查看系统调用的工具就好了,你还别说,还真有一款功能类似的,就是ProcessMonitor,有了这工具,我就可以看到lol.launcher.tencent.exe进程在何时发送TCP数据包,发送后写了哪个文件数据,写了什么内容,先写日志还是先发TCP数据包。。。 如上图,可以看到先发TCP包,还是先写日志文件,哪个进程发,哪个进程写。但还有一点要确认的是,发的TCP包的内容如何跟写的日志内容配对问题,显然,ProcessMonitor工具里有些更有用的信息,你可能没注意到进程每次发包后,会记录发的包大小;进程每次写入文件数据时,会记录写时的offset以及写入文件长度,根据launcher进程写日志时,获取的offset位置,可以找到对应maestro-server.log文件的offset,再根据写入文件时的length数值,可以确定写入内容是什么。那么,机智的你不难看出,写入日志内容的字面含义,很容易确认了TCP数据包中对应的command代表的含义,也明白了command代表的业务逻辑。 好了,lol.launcher_tencent.exe的功能大约理清楚了,再来回顾下League of Legends的客户端架构,以及猜测下服务端架构,根据我的理解,我画了一副图 架构图中的虚线部分,为服务器之间节点通讯,是我的猜测,并不是想客户端部分那样,按照分析客户端通讯得来的架构图。可能不准确。 匹配请求处理服务器,用来处理客户端的登录认证、符文、天赋修改、英雄列表,进入战斗的匹配请求等。 匹配服务器,应该是整个大区用同一个,这里会预算所有与大区玩家相关的数据,战斗力匹配,寻找想匹配对手,划分到对应服务器中。这是核心点,也是瓶颈点。 房间管理服务器,应该要独立拆分出来,与匹配服务器解耦,与战场服务器解耦。当匹配服务器完成后,只要将匹配结果的10名玩家,玩家的符文、天赋配置等发送给房间管理服务器,然后由房间管理服务器去创建对战进程,再获取对战服务器的IP PORT 以及每个人的加密KEY(也应该是认证该角色的凭证),比如"14.17.23.94 5156 QbwA4soB8Jq7COEQ+1xc4g== 4006319232"发送给每一个客户端。客户端拿到消息,再发送给lol.launcher.tencent.exe,再有它启动游戏进程。 房间管理服务器都是可以横向扩展的,尤其是游戏服务器,更是可以横向扩展,单个游戏进程,只跟本次游戏中的10个玩家有关。 port范围5000-5500,也就是说单台服务器最多开500个端口,也就是500场战斗,以每场战斗10个人来算,单台服务器是5000人的承载。实际上可能要依赖服务器的性能承载。15年初网上消息LOL同时在线750W,时隔1年,国服现在同时在线有多少人?能承载多少玩家同时在线?匹配服务器虽然是很难拆分的节点,但业务比较单一,CPU密集型,应该大概也许可能不难解决。其他节点,拓展起来就更方便了,堆堆服务器应该可以解决。当然,我没坐过类似的客户端游戏架构,只是基于页游、手游的经验猜。 回到正题,LOL launcher的业务确定了,与另外两个进程通讯的协议格式也确定了,看看实现时,我要怎么做 监听系统信号的线程 监听TCP 8393端口,等待LolClient.exe连接 监听TCP 8394端口,等待League of legends.exe连接 监听TCP 8395端口?暂时不监听了,目前不知道这端口是干啥的 启动client的线程 保持与client的心跳线程 启动game的线程 保持跟game的心跳线程 接收来自game消息的心跳线程 处理client消息转发到game的线程 处理game消息转发到client的线程 解析协议数据包,获取command,数据包长度,数据包内容 分析command,心跳包直接回复。与game无关消息包,直接回复MAESTROMESSAGETYPE_REPLY类型已收到该消息。 部分消息需要转发给game进程,走chan转发,比如进入游戏后,好友还是可以在游戏大厅发消息到正在游戏中的玩家的,聊天消息转发MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME 就是从client发到launcher,再有launcher转发至game的。 处理client消息转发到game的线程、处理game消息转发到client的线程 当然,还得再启动两个线程来接收launcher转发来的消息, 等等等等,一系列代码写完后,本以为终于可以在osx上玩LOL国服了,没想到,我还是太年轻,想的太简单了。程序启动LolClient.exe之后就假死了,一番排查后,发现是TCP粘包问题 尼玛,是LolClient.exe发送给launcher.exe的TCP数据包分了两个TCP包发送的,尼玛,100个字节都不到,还分两个字节,尼玛。。累不累? 一系列代码写完后,本以为终于可以在osx上玩LOL国服了,没想到,我还是太年轻,想的太简单了。我写的lol launcher启动Lolclient.exe没问题,符文、天赋编辑也都没问题。但启动League of legends.exe时,却进程崩溃退出了,而我却百思不得其解,为什么?通信协议一样的,发送command时机一样的,为什么会崩溃呢?这突然的失败,让我思路中断,思绪全无,不知道如何进行下去。。。。。。冥冥中,我记得在静态分析lol.launcher.tencent.exe时,看到了一个特殊的函数名再回到ProcessMonitor中看看League of legends.exe启动时的环境变量是什么 显然是__COMPAT_LAYER=ElevateCreateProcess(不要问为什么显然...我也曾经懊恼过,上学时,老师总是说显然,可我就是不明白为什么显然) 在OSX上,也找下环境变量是什么获取LeagueOfLegends.app进程执行时的环境变量,去除系统默认的几个,剩下的,就是游戏自己添加的 一系列代码写完后,本以为终于可以在osx上玩LOL国服了,没想到,我还是太年轻,想的太简单了。除了程序执行时的环境变量外,还有一个程序执行时的当前目录: 记得要chdir一下,这么改进后,本以为终于可以在osx上玩LOL国服了,没想到,它确实在windows上登录国服,终于运行起来了 它也在OSX上登录美服,成功运行起来了 那么我只要把OSX上启动LolClient的命令行参数改为国服的参数,就可以登录国服,玩国服游戏咯,尝试一下果然可以,我情不自禁的开始佩服我的聪慧,机智,下一步就开房间,匹配打国服了?然而,我还是太年轻,想的太简单了。程序启动League of Legend.app时出错了,进不去,日志中记录 之后,游戏进程就报错退出了。我太年轻了,想的太简单了......可是,我好像已经不年轻了...... 这到底是为什么,玩个游戏怎么这么难,这么难?我只是想玩个游戏而已,为什么这么难?腾讯LOL项目组的朋友,能不能告诉我,你们还验证了什么?很明显不是版本号,是不是操作系统?是不是拒绝了OSX的登录? 曾经,我曾尝试过从协议上入手,看看LOL游戏协议能不能搞定,可难度比较大,花了好几个星期的空余时间,大约确认了是UDP Enet协议格式,后来,也在网上搜到相关信息,确认了。但网上公开的协议格式,是4.23以前的,现在都6.*的版本,早就改了....唉,以后再说吧... 最近两天,总结了之前研究LOL Launcher的实现过程,疏于工作,拖延了工作进度,我自己也是内心有愧。有愧的不仅仅是会议上的苍白答复,也还有这篇总结本计划与2015年底写完的,也还是拖延到现在。 总结完这边博文,接下来要写部门的年终总结了,离丙申年到来还有5天,犯有严重拖延症的我,能把我自己的年终总结,在乙未年结束之前写完吗? 参考资料: Evolving the Server-Side Architecture of League of Legends IntWars Scaling League of Legends Chat to 70 million Players 代码开源: 2016-02-03 +16 20 0
在mac osx上看lol国服ob录像的技术分析 一天,兴起而至,想在mac osx上看lol ob,要看国服的,没现成方案,实现不了,未遂,便决定研究一番。 在拳头游戏官方的api中SPECTATING GAMES riot games api提到观看ob模式的程序启动参数: "C:\Riot Games\League of Legends\RADS\solutions\lol_game_client_sln\releases\0.0.1.74\ deploy\League of Legends.exe" "8394" "LoLLauncher.exe" "" "spectator spectator.na.lol.riotgames.com:80 P+C3YqI3Mg9oHc6t9eTAKWE4T8prxwzR 1726229459 NA1" 程序启动的第三个参数中: 这里面包括ob server地址、端口,以及本场录像文件加密key,以及游戏id,游戏所属平台id。其中 encryptionKey参数每次都不一样,从数据包分析发现,这个key并不是为了在获取录像时的验证,我觉得是roitgames为了安全,为每场游戏的协议数据包做的加密key,客户端观看ob 录像时,用这个key解密,以提高协议安全度。 在riotgames官方api介绍上,以及github上几个开源的lol项目上,以及本人wireshark抓包分析总结出一场ob游戏观看,一共调用了如下http api接口 GET /observer-mode/rest/consumer/version //获取录像文件版本信息,返回字符串 GET /observer-mode/rest/consumer/getGameMetaData/:region/:id/*ignore //获取游戏的基本信息,包括游戏当前桢,最后一个chunkid,游戏开始时间等信息。返回json格式字符串 GET /observer-mode/rest/consumer/getLastChunkInfo/:region/:id/:end/*ignore //获取录像中最后一个chunk基本信息,包括了当前chunkid,下一个chunkid,当前桢的id之类信息,返回json格式字符串 GET /observer-mode/rest/consumer/getGameDataChunk/:region/:id/:chunkId/*ignore //获取对应chunkid的协议数据,返回字节流 GET /observer-mode/rest/consumer/getKeyFrame/:region/:id/:frame/*ignore //获取对应chunkid的协议数据,返回字节流 lol ob观战模式的实现,是将游戏中所有玩家操作,合并成一个chunk块,来发送到客户端的,以减少通讯量。 GET /record/:region/:gameId/:encryptionKey 这个api在github上有提到过,但我抓包过程中,没有看到被调用过。暂时忽略。 至此已经知道lol 英雄联盟的观战通讯过程,但全球大区中,我们腾讯服务器跟欧美服务器不一样,并没有公开的ob server地址。而是在tgp腾讯游戏助手中实现的。 这是个嵌入的html,真实地址是 http://api.tgp.qq.com/spectator/v1410/watch.shtml ,对应的ob 观战列表文件为 http://api.pallas.tgp.qq.com/core/get_ob_list ,以ob文件http://ob.pallas.tgp.qq.com/ob_data/1_1632443320.ob 来举例子。对于国外其它服务器的观战,是连接到官方的观战服务器进行的。而腾讯lol的观战,却是将录像文件下载到本地的,那是否可说明tgp助手在本地建立了一个http服务器,并解析ob文件,实现ob观战服务器的功能呢? 为此开始确认这个过程,在tgp腾讯游戏助手上观战了这场游戏,查看游戏进程的启动参数中,ob server地址是多少,开Process Monitor抓进程启动参数,开tgp助手观战: 如上图所示,启动参数复合riotgame的api介绍,我们来确认下127.0.0.1: 36722 是否正确,以及是哪个进程开启的。 replay.exe进程启动详情: 腾讯tgp助手,确实在本地开启http ob server来实现的。那么进一步抓包验证。对于回环地址,wireshark抓不到的,要用RawCap来抓。 同时,wireshark 抓取 ob录像文件下载的数据包,查看localhost.pcanpng ,果然是lol ob录像系统调用的几个http请求。 一一查看version\getGameMetaData\getLastChunkInfo\getGameDataChunk\getKeyFrame几个请求。也确实以相应字符串、json字符串、字节流的形式返回,但数据源头来自哪里呢?肯定是最初的ob文件了。 如上图,其中开头的每行分隔的字符串,、json字符串也比较好理解,里面的元素也是lol ob录像系统请求相应的json字符串 LOLOB/1.0 abstract:{"src": 1, "area_id": 1, "score": 5625000, "game_length": 1260, "battle_type": 4, "max_tier": [0, 0], "game_id": 1632443320, "start_time": "2015-10-21 08:13:37", "ob_ver": "1.82.89", "encryption_key": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"} source:{"gameId": 1632443320, "gameStartTime": 1445386417603, "platformId": "HN1", "gameMode": "CLASSIC", "mapId": 11, "gameType": "MATCHED_GAME", "gameQueueConfigId": 4, "observers": {"encryptionKey": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"}, "participants": [{"profileIconId": 7, "championId": 60, "summonerName": "\u5fc3\u6001\u597d\u80fd\u4e0a\u5206\u5417", "skinIndex": 2, "bot": false, "spell2Id": 11, "teamId": 100, "spell1Id": 4}, {"profileIconId": 15, "championId": 117, "summonerName": "\u4e5d\u670813\u65e5\u4e36", "skinIndex": 5, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 841, "championId": 58, "summonerName": "\u501a\u7af9\u8f7b\u8bed\u767d\u886b\u5982\u6545", "skinIndex": 2, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 17, "championId": 429, "summonerName": "\u840c\u515c\u515cTcT", "skinIndex": 2, "bot": false, "spell2Id": 7, "teamId": 100, "spell1Id": 4}, {"profileIconId": 914, "championId": 122, "summonerName": "\u91cd\u5e86\u897f\u5357\u5927\u5b66\u6821\u8349", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 14}, {"profileIconId": 922, "championId": 105, "summonerName": "L4ugh1ng", "skinIndex": 8, "bot": false, "spell2Id": 12, "teamId": 200, "spell1Id": 4}, {"profileIconId": 1, "championId": 28, "summonerName": "\u5170\u4ead\u5e8f", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 11}, {"profileIconId": 9, "championId": 203, "summonerName": "\u8292\u5e02\u9189\u9999\u56ed\u996d\u5e84", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 12}, {"profileIconId": 931, "championId": 110, "summonerName": "15\u5c81\u5357\u660c\u4eba\u670d\u4e0d\u670d", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 7}, {"profileIconId": 28, "championId": 432, "summonerName": "\u6218\u4e36\u65d7TV\u767d\u8272\u98ce\u8f66", "skinIndex": 1, "bot": false, "spell2Id": 14, "teamId": 200, "spell1Id": 4}], "gameTypeConfigId": 2, "gameLength": 5, "bannedChampions": [{"teamId": 100, "championId": 41, "pickTurn": 1}, {"teamId": 200, "championId": 157, "pickTurn": 2}, {"teamId": 100, "championId": 82, "pickTurn": 3}, {"teamId": 200, "championId": 114, "pickTurn": 4}, {"teamId": 100, "championId": 4, "pickTurn": 5}, {"teamId": 200, "championId": 76, "pickTurn": 6}]} obmeta:{"lastChunkId": 14, "gameKey": {"platformId": "HN1", "gameId": 1632443320}, "startTime": "Oct 21, 2015 8:13:37 AM", "endGameKeyFrameId": -1, "gameLength": 0, "keyFrameTimeInterval": 60000, "port": 0, "gameServerAddress": "", "interestScore": 2989, "clientBackFetchingEnabled": false, "clientAddedLag": 30000, "endStartupChunkId": 7, "chunkTimeInterval": 30000, "clientBackFetchingFreq": 1000, "pendingAvailableKeyFrameInfo": [], "decodedEncryptionKey": "", "createTime": "Oct 21, 2015 8:10:24 AM", "featuredGame": true, "gameEnded": false, "encryptionKey": "", "delayTime": 150000, "endGameChunkId": -1, "pendingAvailableChunkInfo": [], "startGameChunkId": 9, "lastKeyFrameId": 3} keyframe_tab:[[1, 9], [2, 11], [3, 13], [4, 15], [5, 17], [6, 19], [7, 21], [8, 23], [9, 25], [10, 27], [11, 29], [12, 31], [13, 33], [14, 35], [15, 37], [16, 39], [17, 41], [18, 43], [19, 45], [20, 47], [21, 49]] chunk_tab:[[1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 30018], [10, 30008], [11, 29986], [12, 30007], [13, 30000], [14, 29985], [15, 30019], [16, 29981], [17, 30000], [18, 30008], [19, 30003], [20, 29982], [21, 30012], [22, 30011], [23, 29980], [24, 29993], [25, 30018], [26, 29982], [27, 30018], [28, 29984], [29, 30020], [30, 29984], [31, 30016], [32, 29980], [33, 29994], [34, 30009], [35, 30016], [36, 29997], [37, 29976], [38, 30006], [39, 30008], [40, 29985], [41, 30000], [42, 30027], [43, 29972], [44, 30006], [45, 30016], [46, 29977], [47, 30024], [48, 29990], [49, 29994], [50, 20015]] 不难看出,其中“LOLOB/1.0”为tgp助手本身识别的版本信息;abstract是ob录像版本,以及录像数据加密key存储的基本信息;obmeta对应着getGameMetaData的数据;keyframe_tab跟chunk_tab暂时不知道是什么意思,可稍后再回来看。 ob文件的剩下其它部分,显然就是录像中,每个chunk包的录像数据的了,那它是如何拼装的呢? 腾讯tgp助手下载的ob文件中,录像部分开头的字节流是01 00 01 00 00 35 88 85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b,回头看看lol游戏进程获取ob录像chunk部分时数据包的内容开头部分 开头的字节流是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b 7d,相比之下,重复部分是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98...,再多找几个录像获取数据的数据包,核对一下 可以确认,lol的ob录像回放协议中,协议头是85 a0 4a 30 1b 30 27 d5,也就是说,tgp下载的ob文件字节流部分,85 a0 4a 30 1b 30 27 d5前面的都不是lol协议的,这开头的01 00 01 00 00 35 88这7个字节为tgp读取识别的协议头部分。最初replay.exe程序读取理解的部分,本身不是lol客户端读取的协议部分。那么,这01 00 01 00 00 35 887个字节分别代表什么含义呢? 那我们开始找85 a0 4a 30 1b 30 27 d5出现的位置即可(关于在数M数据中,查找几个字节所在位置的地方,你最好有比较简便的方法,千万别告诉我你是一行一行看的。) 可以看出如下结果: 第一个在 00001124 部分,前面7个字节是01 00 01 00 00 35 88; 第二个在 000046B3 部分,前面7个字节是02 00 01 00 00 2b 10; 第三个在 000071CA 部分,前面7个字节是01 00 02 00 00 60 48; (wireshark会告诉你当前行所在的位置编号,记得加上所关注字节的偏移位置) 到了这里,根据编程灵(经)感(验),会这么猜想:第二个位置跟第一个位置相差了46B3-1124=358F个位置,也就是3588+7=358F(tgp协议头中,最后两个字节3588 + 协议头长度 7),用第三个跟第二个最比较以确认猜想对不对。71CA-46B3=2B17,即2B10+7符合猜测。 到此,可确认tgp协议头的7个字节中,最后2个字节表示这后续包的长度。但这后续包长度是否仅仅是后面2个字节所标示的,是不是后面3字节,还是4字节?还不能确定。能确定的是,开头的3个字节肯定不会表示后续包长度作用。如果你还是没发现这规律,那可不能怪我了,谁让你当时没有灵(jing)感(yan)呢? 如此一来,可以用更快的方法验证这个猜想,写一段程序,从ob录像文件中,分析tgp协议头,根据后2个字节对应长度的数据包,打印出前面tgp协议头的7字节,再打印后面8个字节(确认是否为85 a0 4a 30 1b 30 27 d5) [1 0 1 0 0 53 136] [133 160 74 48 27 48 39 213 59 235 178 18 77 140 13 98 82 11 21 159 209 122 212] [2 0 1 0 0 43 16] [133 160 74 48 27 48 39 213 54 53 74 75 93 254 127 91 102 147 88 122 160 207 84] [1 0 2 0 0 96 72] [133 160 74 48 27 48 39 213 240 224 50 221 162 250 17 161 77 240 23 76 184 68 252] [2 0 2 0 0 78 80] [133 160 74 48 27 48 39 213 137 33 26 130 55 190 142 64 241 79 180 111 211 4 8] [1 0 3 0 0 125 208] [133 160 74 48 27 48 39 213 156 71 54 190 104 32 173 195 78 15 247 95 154 20 4] [2 0 3 0 0 124 192] [133 160 74 48 27 48 39 213 71 93 96 97 131 246 103 242 117 117 242 122 102 57 252] [1 0 4 0 0 153 128] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 138 120 76 94 198 235 231] [2 0 4 0 0 126 48] [133 160 74 48 27 48 39 213 161 190 218 133 34 189 202 6 4 210 38 115 68 138 93] [1 0 5 0 0 145 56] [133 160 74 48 27 48 39 213 181 160 40 167 228 7 106 250 239 177 147 143 223 218 189] [2 0 5 0 0 124 208] [133 160 74 48 27 48 39 213 133 89 166 5 196 161 242 20 46 198 139 26 18 52 142] [1 0 6 0 0 180 152] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 60 43 67 53 87 214 163] [2 0 6 0 0 130 120] [133 160 74 48 27 48 39 213 158 207 13 38 213 94 203 78 214 51 158 92 104 197 112] [1 0 7 0 0 34 32] [133 160 74 48 27 48 39 213 178 213 133 20 42 35 24 186 160 140 25 92 252 210 242] [2 0 7 0 0 140 112] [133 160 74 48 27 48 39 213 46 67 112 101 89 72 41 205 82 117 228 27 204 109 200] [1 0 8 0 0 82 24] [133 160 74 48 27 48 39 213 142 246 116 70 48 41 45 65 120 25 184 155 91 123 174] [2 0 8 0 0 132 128] [133 160 74 48 27 48 39 213 253 10 27 170 202 136 197 234 239 187 196 5 166 215 182] [1 0 9 0 0 132 32] [133 160 74 48 27 48 39 213 63 45 71 158 103 71 129 113 196 148 131 177 29 128 162] [2 0 9 0 0 131 200] [133 160 74 48 27 48 39 213 52 170 14 71 17 205 63 28 76 253 41 134 241 227 200] [1 0 10 0 0 70 0] [133 160 74 48 27 48 39 213 9 8 197 104 10 153 18 70 24 239 30 230 123 22 102] [2 0 10 0 0 151 72] [133 160 74 48 27 48 39 213 208 112 28 134 21 105 39 113 51 90 157 36 215 94 173] [1 0 11 0 0 69 192] [133 160 74 48 27 48 39 213 207 49 23 198 37 84 251 4 153 255 28 239 177 47 13] [2 0 11 0 0 147 232] [133 160 74 48 27 48 39 213 64 115 149 233 120 244 31 210 191 42 252 38 126 32 27] [1 0 12 0 1 2 80] [133 160 74 48 27 48 39 213 186 139 142 5 35 179 166 152 135 234 102 89 109 33 6] [178 161 62 95 113 100 101] [113 58 47 114 56 216 181 49 171 213 150 80 50 222 228 94 34 110 55 170 212 163 177] [2 213 106 162 142 22 91] [8 155 150 15 27 47 191 79 102 112 16 90 168 160 96 244 195 13 72 185 16 118 29] 当tgp协议头的第五个字节不为0时,开始发生了错乱,截取后续包的内容是113 58 47 114 56 216 181 49,并非133 160 74 48 27 48 39 213(即85 a0 4a 30 1b 30 27 d5的十进制),说明tgp最后2字节表示后续包长度不对,应该继续增加,从目前的错误来看,起码是3个字节,即第5、6、7个字节。但如果是3个字节,而第4个字节也一直是0,显然也不合适。我决定将用后面4个字节表示剩余包的长度来检验一次。如下图: 结果都正确的打印了tgp协议头,以及后续8字节的85 a0 4a 30 1b 30 27 d5(对应十进制为:133 160 74 48 27 48 39 213) 至此,tgp协议头的后4个字节已经确认含义了,那么剩余前3个字节中,第一个字节有两种数值01、02,这两个数值的含义也比较好弄明白,这时,我的灵(jing)感(yan),他们分别对应getGameDataChunk、getKeyFrame的两种类型,可以从tgp录像请求的http中加以确认,如下图: 从本地抓取127.0.0.1的通讯包中,找http请求为getGameDataChunk、getKeyFrame的相应包,对照响应body中,字节流跟ob文件分析结果中,对应的是否一致。请求url为getGameDataChunk的,到tgp协议头第一个字节01的字节流中找。请求为getKeyFrame的,到第一个字节02中查找。这里也可以确认无误了。 至此,tgp协议头中,只剩下第2、3个字节不名含义。在汇总录像播放时,请求的所有url,你会发现getGameDataChunk、getKeyFrame请求中,对应的id参数是递增的,如下图: 再根据最初打印的所有tgp协议头7字节内容中,当第1字节一样时,发现第2字节不变,而第3个字节持续递增,这时有灵(jing)感(yan)了吗?第3个字节肯定是url请求中第n个包的含义。第二字节也可能跟第三字节构成uint16类型的总包数的含义。若表示一场游戏中,录像回放系统中,全部数据包分包数只用1字节表示的话,那一共可存储1<<8 -1 也就是255个,数量可能不够,所以,我假设第二、第三字节一起表示录像回放总包的含义。至此,tgp的分包方式中,包头含义已经全部表示明白了。那么,剩下的,就时写个结构体或者map来存储这些字节流即可。 到这里,ob文件分析完成了,对于lol ob server请求的几个url中,只剩下getLastChunkInfo请求没有处理了,你要知道,ob文件最初的几个json字符串中,还剩下chunk_tab、keyframe_tab 聪明的你,这时应该有灵(jing)感(yan)了吧。你来实现实时?什么?还是没灵(jing)感(yan)?再见...... 程序写好后,可以体验一下mac上lol客户端,看国服ob咯,不联网也可以看哦,只要从tgp助手那下载了ob录像文件。 国服客户端,提供观战功能并不能用,每次都是无法观战,或者观战数据加载错误之类的,不提供实时观战功能。ob录像请求的url返回404。 英雄联盟,游戏分为4个主进程 更新进程:LoLpatcher 启动进程:LoLLauncher 客户端(游戏大厅):LolClient 游戏进程:LeagueofLegends 这里已实现mac osx上观看lol ob录像,故可得知mac lol 游戏主进程 可以解析 国服lol 产生的lol协议文件,可大约证明游戏进程是兼容的,甚至一致的。。 更新进程上关系不大,只要保证版本一致即可。启动进程跟客户端两个进程实现了登陆认证功能,要做的,是实现这国服qq登陆认证绑定,也就是说mac osx上玩国服lol 理论上可行....要不,你来试试? 代码开源: 2015-10-27 +3 7 0