簡介
fooking是一個分佈式網關,其主要目的是用於承載客戶端長連接,然後將接受的客戶端數據以fastcgi協議轉發給後端業務邏輯處理服務器,讓後端服務真正獨立的同時還無需關心擴展的問題,簡單配置即可。
fooking服務包含兩部分,一部分是gateway主要用於承載客戶端鏈接、轉發請求;另一部分是router,主要用於各gateway之間的消息傳遞、數據統計等。gateway可以開多個進程,並且可以配置多臺服務器來提升連接數。
優勢
1、非侵入式,無需安裝擴展,php版本隨你挑,後端錯誤邏輯不會影響網關正常服務
2、開發簡單,做爲後端php可以像寫web一樣,輕鬆echo即可
3、動態網關,方便線上應急擴容
4、session維持,每個客戶端分配唯一ID,無需關心fd
5、單播/組播,指定一人或者多人或者羣組發送消息
6、服務器狀態監控,監控信息細化到每個gateway的進程
7、客戶端事件通知,連接打開與關閉
8、無語言限制,後端只需遵循fastcgi協議即可
9、自定義協議,可用lua自定義協議處理
10、負載均衡,配合分配策略,可讓每臺服務器,甚至每個進程的連接數量基本相同
getting started
既然你已經點到這裏了,那麼我相信你應該瞭解fooking的基本架構了,那麼開始我們的fooking初體驗。
fooking使用c++開發,安裝g++、make等基礎開發工具,那麼接下來的事就簡單了(windows下可以使用cygwin編譯爲exe)。
git clone http://git.oschina.net/scgywx/fooking.git
make
第一招,啓動router。在啓動之前我得再BB兩句,fooking的配置文件和腳本都是使用lua,所以建議瞭解一下lua的基本知識(只是建議,不是必須)。
router的配置文件位於src/router.lua,配置文件裏面的參數都有詳細的解釋,這裏就不多說了,接下來只需要按這個姿勢執行命令就能啓動router了。
cd src
./fooking ../router.lua
第二招,啓動gateway。配置文件位於src/config.lua,配置參數相必閣下見其名便能知其意,只是其中幾個參數需要說明,1是ROUTER_HOST與ROUTER_PORT,這裏配置剛剛啓動router的host與port,主要與router連通;2是BACKEND_SERVER,列出php-fpm的host與port即可(如果是unix domain socket,要在前面加unix://前綴),3是FASTCGI_ROOT與FASTCGI_FILE,分別是php腳本路徑與腳本文件名(僅測試可指到example/chat目錄下)。啓動命令如下
./fooking ../config.lua
第三招,啓動fpm;作爲php後端服務端,php-fpm肯定少不了。
service php-fpm start
第四招,啓動nginx,其實這一步,可有可無的,主要就是用來訪問example/chat下面的靜態資源,把目錄指到example/chat目錄下即可(當然你也可以不用裝nginx,直接用瀏覽器打開example/chat/index.html也行)。
service nginx start
第五招,測試chat,打開你的瀏覽器(需要支持websocket協議的瀏覽器),直接localhost,應該就能聊天了。這個聊天的例子使用了websocket協議,協議部分的實現位於scripts/WebSocket.lua,並且這個文件也配置在config.lua的SCRIPT_FILE參數中。
協議說明
前端協議
前端協議是指gateway與客戶端的通信協議,默認是使用4字節數據長度(大端)+數據,當然你可以使用lua來自定義協議(需要配置SCRIPT_FILE),在lua自定義協議的腳本里有4個事件onConnect,onClose,onInput,onOutput分別對應連接、關閉、輸入(解包)、輸出(打包),具體更詳細的用法可以參見scripts/WebSocket.lua,裏面實現了websocket協議的打包與解包。
後端協議
後端協議是指gateway與後端服務的通信協議,使用fastcgi與php-fpm通信,這個對於phper來說應該不陌生,並且使用很簡單;由於客戶端的請求已經在前端協議中處理了,所以發給php的都是完整的數據包,所以在php裏面要獲取數據包內容就很簡單了,只需要這樣:
file_get_contents("php://input");
另外自定義參數還可以通過配置文件的FASTCGI_PARAMS來隨意添加,在php裏面只需要使用$_SERVER就能獲取參數值了,需要注意的是SESSIONID、EVENT、REMOTE_ADDR、REMOTE_PORT已經被fooking使用,請不需添加在自定義參數列表內,具體說明如下:
$sessionid = $_SERVER['SESSIONID'];//客戶端唯一ID
$event = $_SERVER['EVENT'];//事件類型(0-表示消息事件,1-表示連接事件,2-表示關閉事件)
$ip = $_SERVER['REMOTE_ADDR'];//客戶端ip
$port = $_SERVER['REMOTE_PORT'];//客戶端端口
$myparam = $_SERVER['MY_PARAM'];//自定義參數
那麼php需要返回消息給客戶端怎麼辦?首先你要告訴fooking,你要返回多少數據給客戶端,因此你需要設置header("Content-Length: 字節數"), 接下來只需要echo數據就好了,是不是很簡單?
header("Content-Length: 11");
echo "hello world";
如果你還想指定數據數據的偏移位置,那麼你可以使用Content-Offset,比如:
header("Content-Length: 5");
header("Content-Offset: 6");//也可以使用-5,效果一樣。
echo "hello world";//客戶端會收到world
什麼時候會用到Content-Offset呢?比如你有debug、warning等調試信息在消息前面,這時候就可以使用Content-Offset做負值偏移,同時還能在fooking的日誌中完整的看到debug、warning等信息。
那爲什麼要把debug、warning信息放在消息前面?因爲正文消息可能是加密或者其它二進制流信息,如果帶有\0的字符,那麼日誌就會被截斷,所以把消息放在最後確保日誌能完整輸出。
單播/組播
fooking提供的組播類似於redis的pub/sub,當你對某個組感興趣,可以加入到該組,而該組有消息則會通知你。同時每個client的信息和組信息在gateway上有存儲,並且會同步到router上,所以如果你想要給某個組或者單人發消息,只需要發到router即可,那麼對應php的後端,只需要包含api.php即可。
include 'api.php';
$router = new RouterClient('router host', 'router port');
$router->sendMsg('session id', 'hi');//指定單人發消息
$router->sendAllMsg('hi');//給所有人發消息
$router->kickUser('session id');//關閉指定連接
$router->addChannel('channel name', 'session id');//加入到指定組
$router->delChannel('channel name', 'session id');//從指定組移除
$router->publish('channel name', 'msg');//向指定組發送消息
$router->info();//獲取當前router信息
服務器分配策略
通過使用info接口拉取到各服務器監控信息,可以很輕鬆實現服務器均勻分配,info接口返回如下:
Array(
[clients] => 8 //當前總連接數
[channels] => 0 //當前channel總數
[gateways] => Array(//服務器列表
[1] => Array(
[0] => Array(
[serverid] => 1,//服務器id(這個就是config.lua裏面的SERVER_ID)
[workerid] => 0,//進程編號
[clients] => 4,//當前進程負責連接數
[channels] => 0,//當前進程負責channel數
)
[1] => Array(
[serverid] => 1,
[workerid] => 1,
[clients] => 4,
[channels] => 0,
)
)
)
)
知道info接口數據之後,我們再根據每臺服務器ID當前連接數進行處理,分配當前連接數最少的服務器給客戶端,代碼如下:
$router = new RouterClient();
$info = $router->info();
$gateways = array();
foreach($info[‘gateway’] as $serverid => $workers){
foreach($workers as $worker){
$gateways[$serverid]+= $worker[‘clients’];
}
}
asort($gateways);
$serverid = key($gateways);