skynet總體架構


前言

skynet是我們游戲服務端的底層框架,當初在技術選型的時候仔細閱讀過它的源碼,發現它是一個C語言的工程典范。大多數游戲服務端,要么使用C++,要么使用java,使用C是非常少見的。但是skynet通過C和Lua的結合,實現了一個高效的游戲框架,C層沒有多余的一堆三方庫,只有緊湊的核心結構,提供最核心的消息處理框架;Lua層用來寫游戲邏輯,降低了開發門檻。

目前skynet在阿里游戲大量使用,據我所聞風之大陸,時下很火的三國志使用的都是skynet,而我們游戲當然也用這個框架,已經穩定運營了一年有余。

說起來skynet並不能算是一個游戲服務端框架,它只是提供了一些游戲服務端必須的基礎設施,可以用這套設施去設計符合要求的上層邏輯。按照雲風的說法,skynet實現了類似Erlang 的 Actor 模型,它本質上是一個高並發的消息處理框架,消息從底層派發給上層的“服務”去處理,這里的服務可以用C編寫,當然大部分時候都是用Lua編寫,每個Lua服務是一個獨立的Lua虛擬機,這就保證了服務之間的環境隔離,Lua服務使用協程處理消息,當需要向其他服務通訊時,協程可以掛起等其他服務返回再繼續,這讓我們一方面能像寫同步代碼一樣“順序執行”,另一方面當協程掛起時,該服務可以處理其他消息,這就保證了消息的高並發。

由於skynet內核的精簡,很多人抱着開箱即用的想法,后面發現門檻其實並不低,它仍然要求你對游戲服務器的業務很熟悉,知道自己想要實現什么,然后自己動手。但是正是由於它的精簡,使得他的可定制性很高。

skynet的核心功能

如果要用一句話描述skynet核心功能是什么:它仍然是一個基於事件的高並發消息處理框架。事件主要來源於網絡,定時器和信號通知等,當事件觸發時,skynet將這些事件統一編碼成消息結構,派發給感興趣的服務處理;而服務在處理消息時,也可以主動向其他服務發送消息。因此他是事件來驅動的,如果沒有前面說的那些事件,skynet就沒法做任何事情。

skynet的核心數據結構是 skynet_context ,我對Erlang不熟悉,所以沒法說出它對應於Erlang的什么結構;但它實際上也像操作系統中的進程的概念,在這里我們把它稱之為服務,一個服務包含了下面幾個東西:

  • 服務句柄:和進程ID類似,用於唯一標識服務。
  • 服務模塊:模塊以動態庫的形式提供。在創建skynet_context的時候,必須指定模塊的名字,skynet把模塊加載進來,創建模塊實例,實例向服務注冊一個回調函數,用於處理服務的消息。
  • 消息隊列:每個服務都有一個消息隊列,當隊列中有消息時,會主動掛到全局鏈表。skynet啟動了一定數量的工作線程,不斷從全局鏈表取出消息隊列,派發消息給服務的回調函數去處理。

下面的結構圖展示了skynet最核心的結構:

服務句柄

每個服務都關聯一個句柄,句柄的實現在 skynet_handle.h|c 中,句柄是一個32位無符號整型,最高8位表示集群ID(已不推薦使用),剩下的24位為服務ID。

handle_storage 用於存儲ID和skynet_context的映射:

// 句柄存儲結構 struct handle_storage { struct rwlock lock; // 讀寫鎖  uint32_t harbor; // 集群ID  uint32_t handle_index; // 當前句柄索引  int slot_size; // 槽位數組大小  struct skynet_context ** slot; // skynet_context數組  ... ... };

服務模塊

先來看一下創建服務的API:

// 創建一個服務:name為服務模塊的名字,parm為參數,由模塊自己解釋含義 struct skynet_context * skynet_context_new(const char * name, const char * parm);

這里的name參數就是模塊名,skynet根據這個名字加載模塊,並調用約定好的導出函數。這個過程大概是這樣的:

  • 得到模塊后,調用skynet_module_instance_create函數創建模塊實例。
  • 然后調用skynet_module_instance_init初始化實例,通常實例在初始化時調用skynet_callback向skynet設置回調函數,以后消息處理由該回調函數處理。

消息隊列

創建服務時也會新建一個消息隊列,消息隊列在 skynet_mq.c|h 中實現,消息隊列用下面的結構表示:

// 消息隊列 struct message_queue { struct spinlock lock; uint32_t handle; // 關聯的服務句柄  int cap; // 隊列容量  int head; // 隊列頭的位置  int tail; // 隊列尾的位置  struct skynet_message *queue; // 消息結構數組  struct message_queue *next; // 指向下一個消息隊列  ... ... };

next指向下一個消息隊列,也就是說message_queue會形成一個鏈表,然后由global_queue持有,global_queue就這樣的:

struct global_queue { struct message_queue *head; struct message_queue *tail; struct spinlock lock; };

global_queue持有的鏈表是需要處理消息的消息隊列,這個過程是這樣的:

  • 調用skynet_mq_push向消息隊列壓入一個消息。
  • 然后,調用skynet_globalmq_push把消息隊列鏈到global_queue尾部。
  • 從全局鏈表彈出一個消息隊列,處理隊列中的消息,如果隊列的消息處理完則不壓回全局鏈表,如果未處理完則重新壓入全局鏈表,等待下一次處理。

描述得比較簡單,具體的細節還是要查看skynet_context_message_dispatch這個函數。

skynet啟動及消息處理

上面把服務的三個重要組成部分介紹完,現在可以來看看skynet_context的內容了:

struct skynet_context { void * instance; // 服務模塊的實例指針  struct skynet_module * mod; // 服務模塊指針  void * cb_ud; // 回調函數的用戶數據  skynet_cb cb; // 服務處理消息的回調函數  struct message_queue *queue; // 消息隊列  uint32_t handle; // 服務句柄  ... ... };

其實包含的最核心的部分就是上面介紹的三個,那么skynet是怎么樣啟動起來,並不斷地處理消息呢?答案就是skynet_start這個函數:

  • 第一步初始化各個功能模塊,比如句柄,消息隊列,模塊,定時器,socket等等。
  • 然后創建一個logger服務。創建一個bootstrap服務。
  • 接着創建一定數量的工作線程,這個數量可由配置指定,工作線程的責任就是派發消息。
  • 創建定時器線程,用於記錄時間以及實現timeout事件;
  • 創建sokcet線程,用於處理sokcet消息,socket和timeout事件最終都會轉化成消息,交給工作線程派發給服務處理。
  • 創建monitor線程,這個線程的作用是監控服務有沒有出現死循環。

前面說過,skynet是由事件驅動運行的,這里的事件主要就是兩個,一個是socket,另一個是timeout。分別由兩個線程驅動運行。

工作線程的核心邏輯就是調用skynet_context_message_dispatch去派發消息,派發完成后,它會進入睡眠狀態,等待另外兩個線程來喚醒。這就是非常典型的生產消費者模型,絕大多數服務器程序的核心功能就是這個,skynet也不例外:

 

 

from:https://zhuanlan.zhihu.com/p/84634254


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM