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英雄联盟的目录结构

国服LOL目录结构

国服LOL目录结构

  • 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上的进程启动信息:

英雄联盟在windows上登录时的CLI参数

英雄联盟在windows上登录时的CLI参数


果然是CLI中参数带有signature key,参数如下:

Air\LolClient.exe -runtime .\ -nodebug META-INF\AIR\application.xml .\ -- 8393 gameSignatureLength=120 
szGameSignature=000156AF4DBF0070990736102EF3C9F53FFD8DF469360BB7CD6B08C6380AE617E088291ED9FEA0395EEF92E1CCF80D6421305D3900A76AC4B6DD4E039DB9D2B84EAC0AB881FFFC045139F733D32CCED53C44DCF698E9CD636EE732F5E9006BA0C16C2B2144496E907077F5D6006824FAA52701BB0B68EFF8 
cltkeyLength=16 cltkey=h4.ac}w)gsq53_6Y uId=26693841 --host=hn1-new-feapp.lol.qq.com --xmpp_server_url=hn1-new-ejabberd.lol.qq.com 
--lq_uri=https://hn1-new-login.lol.qq.com:8443 --getClientIpURL=http://183.60.165.214/get_ip.php

可以看出,除了Air运行时参数外,还有8393数字,认证的KEY,当前大区服务器域名,xmpp聊天服务器地址,登录服务器地址之类。另外,在看下LolClient.exe的进程树:

lol在windows上的进程树

lol在windows上的进程树


如上图,游戏大厅进程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,先查看进程监听列表

C:\Users\chenchi>tasklist|find "lol.launcher_tencent.exe"
lol.launcher_tencent.exe      5416 Console                    1     10,564 K

C:\Users\chenchi>netstat -anto |find "5416"
  TCP    127.0.0.1:8393         0.0.0.0:0              LISTENING       5416     InHost
  TCP    127.0.0.1:8393         127.0.0.1:2552         ESTABLISHED     5416     InHost
  TCP    127.0.0.1:8394         0.0.0.0:0              LISTENING       5416     InHost
  TCP    127.0.0.1:8395         0.0.0.0:0              LISTENING       5416     InHost

从结果中,可以看到进程lol.launcher_tencent.exe监听了本地的8393、8394、8395端口,而且,LolClient.exe已经连接了8393端口,也就是其启动时命令行参数中的对应数值。那么另外两个端口8394、8395也应该是其他进程来连接的咯。抓本地数据包,看看他们之间有没有通讯,如看看他们通讯了什么内容:(在windows上,wireshark抓不到回环地址通讯包,可以用rawcap来抓包)

E:\crt_download\ProcessMonitor>RawCap.exe 127.0.0.1 localhost-2016-02-01.pcapng
Sniffing IP : 127.0.0.1
File        : localhost-2016-02-01.pcapng
Packets     : 3131

之后,wireshark打开这个文件,可以看到如下TCP数据包:

lol-launcher-tentcent.exe与LolClient.exe的通讯TCP数据包

lol-launcher-tentcent.exe与LolClient.exe的通讯TCP数据包

    00000000  10 00 00 00 01 00 00 00  04 00 00 00 00 00 00 00 ........ ........
00000000  10 00 00 00 01 00 00 00  04 00 00 00 00 00 00 00 ........ ........
    00000010  10 00 00 00 01 00 00 00  05 00 00 00 00 00 00 00 ........ ........
00000010  10 00 00 00 01 00 00 00  00 00 00 00 34 00 00 00 ........ ....4...
00000020  31 34 2e 31 37 2e 32 33  2e 39 34 20 35 31 35 36 14.17.23 .94 5156
00000030  20 51 62 77 41 34 73 6f  42 38 4a 71 37 43 4f 45  QbwA4so B8Jq7COE
00000040  51 2b 31 78 63 34 67 3d  3d 20 34 30 30 36 33 31 Q+1xc4g= = 400631
00000050  39 32 33 32                                      9232

从协议包结果上来看,对比发送、接收的4个包作为例子,不难看出,他们的数据包格式拆分为前8bytes10 00 00 00 01 00 00 00为固定值(暂且认为是固定值);后面4bytes04 00 00 0005 00 00 00为一段;再后面的4bytes00 00 00 0034 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

lol-launcher-tencent.exe导入了RiotLauncher.dll

lol-launcher-tencent.exe导入了RiotLauncher.dll


再静态分析RiotLauncher.dll,在Exports里看到Maestro_MessageTypeToString函数
RiotLauncher.dll中Maestro_MessageTypeToString

RiotLauncher.dll中Maestro_MessageTypeToString

跟进,看到MAESTROMESSAGETYPE_GAMECLIENT_CREATE等字样
RiotLauncher.dll中MAESTROMESSAGETYPE_GAMECLIENT_CREATE

RiotLauncher.dll中MAESTROMESSAGETYPE_GAMECLIENT_CREATE

,F5导出
riotlauncher-dll-MM-type-to-string-2

riotlauncher-dll-MM-type-to-string-2

signed int __cdecl Maestro_MessageTypeToString(int a1, char *a2, rsize_t a3)
{
  char *v4; // [sp-8h] [bp-8h]@2

  switch ( a1 )
  {
    case 0:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CREATE";
      break;
    case 1:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_STOPPED";
      break;
    case 2:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CRASHED";
      break;
    case 7:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_ABANDONED";
      break;
    case 8:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_LAUNCHED";
      break;
    case 9:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_VERSION_MISMATCH";
      break;
    case 10:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CONNECTED_TO_SERVER";
      break;
    case 3:
      v4 = "MAESTROMESSAGETYPE_CLOSE";
      break;
    case 4:
      v4 = "MAESTROMESSAGETYPE_HEARTBEAT";
      break;
    case 5:
      v4 = "MAESTROMESSAGETYPE_REPLY";
      break;
    case 6:
      v4 = "MAESTROMESSAGETYPE_LAUNCHERCLIENT";
      break;
    case 11:
      v4 = "MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME";
      break;
    case 12:
      v4 = "MAESTROMESSAGETYPE_CHATMESSAGE_FROM_GAME";
      break;
    case 20:
      v4 = "MAESTROMESSAGETYPE_PLAY_REPLAY";
      break;
    case 13:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CREATE_VERSION";
      break;
    case 14:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_INSTALL_VERSION";
      break;
    case 15:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CANCEL_INSTALL";
      break;
    case 16:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_INSTALL_PROGRESS";
      break;
    case 17:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_INSTALL_PREVIEW";
      break;
    case 18:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CANCEL_PREVIEW";
      break;
    case 19:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_PREVIEW_PROGRESS";
      break;
    case 21:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_UNINSTALL_VERSION";
      break;
    case 22:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CANCEL_UNINSTALL";
      break;
    case 23:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_UNINSTALL_PROGRESS";
      break;
    case 24:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_UNINSTALL_PREVIEW";
      break;
    case 25:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CANCEL_UNINSTALL_PREVIEW";
      break;
    case 26:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_PREVIEW_UNINSTALL_PROGRESS";
      break;
    case 27:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_ENUMERATE_INSTALLED_VERSIONS";
      break;
    case 28:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_CREATE_CLIENT_AND_PRELOAD";
      break;
    case 29:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_UPDATE_PRELOADED_GAME_WITH_CREDENTIALS";
      break;
    case 30:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_INITIAL_PRELOAD_COMPLETE";
      break;
    case 31:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_UPDATE_PLAYER_CONNECTION";
      break;
    case 32:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_PLAY_PRELOADED_GAME";
      break;
    case 33:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_GAME_LOADING_COMPLETE";
      break;
    case 34:
      v4 = "MAESTROMESSAGETYPE_KILL_GAMECLIENT_PROCESS";
      break;
    case 35:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_ENUMERATE_UNINSTALLABLE_VERSIONS";
      break;
    case 36:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_ENUMERATE_LATEST_VERSIONS";
      break;
    case 37:
      v4 = "MAESTROMESSAGETYPE_GAMECLIENT_INSTALLED_GAME_VERSION_SIZE";
      break;
    default:
      v4 = "MAESTROMESSAGETYPE_INVALID";
      break;
  }
  sub_10027C07(a2, a3, v4);
  return 1;
}

至此,可以看到lol launcher的协议指令类型都在这里。再看看哪里调用了Maestro_MessageTypeToString函数:

signed int __cdecl MaestroMessageAgent_SendMessage(int a1, int a2, const char *a3)
{
  unsigned int v4; // [sp+24h] [bp-28Ch]@2
  char v5; // [sp+28h] [bp-288h]@12
  char v6; // [sp+168h] [bp-148h]@12

  if ( a3 )
    v4 = strlen(a3);
  else
    v4 = 0;
  if ( !*(_DWORD *)(a1 + 16) )
  {
    Maestro_Log(2, "Cannot send messages when the MessageAgent isn't started.\n");
    return 0;
  }
  sub_1001FE7A(*(_DWORD *)(a1 + 12));
  if ( !sub_10020C02(16) || a3 && !sub_10020C02(v4) )
  {
    sub_10027FBA();
    return 0;
  }
  sub_10027FBA();
  if ( *(_DWORD *)(a1 + 36) == 1 || a2 != 4 && a2 != 5 )
  {
    Maestro_MessageTypeToString(a2, &v5, 0x140u);
    sub_10027D05(&v6, 320, "SendMessage [%s]\n", &v5);
    Maestro_Log(0, &v6);
  }
  return 1;
}

再看看MaestroMessageAgent_SendMessage的调用处:

signed int __cdecl MaestroGameController_SendGameConnectedToServerMessage()
{
  signed int result; // eax@2

  if ( dword_101576E0 )
  {
    result = MaestroMessageAgent_SendMessage(**(_DWORD **)dword_101576E0, 10, NULL);
  }
  else
  {
    Maestro_Log(2, "MaestroGameControllerStruct is not yet initialized.\n");
    result = 0;
  }
  return result;
}

MaestroGameController_SendGameConnectedToServerMessage函数名的字面含义上理解,此函数为『当游戏连接到服务器时,发送消息』,继续跟进…可是,我静态分析水平比较弱,越是继续跟进,我的思路越混乱,决定另辟蹊径。

在文中最初的LOL目录结构中,有个Logs目录,这个目录下Maestro Logs目录中的maestro-server.log、maestro-game_client.log中有一些日志,是开发人员用来排查调试程序执行结果的信息,大约如下:

INFO: maestro [server] initializing...
INFO: New thread go!
INFO: Starting Process Thread Spinning
INFO: Using registered HWND: 00000000 to launch the air client.
INFO: StartProcess [MAESTROPROCESSTYPE_AIR] successful
INFO: Launcher init of maestro completed.
INFO: ReceiveMessage [MAESTROMESSAGETYPE_GAMECLIENT_CREATE]
INFO: New thread go!
INFO: Starting Process Thread Spinning
INFO: StartProcess [MAESTROPROCESSTYPE_GAME] successful
INFO: ReceiveMessage [MAESTROMESSAGETYPE_GAMECLIENT_LAUNCHED]
INFO: SendMessage [MAESTROMESSAGETYPE_GAMECLIENT_LAUNCHED]
INFO: ReceiveMessage [MAESTROMESSAGETYPE_GAMECLIENT_CONNECTED_TO_SERVER]
INFO: SendMessage [MAESTROMESSAGETYPE_GAMECLIENT_CONNECTED_TO_SERVER]
INFO: ReceiveMessage [MAESTROMESSAGETYPE_CHATMESSAGE_FROM_GAME]
INFO: SendMessage [MAESTROMESSAGETYPE_CHATMESSAGE_FROM_GAME]
INFO: ReceiveMessage [MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME]
INFO: SendMessage [MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME]
INFO: ReceiveMessage [MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME]
INFO: SendMessage [MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME]
INFO: ReceiveMessage [MAESTROMESSAGETYPE_GAMECLIENT_ABANDONED]
INFO: SendMessage [MAESTROMESSAGETYPE_GAMECLIENT_ABANDONED]
INFO: [MaestroMessageAgent_ReadFromSocketInternal] 操作成功完成。
WARNING: Received 0 bytes on Maestro socket when data was expected.
INFO: ReceiveMessage [MAESTROMESSAGETYPE_CLOSE]
INFO: SendMessage [MAESTROMESSAGETYPE_CLOSE]
INFO: maestro exiting...

这里确实可以对应着launcher协议中,某些command被接收后,会被记录到这里。如何确认每个日志记录指令对应是哪个TCP包发送后的结果呢?如何利用这些日志信息呢?如果windows上有类似linux的strace查看系统调用的工具就好了,你还别说,还真有一款功能类似的,就是ProcessMonitor,有了这工具,我就可以看到lol.launcher.tencent.exe进程在何时发送TCP数据包,发送后写了哪个文件数据,写了什么内容,先写日志还是先发TCP数据包。。。

process-monitor-tcp-file-order

process-monitor-tcp-file-order


如上图,可以看到先发TCP包,还是先写日志文件,哪个进程发,哪个进程写。但还有一点要确认的是,发的TCP包的内容如何跟写的日志内容配对问题,显然,ProcessMonitor工具里有些更有用的信息,你可能没注意到进程每次发包后,会记录发的包大小;进程每次写入文件数据时,会记录写时的offset以及写入文件长度,根据launcher进程写日志时,获取的offset位置,可以找到对应maestro-server.log文件的offset,再根据写入文件时的length数值,可以确定写入内容是什么。那么,机智的你不难看出,写入日志内容的字面含义,很容易确认了TCP数据包中对应的command代表的含义,也明白了command代表的业务逻辑。
process-monitor-offset
process-monitor-offset-1

好了,lol.launcher_tencent.exe的功能大约理清楚了,再来回顾下League of Legends的客户端架构,以及猜测下服务端架构,根据我的理解,我画了一副图

League of Legend英雄联盟架构图猜测

League of Legend英雄联盟架构图猜测


架构图中的虚线部分,为服务器之间节点通讯,是我的猜测,并不是想客户端部分那样,按照分析客户端通讯得来的架构图。可能不准确。

  • 匹配请求处理服务器,用来处理客户端的登录认证、符文、天赋修改、英雄列表,进入战斗的匹配请求等。
  • 匹配服务器,应该是整个大区用同一个,这里会预算所有与大区玩家相关的数据,战斗力匹配,寻找想匹配对手,划分到对应服务器中。这是核心点,也是瓶颈点。
  • 房间管理服务器,应该要独立拆分出来,与匹配服务器解耦,与战场服务器解耦。当匹配服务器完成后,只要将匹配结果的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,数据包长度,数据包内容

  type Header struct {
	pHead0   uint32 //默认0x10
	pHead1   uint32 //默认0x01
	pCommand uint32 //默认0x00
	pLength  uint32 //默认0x00
  }
  func (head *Header) Read(buf []byte) {
	head.pHead0 = binary.LittleEndian.Uint32(buf[:4])
	head.pHead1 = binary.LittleEndian.Uint32(buf[4:8])
	head.pCommand = binary.LittleEndian.Uint32(buf[8:12])
	head.pLength = binary.LittleEndian.Uint32(buf[12:16])
  }

  // Packet
  type LolLauncherPacket struct {
	pHead Header
	pData []byte
  }

