查看原文
其他

程序丨技术探讨:LOL新版客户端LCU架构探析

2017-07-10 springwang Gad-腾讯游戏开发者平台

在做LOL竞技场项目(项目总结)的时候,发现WEB页面可以直接调用客户端里面的接口和数据,这使我很好奇,决心花点时间再研究下这个实现的大致原理,拓展一下思路和知识面,也为后续这种内嵌客户端的项目开发积累经验,然后在得到多位大牛的帮助下,最后才有了这篇浅显的分析,希望能抛砖引玉,如果你了解得更深入,也可以消息我,一起探讨。


初探:入口


首先我们来看下,在我做的页面里调用客户端接口的代码:


首先是请求接口



其次是等待结果返回



从上面的实现我们大致可以判断,这里是通过window的postMessag消息机制来进行数据通讯的,我们再继续往下分析下 RClientWindowMessenger 的定义,看能否证实一下,该对象是在一个JS文件中定义的对象,而sendMessage的定义如下:



如果我们把上面sendMessage调用和实现的代码组合到一起看,其实就是我们在自己的页面里面调用顶层的window对象来发送一条消息,具体如下:



而数据也是通过消息从客户端传送回来的,而RClientWindowMessenger.addMessageListener的实现就比较简单了,就是实现一个事件分发,把对应事件请求结果分发给事情处理器,如下:



到这里我们只是了解了我们的页面向客户端请求数据使用的是window.postMessage消息机制,但是我们并不了解客户端是怎么接收到这个消息,并处理再返回对应结果的,咱接着往下深入看看。


二探:架构


从上面我们大概能知道我们的页面是嵌入到一个父级页面里面的,因为使用了top,为了证实这个,我在LOL的客户端测试过,在我们的页面里,判断top==window=false,这说明top是存在的另外一个页面,然后我就猜想,top里面应该隐藏了很多的实现逻辑。为了继续深入,我找了负责对接LOL新版LCU客户端的同事,从他们那里了解了到了更多的信息,这更多信息就得了解下riot为啥会有LCU客户端,是为了解决什么问题?并且是怎么解决的呢?这个答案在LCU架构大神自己分享的文章里面我找到了答案,有三个原因:


在博客里大神提到LCU之前的客户端是08年使用AdobeAir实现的,但是随着时间发展,这个框架遇到了下面几个最突出的问题:


第一、H5和JS实现桌面Client端应该有很成熟的方案,并且这能带来额外的好处,比如标准化的流程,开发工具和开发者。


第二、玩家想要在退出游戏的时候保持登录态,接收好友请求或者游戏邀请,而air资源占用较高,有些人会把进程杀掉


第三、随着项目扩大,当很多team都想为客户端增加功能的时候,冲突问题越来越严重


为了解决上面的三个问题,大神设计了超级腻害的架构(当然中间也躺了很多坑,具体可以去文末参考资料看英文原文),就是利用H5+JS来渲染前端UI,然后C++来处理业务逻辑和后端通信,而前端UI接收事件,通过websocket跟后端的C++模块通信。 



通过上图我们可以看到,H5部分是运行在CEF(Chromium Embedded Framework)容器里的,这里的CEF你可以简单的理解为就是一个Webview,只不过它比Webview更加灵活,可深度定制,因为它是Google大大开源的,是chrome浏览器实现的内核版本,能实现HTML,JS,CSS的解析,而C++部分还是native实现。


看完上面我们就能知道,这不就是一个CS架构哇,前端UI利用H5技术,后端跑着一堆C++的Microservice,对的没错,大神自己也说这就是一个CS架构,那这个架构是如何解决上面遇到的三个问题的呢?


首先:基于CEF, 使用标准规范的H5+JS实现UI展示和变换逻辑,轻松解决问题1。


其次:游戏时,可以直接关闭CEF进程,只保留后端C++微服务,内存占用20M(最新版本实测30M),登录态保留在微服务中,并能实现tip提示,而CEF UI完全可以通过从微服务拉取数据重建。完美解决问题2。


最后:多人协作的问题,这里大神设计还是相当巧妙的,H5和C++层都设计成了插件机制,能无限扩展,而不会互相冲突,其次还可以按需加载,一劳永逸的解决了问题3。


三探:组件


对于CEF本身和C++的MicroService实现部分我们这里就不去详细深入介绍了(水太深),我感兴趣的还是前端部分,所以这里主要是探究下在CEF里面运行的前端组件部分的实现思路。


首先我们想象中的前端组件就是html,js和css的文件组合,但是在LCU客户端这里还不太一样,安装完LOL游戏客户端后,在安装目录 LeagueClient\Plugins 下面有一堆文件夹,分别是以rcp-be- 或者 rcp-fe- 开头的,如下图



be 代表的是C++的MicroService组件,而fe就是我们要研究的前端组件实现了,打开fe的一个目录,我们可以看到一般有2个文件

1.  description.json 模块描述信息

2.  assets.wad 包含完整组件文件(不过这里都是压缩过的)


接下来我们要看下wad里面都是什么东东,发现一般的解压缩软件无法解压,然后搜索在github上找到了解压wad文件的node工具包,解压完就看到我们熟悉的内容了html,js和json,图片等资源,我这里先打开rcp-fe-lol-home组件,这个是LCU打开加载的首页,如下图:



在解压完的根目录了,有个928200cf91a315ce.min.js文件,这个就是主窗口的js打包压缩文件,我们可以用编辑器打开格式化一下,还是可以大致分析出很多我们需要的信息,在这个js里,我们发现了主窗口容器常用的消息定义:


容器接收:

rcp-fe-lol-home-loaded:告知框架首页加载完毕

rcp-fe-lol-home-data-request:请求当前账号和环境信息

rcp-fe-lol-home-session-request:请求当前登录token

rcp-fe-lol-home-champ-game-data-request:请求指定英雄和皮肤的详细信息

rcp-fe-lol-home-open-store:请求打开商店

rcp-fe-lol-home-play-sound:请求播放声音


容器发送

rcp-fe-lol-home-hide:主窗口隐藏

rcp-fe-lol-home-show:主窗口显示

rcp-fe-lol-home-settings-changed:游戏配置更新

rcp-fe-lol-home-data-response:返回当前账号和环境信息

rcp-fe-lol-home-session-response:返回当前登录的token

rcp-fe-lol-home-champ-game-data-response:返回指定英雄和皮肤的相信信息


除了上面的消息定义,我还找到基础关键的代码实现


1、首先是iframe创建:



2、给iframe发送消息



3、消息通讯



上图我们可以看到上面常见的几个消息,这里接受到消息后,会有相应的处理,然后处理结束后,会把结果再通过消息返回,下面我们分析下几个消息的处理细节:


消息处理分析

rcp-fe-lol-home-data-request

我们可以看到此消息的hanlder内部是直接调用了f.getClientData方法,然后得到结果t,通过response消息返回,f不用管,下面我们看下getClientData方法的实现:



上面通过字段名称,我们可以了解到是直接返回了账户和系统相关的信息。同样的rcp-fe-lol-home-session-request也是类似的,接下来我们分析下一个比较有用的消息。


rcp-fe-lol-home-champ-game-data-request

获取英雄或者皮肤详细信息,这个消息的handler我们看到,内部貌似是调用了另外一个模块(lol-game-data)里的一个json文件,然后把这个文件的内容返回了,这个lol-game-data,我搜索了下,原来这是另外一个插件,不过按插件文件夹名称:rcp-be-lol-game-data,这里被定义为了后端插件,但是里面没有C++ DLL插件,而是2个wad文件,default-assets.wad和zh_CN-assets.wad,我们前面讲过,wad其实就是一堆静态资源(js css html image text等)的打包文件,应该大部分资源都打包到这里了,因为这2个文件大概有800M,我解压看了下,确实挺多内容的,这里不详述都包含哪些内容了。


我们继续看 p("/lol-game-data”) 应该是加载插件,然后通过get方法获取数据,get这里接受了一个 /assets/v1/champions/" + championId + ".json”,很明显是个json文件,这个文件里就是对应的英雄或者皮肤的配置数据。而我们获取的到数据格式如下:


我们可以看到,上面英雄的图片,声音资源都是通过 127.0.0.1:40854 来访问的,那这就说明LCU客户端自己开了一个本地的WebServer,为了防止冲突,这里每次都是随机设置了端口号,很明显这里的webserver不是我们通常意义上的apache或者nginx,因为这里是从wad压缩包里读取数据,而不是常规的目录里,所以实现肯定不一样。具体实现没有深究,


上面的分析总结一下:H5页面和客户端通信的核心原理就是message,而html资源都是通过本地开启webserver来访问。具体实现就是:LCU会创建CEF的进程,然后创建一个webview的主容器,最后会在主容器里创建一个IFrame,用来加载我们的页面,正如上面讲的,我们的页面定义了消息发送(top)和接收的方法,而主容器里也定义了同样的消息发送(IFrame)和接收的方法,实现了H5和Client本地数据的双向通讯。


四探:进程通讯


接下来我们进一步分析客户端的实现,打开LCU客户端,我们可以看到,如下几个进程:



LeagueClient:主进程,承载后端插件,前端插件,并且负责和服务器通信。

LeagueClientUx:CEF承载进程,负责前端主容器逻辑处理和与LeagueClient主进程通讯。

LeagueClientUxRender:CEF承载进程,应该只负责HTML UI界面渲染,强制kill掉它,会自动重新拉起来。


我们最前面讲大神架构的时候说到,在启动游戏客户端后,就可以关闭掉UI部分,在设置里我选择了启动游戏关闭客户端,在进程里,发现Ux和UxRender进程就被杀掉了:




然后我们观察网络通讯可以看到,进程之间通过websocket通讯,这个在大神的文字里也有说到,下面可以看到,这个通讯是双向的,根据端口我们可以看到,LeagueClient 和LeagueClientUx之间,互相有通讯,然后UX还和GameLoader之间也有通讯,还可以看到LeagueClient和Ux都开了多个websocket通道,而且多个通道之间会通讯,具体每个通道负责什么信息,这里不是太清楚。



实际上,通过 lol-home-data 消息,我们可以得到客户端启动的http webserver的地址和端口,后面所有的资源访问都是通过这个地址,通过端口50992,我们可以确定,LeagueClient进程负责启动这个webserver,实际上通过下图中assetUrls里面的url信息,我们可以推断出,这个webserver就是把整个plugins目录当做了跟目录,然后CEF承载前端fe插件的加载访问也是通过这个webserver使用https的协议,而不是特殊的本地文件处理,完全符合web的规范。这样前端插件就真的跟web开发一摸一样了。只是多了riot提供的更丰富的API接口而已。



然后我试着直接在chrome里访问对应的资源,发现需要权限验证,在客户端里访问应该是种了带权限的cookie或者token,所以能直接通过,而外面是无法访问到的。



总结


通过这个分析,对于LCU的客户端架构有了大致的了解(前端部分),也算是拓展了自己的思路,近3年,随着node的发展,其实对于前端开发来说,对于客户端的实现方案还是挺多选择的,这里使用了CEF实现的方式,这里瞎扯淡一下:是否可以改为node+webkit的nwjs架构,或者基于Atom编辑器实现的electron框架,这个可以纯前端开发实现,也可以支持插件化或者模块化开发,是不是别这里的CEF更简单呢?当然任何一个架构都是在历史架构基础之上演化而来,而离开历史框架谈架构意义不大。


今日推荐


5分钟教你打造一个秒开的Android App

2017上半年精选好文合集!收藏指数爆表!


添加小编微信,可享双重福利

1.加入GAD程序猿交流基地

获取行业干货资讯,观看大牛分享直播

2.领取60G独家程序资料库,地址在小编朋友圈

包括腾讯内部分享、文章教程、视频教程等全套资料

 

↓长按添加小编GAD苏苏↓

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存