游戲啟動過程
啟動入口
在使用pomelo進行游戲開發時,工程目錄下的app.js是整個游戲服務器的啟動運行入口。app.js中創建項目,進行默認配置并啟動服務器的代碼如下:
var pomelo = require('pomelo');
var app = pomelo.createApp();
app.set('name', 'nameofproject');
app.start();
從上面的代碼可以看出,用戶首先需要在項目中引入pomelo,然后創建application的實例app,接著完成一些基本的應用配置,最后應用就可以啟動了。當app.js運行起來后,pomelo會根據游戲的配置啟動不同的相關組件及服務器。
服務器與組件
服務器啟動流程的主要工作就是逐一啟動app.load注冊的組件。組件是連接pomelo框架和當前服務器所依賴的服務之間的橋梁。不同的服務器可以選擇加載不同的組件。Pomelo提供了一些系統默認的組件,主要包括:handler, filter, master, monitor, proxy, remote, server, sync, connection。開發者也可以根據需要,開發自己的組件,并加載到服務器進程中。
組件同時是具有生命周期的,其生命周期可以包括start, after start, stop等。在組件中可以實現這些方法,應用服務器會在不同的運行階段執行組件不同生命周期的方法。
啟動流程詳述
應用創建及啟動
所有服務器的啟動都是從運行app.js開始。每一個服務器的啟動都首先創建一個全局唯一的application對象,該對象中掛載了所在服務器的所有信息,包括服務器物理信息、服務器邏輯信息、以及pomelo的組件信息等。同時,該對象還提供應用管理和配置等基本方法。 在app.js中調用app.start()方法后,application對象首先會通過loadDefaultComponents方法加載默認的組件。
組件加載
在加載組件時,系統會根據application對象中服務器的信息,針對不同的服務器加載不同的組件,從而使得不同服務器進程對外提供不同個服務。對于master服務器,主要加載的組件是master組件。Master組件主要負責根據根據servers.json文件中的配置信息和啟動參數去啟動其他服務器。對于其它服務器默認加載proxy、channel、sync、backendSession和server組件,特定的服務器還需要加載特定的組件,例如前端服務器會加載統計客戶端連接數量的connection組件。具體組件的說明如下:
- master: master組件主要負責啟動master服務器。
- monitor: monitor組件主要負責啟動各個服務器的monitor服務,該服務負責收集服務器的信息并定期向master進行消息推送,保持master與各個服務器的心跳連接。
- proxy: proxy組件主要負責生成服務器rpc客戶端,由于系統中存在多個服務器進程,不同服務器進程之間相互通信需要通過rpc調用(master服務器除外)。
- remote: remote組件主要負責加載后端服務器的服務并生成服務器rpc服務端。
- server:server組件主要負責啟動所有服務器的用戶請求處理服務。
- connector: connector組件主要負責啟動前端服務器的session服務和接收用戶請求。
- sync: sync組件主要負責啟動數據同步模塊并對外提供數據同步功能。
- connection: connection組件主要負責啟動用戶連接信息的統計服務。
- channel: channel組件主要負責啟動channelService服務,該服務主要提供channel相關的功能包括創建channel,通過channel進行消息推送等。
- session: session組件主要負責啟動sessionService服務,該服務主要用來對前端服務器的用戶session進行統一管理。
- backendSession: backendSession組件主要負責啟動backendSession服務,該服務主要負責維護服務器本地session并與前端服務器進行交互。
- dictionary: dictionary組件主要負責生成handler的字典。
- protobuf: protobuf組件主要負責解析服務端和客戶端的proto buffer的定義,從而對客戶端和服務端的通信內容進行壓縮。
組件概述
我們知道,pomelo的應用程序執行的全部過程,就是對其相應組件的生命周期的管理,實際的所有邏輯功能均由pomelo組件提供。pomelo內建提供了十多個組件,這些組件適用于不同的服務器,提供不同的功能。有些組件提供的功能比較復雜,有些則比較簡單。下面我們將以提供的功能為主線來闡述pomelo提供的內建組件。
master組件
master組件僅僅由master服務器加載,它主要的功能包括啟動所有的應用服務器、管理和監控所有的應用服務器和接受管理客戶端的請求與響應。
master服務器總是率先啟動,master組件在其start調用的最后才會調用Starter.start,Starter.start才會啟動所有的應用服務器,因此Master組件總是最先start。在Master組件的start調用中,會完成以下幾步:
-
加載注冊Module到MasterConsoleService,Module的導出方式有兩種,可以導出工廠函數,也可以導出對象,如果導出工廠函數的話,其簽名應該是 FacFunc(opts, ConsoleServicee),其中opts是用戶調用app.registerAdmin的時候傳入的,ConsoleService則是具體的加載注冊Module的MasterConsoleService。
-
在加載注冊完所有的Module后,會開啟MasterAgent對端口的監聽,此時,master就已經可以接收來自monitor和client的request和notify了。
-
開啟監聽后,MasterConsoleService會enable所有的module,這步操作主要是看看有沒有module配置了周期性地拉去monitor信息,也就是module的配置中有type選項和interval選項,且type的值為'pull',interval指定了周期,則認為其配置了周期性監控操作,此時會完成周期性事件的調度,使得master可以周期性地獲取監控信息。
-
最后如果有Module定義了start回調,將會在這里調用,一般在start回調里會做一些初始化信息。 經歷了這些步驟后,master完成啟動。
在master組件的start方法里,會根據用戶提供的服務器配置信息,啟動用戶配置的所有的具體應用服務器。
當master組件start結束后,他將開啟一個socket監聽端口,接受應用服務器和監控客戶端的連接和注冊,收集應用服務器上報的監控信息,給應用服務器推送一些消息,并對管理客戶端發出的管理請求給予響應。管理客戶端如pomelo-cli可能發出的請求包括查看某個服務器進程狀態,增加一個服務器,停掉一個服務器等。以增加一個服務器為例,當管理客戶端發出增加服務器請求時,會提供相應的服務器參數,如服務器類型,主機ip,開啟的端口等。此時,master組件接受后,會啟動相應的服務器,并將新增加的服務器信息廣播通知給其他已經啟動的服務器。
master組件無配置項。
monitor組件
monitor組件由所有的包括master服務器在內的服務器都會加載,它的主要功能就是與master建立連接進行通信,從而對整個應用服務器群進行管理和監控。master服務器本身也會加載monitor服務器,因為master服務器也會收集其本身自己的監控信息。
由于應用服務器是在master組件啟動后期才創建,因此monitor總是后于master啟動。monitor的啟動過程與master類似,唯一不同的就是,monitor會發起到master的連接,而不是監聽接口。monitor中同樣也會使用與master完全相同的方式,加載注冊Module,如果有Module配置了周期性地推送監控數據到master的話,即其配置type的值為'push',這里也會調度對應的事件,使得能夠周期地推送數據。最后如果有Module定義了start的話,則會回調start。Monitor的啟動過程與master基本一致。
monitor會通過master接受一些命令,比如關閉整個服務器等。對于一些周期性監控的信息,pomelo提供了兩種收集方式,即pull方式和push方式。pull方式要求master周期地去與monitor通信,拉取相應的監控信息;push方式,則是由monitor周期地主動地向master報告其監控信息。
monitor組件無配置項。
filter
pomelo內建了常見的一些filter,用戶可以通過如下的方式啟用:
app.filter(pomelo.filters.<filterName>(<args>));
下面介紹一下這幾個fitler:
serial
這個filter是用來對用戶請求做串行化的,可以使得用戶的請求只有在第一個請求被處理完后,才會處理第二個請求。serial中使用了一個taskManager,當用戶請求到來時,在beforeFilter中,將用戶的請求放到taskManager中,taskManager中維護著一個task隊列。在對應的afterFilter中,如果taskManager還有未處理的請求,將會處理其請求,即在一個請求的afterFilter里啟動在taskManager中還沒處理的下一個請求,這樣就實現了請求的序列化。
timeout
這個filter是用來對服務端處理超時進行警告的,在beforeFilter中會啟動一個定時器,在afterFilter中清除。如果在其定時器時間內,afterFilter被調用,定時器將會被清除,因此不會出現超時警告。如果定時器超時時,afterFilter還沒有執行到,則會引發超時警告,并記錄日志。默認的處理超時是3秒,可以在加載timeout的時候作為參數傳入。
time
這個filter使用來記錄服務器處理時間的,在beforeFilter中會記錄一下當前的時間戳,在afterFilter中再次獲取當前的時間戳,然后兩個時間戳相減,得到整個處理時間,然后記錄日志。
toobusy
這個filter中,一旦檢測到node.js中事件循環的請求等待隊列過長,超過一個閥值時,就會觸發toobusy。一旦觸發了toobusy,那么toobusy的filter中將終止此請求處理鏈,并在next調用中,傳遞錯誤參數。
connector組件
connector組件是一個重量級的組件,它會依賴于session組件,server組件,pushScheduler組件和connection組件。connector組件僅僅被前端服務器加載,它主要用來管理客戶端的連接。connector組件會加載底層的connector,創建端口監聽,綁定事件響應。當有客戶端連接請求時,connector組件會請求session組件,獲得當前連接的session,如果session組件中沒有相應的session的話,session組件會為這個新連接創建新的session,并維護相應的連接;然后connector組件還會向connection組件上報連接信息,供統計使用;最后,將拿到的session以及客戶端的請求,一起拋給server組件,由server組件進行請求處理。當server組件處理完請求后,又會通過connector組件將響應返回給客戶端。在返回響應給客戶端的時候,connector組件做了一個緩存選擇,這個緩存實現依賴于pushScheduler組件,也就是說connector組件并不是直接將響應發給客戶端,而是將響應給pushScheduler組件。pushScheduler組件根據相應調度策略,可能不緩存直接通過session組件維護的連接,將響應發出去,也可能進行緩存,并按時flush。這是可以配置的。
connector組件支持如下配置項:
- connector: 底層使用的通信connector,不配置的話,會默認使用sioconnector;
- useProtobuf: 目前僅僅支持connector配置使用hybridconnector的情況,配置其為true,將開啟消息的protobuf功能;
- useDict: 目前僅僅支持connector配置使用hybridconnector的情況,配置其為true時,將會開啟基于字典的路由消息壓縮;
- useCrypto: 目前僅僅支持connector配置為hybridconnector的情況,配置其為true時,將會啟用通信時的數字簽名;
- encode/decode: 消息的編碼解碼方式,如果不配置的話,將會默認使用connector配置中,底層connector提供的相應的編碼解碼函數。
- transports:這個配置選項是用于sioconnector的,因為socket.io的通信方式可能會有多種,如websocket,xhr-polling等等。通過這個配置選項可以選擇需要的方式。
配置connector組件,通過調用如下方式進行:
app.set('connectorConfig', opts);
session組件
session組件跟connector相關,也是僅僅被前端服務器加載,為sessionService提供一個組件包裝, 加載session組件后,會在app的上下文中增加sessionService
,可以通過app.get('sessionService')
獲取。它主要用來維護客戶端的連接信息,以及生成session并維護session。如果與經典TCP進行類比的話,那么session中維護的連接就可以粗略地認為就是TCP服務器端accept返回的socket句柄
。一個連接與一個session對應,同時session組件還維護具體登錄用戶與session的綁定信息。一個用戶可以有多個客戶端登錄,對應于多個session。當需要給客戶端推送消息或者給客戶端返回響應的話,必須通過session組件拿到具體的客戶端連接來進行。
session組件支持如下配置項:
- singleSession: 如果這個配置項配置為true的話,那么將將不允許一個用戶同時綁定到多個session,在綁定用戶一次后,后面的綁定將會失敗。
配置session組件,通過調用如下方式進行:
app.set('sessionConfig', opts);
connection組件
connection組件是一個功能相對簡單的組件,也是僅僅被前端服務器加載,為connectionService提供一個組件包裝,他主要進行連接信息的統計,connector組件接收到客戶端連接請求以及有客戶端離線時,以及用戶登錄下線等等情況,都會向其匯報。
connection組件無配置項。
server組件
server組件也是一個功能比較復雜的組件,它被除master外的服務器加載。server組件會加載并維護自身的Filter信息和Handler信息。server組件會從connector組件的回調里獲得到相應的客戶端請求或者通知,然后會使用自己的before filters對其消息進行過濾,再次調用自己的相應Handler進行請求的邏輯處理,然后將響應通過回調的方式發給connector處理。最后調用after filters進行一些清理處理。
當然,如果客戶請求的服務本來就是前端服務器提供的話,會是上面的那種處理流程。如果客戶請求的服務是后端服務器提供的服務的話,則將不是上面的那種處理流程,此時會出現sys rpc調用。前面那種前端服務器自己處理的情況具體調用為doHandle,而發起rpc調用的情況則為doForward。這兩種處理流程的不同點是,對于自身的請求,調用自己的filter-handler鏈進行處理,對于不是前端服務器自己提供的服務,則是發起一個sys rpc,然后將rpc調用的結果作為響應,發給connector進行處理。關于這個rpc調用則是pomelo內建的msgRemote實現的。
對于后端服務器來說,其客戶請求不是直接來源于真實的客戶端,而是來源于前端服務器對其發起的sys rpc調用,這個rpc調用的實現就是pomelo內建的msgRemote,在msgRemote的實現里,會將來自前端服務器的sys rpc調用請求派發給后端服務器的server組件,然后后端服務器會啟用filter-handler鏈對其進行處理,最后通過rpc調用的返回將具體的響應返回給前端服務器。
在前端服務器將客戶端請求向后端服務器分派時,由于同類型的后端服務器往往有很多,因此需要一個路由策略router,一般情況下用戶通過Application.route調用為后端服務器配置router。
server組件無配置項。
pushScheduler組件
pushScheduler組件也是一個功能較為簡單的組件,它僅僅被前端服務器加載,與connector組件的關系密切。當connector組件收到server組件的對客戶端請求的響應后,connector并不直接將此響應返回給客戶端,而是將這個給客戶端發送響應的操作調度給scheduler組件。pushScheduler組件完成最后通過session組件拿到具體的客戶端連接并將請求的響應發送給客戶端的任務。因此,通過pushScheduler組件可以對發給用戶的響應進行緩沖,從而提高通信效率。pomelo實現了兩種調度策略,一種是不進行任何緩沖,直接將響應發送給客戶端,一種是進行緩沖,并定時地將已緩沖的響應發送給對應的客戶端。
pushScheduler配置項:
- scheduler: scheduler組件的具體調度策略配置,默認的是直接將響應發給客戶端,同時pomelo還提供了有緩沖并且定時刷新的調度策略。用戶也可以自定義自己的調度策略。
配置pushScheduler組件,通過調用如下:
app.set('pushSchedulerConfig', opts);
如果要啟用使用緩沖的scheduler的話,可以在app.js中增加:
app.set('pushSchedulerConfig', {scheduler: pomelo.pushSchedulers.buffer, flushInterval: 20});
flushInterval是刷新周期,默認為20毫秒。
proxy組件
proxy組件是一個重量級的組件,它被除master外的所有服務器加載。proxy組件會掃描具體應用服務器的目錄,抽取其中的remote部分,由于javascript語言的動態性,可以很輕易地獲得到remote中的關于遠程調用的元信息,生成stub,并將這些調用都掛到app.rpc下面,當用戶發起rpc調用時,proxy組件會查看其掃描到的stub信息,以此決定此遠程調用是否合法。同時,proxy又會創建一個RpcClient,當發起遠程調用時,負責與遠端的remote進行通信,并得到遠程調用的結果供調用者使用。當進行遠程調用時,由于同類型的遠程服務器可能有多個,所以這里同樣需要配置相應的router。
proxy的配置項:
- cacheMsg, 配置cacheMsg為true的話,將開啟rpc調用時的對消息的緩沖,而不是直接一旦有rpc請求就發出。
- interval, 與配置參數cacheMsg配合使用,設置flush緩存的周期
- mailBoxFactory, rpc底層實現需要的,用戶可以定義自己的mailBoxFactory,我們將在rpc原理里面詳述。
另外,可以開啟rpc的調用日志,通過如下的調用:
app.enable('rpcDebugLog');
配置proxy使用:
app.set('proxyConfig', opts);
remote組件
remote組件是與proxy組件對等的組件,它用來提供rpc調用服務。rpc組件完成對當前服務器的remote的加載,并開啟監聽端口,等待rpc客戶端的連接及相應的rpc調用。當接收到具體的調用請求時,會根據調用請求中描述的調用請求信息,調用相應的remote中的相應方法。然后再將具體的處理結果返回給rpc客戶端。rpc服務端還支持對調用請求的filter,也就是說跟server組件處理客戶端請求一樣,rpc服務端處理具體請求時也會使用filter-remote鏈進行處理。
remote組件配置項:
- cacheMsg, 與proxy組件的含義相同
- interval, 與proxy組件的含義相同
- acceptorFactory, rpc底層實現需要的,可以認為跟proxy配置中的mailBoxFactory是對等的,我們將在rpc原理里面詳述。
跟proxy組件一樣,用戶可以開啟rpcDebugLog來得到所有的rpc調用過程的日志。 配置remote組件使用:
app.set('remoteConfig', opts);
dictionary組件
dictionary組件是一個可選組件,不會被默認加載,只有當connector組件的配置中開啟了useDict的時候,此組件才會被加載。此組件會遍歷所有handler的route字符串,還會從config/dictionary.json中讀取客戶端的route字符串,然后對這些字符串進行編碼,給予每一個路由賦予一個唯一的小整數,實現route信息壓縮,當客戶端與前端服務器通信時需要路由信息時,將不會再使用很長的那個字符串,而僅僅使用一個小整數。
dictionary的配置項:
-
dict, 客戶端路由字符串文件的位置,默認使用的是config/dictionary.json 配置dictionary組件使用:
app.set('dictionaryConfig', opts);
protobuf組件
protobuf組件也是一個可選組件,不會被默認加載,只有當connector組件的配置中開啟了useProtobuf的時候,此組件才會被加載。此組件會加載對應的proto文件,并完成消息的基于protobuf的編解碼。默認的proto文件的配置信息在config/serverProtos.json和config/clientProtos.json中。具體會在詳細介紹pomelo-protobuf中詳細介紹。
protobuf組件無配置項。
channel組件
channel組件維護channel信息,可以被除了master之外的服務器加載。channel組件可以看作是channelService的組件包裝,加載該組件后,會在app上下文中加入channelService
,可以通過app.get('channelService')
獲取。可以認為一個channel就是一個用戶的集合,每一個用戶大致對應于前端服務器中的一個session,用戶可以通過channel組件向一個channel里面的所有用戶推送消息。當然,由于后端服務器并不與客戶端直接相連,故后端服務器會發起一個sys rpc來表示向客戶端推送消息,接受這個遠程調用的是pomelo已經實現的ChannelRemote。
channel組件的配置項:
-
broadcastFilter, broadcast的過濾函數。會在執行channel的broadcast的時候,在前端服務器上,在消息發送給每個session之前,進行一個過濾。其函數簽名為
broadcastFilter(session, msg, filterParam)
其中filterParam參數由在channelService的broadcast調用時傳入,如下:
channelService.broadcast(type, route, {filterParam: param}, cb);
可以通過如下方式對Channel組件進行配置:
app.set('channelConfig', opts)
backendSession組件
BackendSession組件可以看作是BackendSessionService的組件包裝,加載該組件后,會在app的上下文中加入backendSessionService
,可以通過app.get('backendSessionService')
調用獲取。可以被除了master之外的服務器加載。它主要為后端服務器提供BackendSession信息,并通過遠程過程調用完成一些比如對原始session綁定uid等操作。
backendSession組件無配置項。