分析command,心跳包直接回复。与game无关消息包,直接回复MAESTROMESSAGETYPE_REPLY类型已收到该消息。

      func (this *LolLauncherClientCallback) OnMessage(c *gotcp.Conn, p gotcp.Packet) bool {
	packet := p.(*LolLauncherPacket)
	data := packet.GetData()
	commandType := packet.GetCommand()
	switch commandType {
	case MAESTROMESSAGETYPE_GAMECLIENT_CREATE:
		// 0x00 存储data数据,此数据为League of legends 启动参数
		c.AsyncWritePacket(NewLolLauncherPacket(MAESTROMESSAGETYPE_REPLY, []byte{}), 0)
		log.Println("LOGIN KEY:", string(data))
		////启动游戏进程
		this.PacketSendChanToMain <- packet
	case MAESTROMESSAGETYPE_CLOSE:
		//0x03 游戏进程退出
		this.PacketSendChanToMain <- packet
	case MAESTROMESSAGETYPE_HEARTBEAT:
		//0x04 回复收到心跳
		c.AsyncWritePacket(NewLolLauncherPacket(MAESTROMESSAGETYPE_REPLY, []byte{}), 0)
	case MAESTROMESSAGETYPE_REPLY:
		//0x05 不处理(一般不会有这种消息)
	case MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME:
		//0x0b 来自游戏大厅的消息,需要转发至游戏进程
		this.PacketSendChanToMain <- packet
	default:
		//MAESTROMESSAGETYPE_INVALID
		log.Println("Client->OnMessage->MAESTROMESSAGETYPE_INVALID:", commandType, " -- ", packet.pHead, " -- ", data)
	}

部分消息需要转发给game进程,走chan转发,比如进入游戏后,好友还是可以在游戏大厅发消息到正在游戏中的玩家的,聊天消息转发MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME 就是从client发到launcher,再有launcher转发至game的。
处理client消息转发到game的线程、处理game消息转发到client的线程

	//监听客户端通道消息
	go func() {
		for {
			packet := <-clientPacketChan.packetSendChanToMain
			data := packet.GetData()
			commandType := packet.GetCommand()
			switch commandType {
			case proto.MAESTROMESSAGETYPE_GAMECLIENT_CREATE:
				//获取参数,启动游戏进程
				go lolCommands.LolGameCommand(strconv.Itoa(int(lolgame_port)), string(data))
				//消息发送至game客户端
			case proto.MAESTROMESSAGETYPE_CLOSE:
				// 0X03 游戏关闭
			case proto.MAESTROMESSAGETYPE_HEARTBEAT:
				//0x04 回复收到心跳
			case proto.MAESTROMESSAGETYPE_REPLY:
				//0x05 确认收到消息包的回复(可以不做处理)
			case proto.MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME:
				//0x0b 来自游戏大厅的消息,需要转发至游戏进程(在ClientCallback中实现)
				gamePacketChan.packetReceiveChanFromMain <- packet
			default:
				//MAESTROMESSAGETYPE_INVALID
				log.Println("Client(main)->OnMessageFromMain->MAESTROMESSAGETYPE_INVALID:", commandType, " -- ", packet.GetHeader(), " -- ", data)
			}
		}
	}()

当然,还得再启动两个线程来接收launcher转发来的消息,

func (this *LolLauncherClientCallback) OnMessageFromMain(c *gotcp.Conn) {
	for {
		packet := <-this.PacketReceiveChanFromMain
		data := packet.GetData()
		commandType := packet.GetCommand()
		switch commandType {
	    case MAESTROMESSAGETYPE_GAMECLIENT_ABANDONED:
	    	c.AsyncWritePacket(packet, 0)
		case MAESTROMESSAGETYPE_GAMECLIENT_LAUNCHED:
			//0X08
			c.AsyncWritePacket(packet, 0)
		case MAESTROMESSAGETYPE_GAMECLIENT_CONNECTED_TO_SERVER:
			//0XOA 连接到服务器
			c.AsyncWritePacket(packet, 0)
		case MAESTROMESSAGETYPE_CHATMESSAGE_TO_GAME:
			//0x0b 来自游戏大厅的消息,需要转发至游戏进程(在ClientCallback中实现)
			c.AsyncWritePacket(packet, 0)
		case MAESTROMESSAGETYPE_CHATMESSAGE_FROM_GAME:
			c.AsyncWritePacket(packet, 0)
		default:
			//MAESTROMESSAGETYPE_INVALID
			log.Println("Client->OnMessageFromMain->IGNOREMESSAGE:", commandType, " -- ", packet.pHead, " -- ", data)
		}
	}
}

等等等等,一系列代码写完后,本以为终于可以在osx上玩LOL国服了,没想到,我还是太年轻,想的太简单了。程序启动LolClient.exe之后就假死了,一番排查后,发现是TCP粘包问题

LolClient.exe的TCP 粘包

LolClient.exe的TCP 粘包

LolClient.exe的TCP 粘包

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时,看到了一个特殊的函数名

LOL进程启动的环境变量

LOL进程启动的环境变量

再回到ProcessMonitor中看看League of legends.exe启动时的环境变量是什么
League of legends.exe的环境变量

League of legends.exe的环境变量


显然是__COMPAT_LAYER=ElevateCreateProcess(不要问为什么显然…我也曾经懊恼过,上学时,老师总是说显然,可我就是不明白为什么显然)

在OSX上,也找下环境变量是什么获取LeagueOfLegends.app进程执行时的环境变量,去除系统默认的几个,剩下的,就是游戏自己添加的

cfc4n@cnxct:~$ ps -ef|grep League
  501  3955  3848   0  4:37下午 ??         0:00.55 /Applications/League of Legends.app/Contents/LoL/RADS/solutions/lol_game_client_sln/releases/0.0.0.193/deploy/LeagueOfLegends.app/Contents/MacOS/LeagueofLegends 8394 LoLPatcher /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_air_client/releases/0.0.0.210/deploy/bin/LolClient 182.162.122.113 5102 yvqljG4isLEGWoQS4WEEUA== 39920272
cfc4n@cnxct:~$ ps -wwE -p 3955
  PID TTY           TIME CMD
 3955 ??         0:24.44 /Applications/League of Legends.app/Contents/LoL/RADS/solutions/lol_game_client_sln/releases/0.0.0.193/deploy/LeagueOfLegends.app/Contents/MacOS/LeagueofLegends 8394 LoLPatcher /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_air_client/releases/0.0.0.210/deploy/bin/LolClient 182.162.122.113 5102 yvqljG4isLEGWoQS4WEEUA== 39920272 LOGNAME=cfc4n SHELL=/bin/bash USER=cfc4n riot_launched=true SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.8MjNgu5Xfd/Listeners XPC_SERVICE_NAME=com.riotgames.MacContainer.67552 HOME=/Users/cfc4n __CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34 TMPDIR=/var/folders/dn/qvv6vmxd6c5g8bf6t0dtwwcw0000gn/T/ PATH=/usr/bin:/bin:/usr/sbin:/sbin Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.ZBoYrDDTEF/Render XPC_FLAGS=0x0
//对其格式,如下
LOGNAME=cfc4n
SHELL=/bin/bash
USER=cfc4n riot
launched=true
SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.8MjNgu5Xfd/Listeners
XPC_SERVICE_NAME=com.riotgames.MacContainer.67552
HOME=/Users/cfc4n
__CF_USER_TEXT_ENCODING=0x1F5:0x19:0x34
TMPDIR=/var/folders/dn/qvv6vmxd6c5g8bf6t0dtwwcw0000gn/T/
PATH=/usr/bin:/bin:/usr/sbin:/sbin
Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.ZBoYrDDTEF/Render
XPC_FLAGS=0x0
func (this *launcher_commands) LolGameSetenv() {
	var LolEnvKeys = [12]string{"LOGNAME", "SHELL", "USER", "SSH_AUTH_SOCK", "XPC_SERVICE_NAME", "HOME", "__CF_USER_TEXT_ENCODING", "TMPDIR", "PATH", "Apple_PubSub_Socket_Render", "XPC_FLAGS", "riot_launched"}
	var LolEnvs map[string]string
	LolEnvs = make(map[string]string)
	for _, v := range LolEnvKeys {
		switch v {
		case "riot_launched":
			LolEnvs[v] = "true"
		case "XPC_SERVICE_NAME":
			LolEnvs[v] = "com.riotgames.MacContainer.67552"
		default:
			LolEnvs[v] = os.Getenv(v)
		}
	}
	os.Clearenv()
	for k1, v1 := range LolEnvs {
		os.Setenv(k1, v1)
	}
}

一系列代码写完后,本以为终于可以在osx上玩LOL国服了,没想到,我还是太年轻,想的太简单了。除了程序执行时的环境变量外,还有一个程序执行时的当前目录:

cfc4n@cnxct:~$ ps -ef|grep League
  501  3828     1   0  4:34下午 ??         0:04.64 /Applications/League of Legends.app/Contents/LoL/RADS/system/UserKernel.app/Contents/MacOS/UserKernel updateandrun lol_launcher LoLLauncher.app
  501  3847  3828   0  4:34下午 ??         0:00.32 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_launcher/releases/0.0.0.170/deploy/LoLLauncher.app/Contents/MacOS/LoLLauncher
  501  3848  3847   0  4:34下午 ??         0:00.24 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_patcher/releases/0.0.0.42/deploy/LoLPatcher.app/Contents/MacOS/LoLPatcher
  501  3871  3848   0  4:34下午 ??         0:02.26 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_air_client/releases/0.0.0.210/deploy/bin/LolClient -runtime .\ -nodebug META-INF\AIR\application.xml .\ -- 8393
  501  3955  3848   0  4:37下午 ??         0:00.55 /Applications/League of Legends.app/Contents/LoL/RADS/solutions/lol_game_client_sln/releases/0.0.0.193/deploy/LeagueOfLegends.app/Contents/MacOS/LeagueofLegends 8394 LoLPatcher /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_air_client/releases/0.0.0.210/deploy/bin/LolClient 182.162.122.113 5102 yvqljG4isLEGWoQS4WEEUA== 39920272
  501  3880   824   0  4:34下午 ttys000    0:00.00 grep League
cfc4n@cnxct:~$ lsof -a -d cwd -p 3828
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
UserKerne 3828 cfc4n  cwd    DIR    1,4      136 15223994 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_launcher/releases/0.0.0.170/deploy
cfc4n@cnxct:~$ lsof -a -d cwd -p 3847
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
LoLLaunch 3847 cfc4n  cwd    DIR    1,4      136 15223994 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_launcher/releases/0.0.0.170/deploy
cfc4n@cnxct:~$ lsof -a -d cwd -p 3848
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
LoLPatche 3848 cfc4n  cwd    DIR    1,4      170 15224042 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_patcher/releases/0.0.0.42/deploy
cfc4n@cnxct:~$ lsof -a -d cwd -p 3871
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
LolClient 3871 cfc4n  cwd    DIR    1,4      680 15258110 /Applications/League of Legends.app/Contents/LoL/RADS/projects/lol_air_client/releases/0.0.0.210/deploy/bin
cfc4n@cnxct:~$ lsof -a -d cwd -p 3955
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
LeagueofL 3955 cfc4n  cwd    DIR    1,4      170 15490389 /Applications/League of Legends.app/Contents/LoL/RADS/solutions/lol_game_client_sln/releases/0.0.0.193/deploy

记得要chdir一下,这么改进后,本以为终于可以在osx上玩LOL国服了,没想到,它确实在windows上登录国服,终于运行起来了

自己写的lol launcher在windows上成功运行了

自己写的lol launcher在windows上成功运行了


它也在OSX上登录美服,成功运行起来了
我自己写的lol launcher成功在OSX上运行了

我自己写的lol launcher成功在OSX上运行了美服、韩服


那么我只要把OSX上启动LolClient的命令行参数改为国服的参数,就可以登录国服,玩国服游戏咯,尝试一下
LOL OSX登录国服

LOL OSX登录国服

果然可以,我情不自禁的开始佩服我的聪慧,机智,下一步就开房间,匹配打国服了?然而,我还是太年轻,想的太简单了。程序启动League of Legend.app时出错了,进不去,日志中记录

000004.769|       0.0000kb|      0.0000kb added| ALWAYS| r3dRenderLayer::RecreateOwnedResources
000005.436|       0.0000kb|      0.0000kb added| ALWAYS| r3dRenderLayer::InitResources exit successfully
000005.463|       0.0000kb|      0.0000kb added| ALWAYS| Waiting for client ID
000005.704|       0.0000kb|      0.0000kb added| ALWAYS| {"messageType":"riot__game_client__connection_info","message_body":"Hard Connect"}
000005.704|       0.0000kb|      0.0000kb added| ALWAYS| Received client ID
000005.704|       0.0000kb|      0.0000kb added| ALWAYS| Set focus to app
000005.704|       0.0000kb|      0.0000kb added| ALWAYS| Input started
000005.718|       0.0000kb|      0.0000kb added| ALWAYS| Query Status Req started
000006.864|       0.0000kb|      0.0000kb added| ALWAYS| Query Status Req ended
000006.864|       0.0000kb|      0.0000kb added| ALWAYS| Waiting for server response...

之后,游戏进程就报错退出了。我太年轻了,想的太简单了……可是,我好像已经不年轻了……

这到底是为什么,玩个游戏怎么这么难,这么难?我只是想玩个游戏而已,为什么这么难?腾讯LOL项目组的朋友,能不能告诉我,你们还验证了什么?很明显不是版本号,是不是操作系统?是不是拒绝了OSX的登录? 曾经,我曾尝试过从协议上入手,看看LOL游戏协议能不能搞定,可难度比较大,花了好几个星期的空余时间,大约确认了是UDP Enet协议格式,后来,也在网上搜到相关信息,确认了。但网上公开的协议格式,是4.23以前的,现在都6.*的版本,早就改了….唉,以后再说吧…

最近两天,总结了之前研究LOL Launcher的实现过程,疏于工作,拖延了工作进度,我自己也是内心有愧。有愧的不仅仅是会议上的苍白答复,也还有这篇总结本计划与2015年底写完的,也还是拖延到现在。
总结完这边博文,接下来要写部门的年终总结了,离丙申年到来还有5天,犯有严重拖延症的我,能把我自己的年终总结,在乙未年结束之前写完吗?

参考资料:

代码开源:

18 thoughts on “osx平台上lol英雄联盟launcher启动器的分析实现

  1. 匹配游戏时应该是由一个调度服务器进行分配游戏IP
    能确定得出调度服务器的地址有哪些吗?或者弄出游戏服务器的地址池

  2. 好像LOL客户端会上传系统信息,比如操作系统什么的= =

    000002.018| 0.0000kb| 0.0000kb added| ALWAYS| —–HARDWARE INFORMATION START—–
    000002.018| 0.0000kb| 0.0000kb added| ALWAYS| Operating System: 10 Professional, x86

    • 这里只是日志中记录了操作系统信息,但不能确认操作系统信息提交到服务器了。也不能确定服务器验证了客户端信息,这些都是未知的,很难去分析。。。

发表评论

电子邮件地址不会被公开。 必填项已用*标注