基於socket.io的實時消息推送

用戶訪問Web站點的過程是基於HTTP協議的,而HTTP協議的工做模式是:請求-響應,客戶端發出訪問請求,服務器端以資源數據響應請求。 也就是說,服務器端始終是被動的,即便服務器端的資源數據發生變化,若是沒有來自客戶端的請求,用戶就不會看到這些變化。 這種模式是不適合某些應用場景的,好比在社交網絡用戶須要近乎實時地知道其餘用戶最新的信息。對於普通站點來講, 請求-響應模式能夠知足絕大多數的功能需求,但總有某些功能咱們但願可以爲用戶提供實時消息的體驗。php

爲解決這個問題,有兩種方案能夠選擇:git

  1. 仍舊使用請求-響應模式,只是增大請求的頻率或者使用長鏈接,來達到儘量接近實時的效果,如使用polling/long-polling,但可能會極大地增長服務器的負載壓力或下降服務器的吞吐量
  2. 使用新的協議,在服務器端有資源數據更新時,主動推送給客戶端,如WebSocket,雖然這種思路也是使用了長鏈接,但效率更高,且是客戶端服務器端之間的全雙工通訊。 問題在於目前各大瀏覽器並不都支持WebSocket。

那麼目前最好的方式就是結合以上兩種方案,在不一樣的瀏覽器中,儘量使用瀏覽器支持的最好的方案,即瀏覽器支持第二種方案時,優先使用第二種方案,不然使用第一種方案。socket.io就是這麼作的,而且在服務器端和客戶端對於不一樣的方案提供統一的接口。github


在咱們產品的站內信功能中,但願可以給在線用戶實時推送公共消息或私有消息。考慮到之後可能還有其餘功能須要實現實時消息推送,因此將實時消息推送實現爲一個單獨的服務。這種針對不一樣特性的功能進行解耦也爲以後針對性的優化作了鋪墊。後端

解耦以後的系統結構以下所示:瀏覽器

socket.io-push-server

當站點服務器(A)監測到資源數據更新事件發生時,先將數據推送到消息推送服務器(B),B根據消息的類型以及消息的目標接收人來決定是否推送,如何推送。服務器

因爲咱們的Web後端是基於Yii框架實現,那麼該如何實現A與B的socket.io服務通訊呢?socket.io有本身的一套協議,若是本身實現PHP庫來與socket.io服務交互,還有一些工做量。最終咱們選擇elephant.io這個PHP庫,並將elephant.io封裝爲Yii框架的一個組件,實現以下:cookie

<?php $basePath = Yii::getPathOfAlias('application.vendor.elephantio.lib.ElephantIO'); require_once($basePath . DIRECTORY_SEPARATOR . 'Client.php'); require_once($basePath . DIRECTORY_SEPARATOR . 'Payload.php'); use ElephantIO\Client as Elephant; class extElephantIO extends CApplicationComponent { public $host = null; public $port = null; public $namespace = null; private $elephant = null; private $ioNameSpace = null; public function init() { if ($this->host === null || $this->port === null) { throw new Exception('%s: %s: %s, Please give me parameters host and port', basename( __FILE__ ), __FUNCTION__, __LINE__); } } public function setNameSpace($nameSpace) { if ($this->elephant === null) { $this->elephant = new Elephant('http://' . $this->host . ':' . $this->port, 'socket.io', 1, false, true, true); $this->elephant->init(); } $this->ioNameSpace = $this->elephant->createFrame(null, $nameSpace); } public function sendMsg($event, $msg) { if ($this->ioNameSpace === null) { if ($this->namespace !== null) { $this->ioNameSpace = $this->elephant->createFrame(null, $this->namespace); } else { throw new Exception('%s: %s: %s, Please setNameSpace before sendMsg', basename( __FILE__ ), __FUNCTION__, __LINE__); } } $this->ioNameSpace->emit($event, $msg); } public function close() { $this->elephant->close(); $this->elephant = null; } }

將該代碼文件放在應用目錄extensions下,而後爲Yii添加以下配置項:網絡

'components' => array( 'ElephantIO' => array( 'class' => 'application.extensions.extElephantIO', 'host' => 'xxx', 'port' => xxx, ), ... ),

當有資源數據變動事件產生時,以下調用向消息推送服務器發送消息:併發

$elephant = Yii::app()->ElephantIO; $elephant->setNameSpace('/message_namespace'); $elephant->sendMsg( 'message_event_type', $messageContent ); $elephant->close();

對於私有消息推送,如何判斷用戶當前是否在線?如何驗證用戶的身份?app

能夠基於cookie來實現,socket.io提供的瀏覽器端JS庫,在每次鏈接時,和普通HTTP請求同樣,會攜帶站點域名下的cookie(咱們的消息推送服務的域名爲站點服務器域名的子域,因此能拿到站點域名下的全部cookie),消息推送服務器在接收到鏈接(connection事件)請求時,從鏈接中取出全部cookie,而後向站點Web後端的某個API轉發這些cookie,這個API根據cookie驗證用戶身份,並將用戶信息返回給消息推送服務器,消息推送服務器根據用戶信息存儲當前鏈接對象,以後當站點服務器向消息推送服務器發送該用戶的消息時,就經過該鏈接對象給用戶推送消息。

對於公有消息,即廣播消息,實現則比較簡單,直接向當前全部鏈接推送消息便可。


也許有人會問,既然在某些瀏覽器中socket.io會退化爲使用polling/long-polling來傳輸消息,那麼相比直接向站點服務器進行polling/long-polling,有什麼優點嗎?

我認爲優點有兩點:

  1. NodeJS的異步事件回調的方式,適合大併發長鏈接的應用場景。若是Web後端是使用PHP等實現,則更適合短鏈接的服務。
  2. 將站點的業務邏輯與消息推送邏輯解耦,那麼瀏覽器經過polling/long-polling來獲取消息時,只是涉及消息推送邏輯,不須要執行業務邏輯的代碼,而業務邏輯的代碼可能很複雜,每次polling,都須要執行一遍的話,會浪費服務器不少資源。
相關文章
相關標籤/搜索