使用websocket構建實時通知程序或者web聊天程序

先搞清楚幾個概念 


1、WebSocket是什麼?

WebScoket是一種讓客戶端和服務器之間能進行雙向實時通信的技術。它是HTML最新標準HTML5的一個協議規範,本質上是個基於TCP的協議,它通過HTTP/HTTPS協議發送一條特殊的請求進行握手後創建了一個TCP連接,此後瀏覽器/客戶端和服務器之間便可以通過此連接來進行雙向實時通信。

2、爲什麼要用WebSocket?

1)一直以來,HTTP協議是無狀態、單向通信的,即客戶端請求一次,服務器回覆一次。如果想讓服務器消息及時下發到客戶端,需要採用類似於輪詢的機制,即客戶端定時頻繁的向服務器發出請求,這樣效率很低,而且HTTP數據包頭本身的字節量較大,浪費了大量帶寬和服務器資源;

2)爲提高效率,出現了AJAX/Comet技術,它實現了雙向通信且節省了一定帶寬,但仍然需要發出請求,本質上仍然是輪詢;

3)新一代HTML標準HTML5推出了WebSocket技術,它使客戶端和服務器之間能通過HTTP協議建立TCP連接,之後便可以隨時隨地進行雙向通信,且交換的數據包頭信息量很小;

Azure 應用程式閘道中的WebSocket 支援| Microsoft Docs

3、如何使用WebSocket?

在支持WebSocket的瀏覽器中,創建Socket之後,通過onopen、onmessage、onclose、onerror四個事件的實現來處理Socket的響應;

4、WebSocket與HTTP、TCP的關係

WebSocket和HTTP都屬於應用層協議,且都是基於TCP的,它們的send函數最終也是通過TCP系統接口來做數據傳輸。那麼WebSocket和HTTP的關係呢?WebSocket在建立握手連接時,數據是通過HTTP協議傳輸的,但是在連接建立後,真正的數據傳輸階段則不需要HTTP協議的參與。中間重疊的部分是表示升級到websocket協議,它們之間的關係如下圖:

WebSocket 協議簡介- Aidan's personal website

5、什麼情況下使用WebSocket?

如果遊戲需要同時支持手機端、Web端,那毫無疑問應該使用WebSocket,現在各個平臺都提供了相應的WebSocket實現。如果遊戲不需要支持Web端,且對實時性要求比較高,如多人射擊、MMORPG之類,那麼使用TCP/UDP結合的原生Socket會比較好。

6、SocketIO

WebSocket是HTML5最新提出的規範,雖然主流瀏覽器都已經支持,但仍然可能有不兼容的情況,爲了兼容所有瀏覽器,給程序員提供一致的編程體驗,SocketIO將WebSocket、AJAX和其它的通信方式全部封裝成了統一的通信接口,也就是說,我們在使用SocketIO時,不用擔心兼容問題,底層會自動選用最佳的通信方式。因此說,WebSocket是SocketIO的一個子集。

首先用composer程序中加入workman依賴

如果你對composer不熟悉,可以移步這裏, https://blog.csdn.net/robinhunan/article/details/106377501 文章詳細介紹了composer的配置,使用教程。

composer require  walkor/workerman

 

Server端程序,編輯server端程序ws.php 

<?php
/**
 * websocket server程序,監聽端口19988
 */
use Workerman\Worker;
require_once __DIR__.'/vendor/autoload.php';

//初始化一個worker容器,監聽19988端口
$worker = new Worker('websocket://0.0.0.0:19988');

/**
 * 這裏進程數必須設置爲1,否則會報端口占用錯誤
 * (php7 可以設置進程數大於1,前提是$inner_worker->reusePort=true)
 */
$worker->count = 1;

//$worker進程啓動後創建一個text Worker,以便打開一個內部通訊端口
$worker->onWorkerStart = function($worker)
{
    //開啓一個內部端口,方便內部系統推送數據,Text協議格式 文本+換行符
    $inner_text_worker = new Worker('text://0.0.0.0:19989');
    $inner_text_worker->onMessage=function($connection,$buffer)
    {
        //$data數組格式,裏面有uid,表示向那個uid的頁面推送數據
        $data = json_decode($buffer,true);
        $uid = $data['uid']; 

        //通過workerman,向uid的頁面推送數據
        $ret = sendMessageByUid($uid,$buffer);

        //返回推送結果
        $connection->send($ret?'ok':'fail');
    };
    $inner_text_worker->listen();
};

//新增一個屬性,用來保存uid到connection的映射
$worker->uidConnections = array();

//當有客戶端發來消息時執行的回調函數
$worker->onMessage = function($connection,$data)
{
    global $worker;
    $date = date('Y-m-d H:i:s',time());
    file_put_contents('./workerman.log',$date.' :  '.$data.PHP_EOL,FILE_APPEND );
    $data = json_decode($data,true);

    if(!isset($connection->uid))
    {
        //正式環境需要根據$data裏面的信息驗證用戶身份,演示用直接根據客戶端第一次發來的uid直接綁定了用戶
        $connection->uid = $data['uid']; 

        //保存uid到connection映射,這樣可以方便的通過uid查找connection,實現針對特定的uid推送數據
        $worker->uidConnections[$connection->uid] = $connection;
        return;
    }
};

//當有客戶端連接斷開時,刪除映射
$worker->onClose = function($connection)
{
    global $worker;
    if(isset($connection->uid))
    {
        //連接斷開時,刪除映射
        unset($worker->uidConnections[$connection->uid]);
    }
};

//向所有驗證的用戶推送數據
function broadcast($message)
{
    global $worker;
    foreach($worker->uidConnections as $connection)
    {
        $connection->send($message);
    }
}

//針對uid推送數據
function sendMessageByUid($uid,$message)
{
    global $worker;
    if(isset($worker->uidConnections[$uid]))
    {
        $connection = $worker->uidConnections[$uid];
        $connection->send($message);
        return true;
    }
    return false;
}

//運行所有worker
Worker::runAll();

    啓動websocket的server程序 

     瀏覽器端程序或者客戶端程序 client.html ,通過瀏覽器,建議是chrome或者firefox瀏覽器訪問 http://localhost/client.html, 並打開控制檯,我這裏是簡化了流程,正常的話,還是需要登錄的,可以在下面的send消息裏面,加上用戶信息,server端判斷是否正確,再綁定,爲了簡化理解,我就去掉了websocket登錄部分。

<script>
ws = new WebSocket("ws://127.0.0.1:19988/");
ws.onopen = function() {
    console.log("連接成功");
    ws.send('{"uid":"yubing"}'); //這裏放登錄信息
    console.log("給服務器發送uid信息:yubing");
};
ws.onmessage = function(e) {
    console.log("收到服務端的消息:" + e.data);
};
</script>

打開控制檯後,可以發現瀏覽器已經連接上websoket服務器,並且給服務器發送了一個消息。

 

第三方客戶端,直接調用tcp發送消息,下面的例子client.php 演示了通過php直接發送推送消息,執行php client.php ,瀏覽器對應的上個用戶我這裏設置的是yubing,就能收到消息了。

<?php
//建立socket連接到內部推送端口
$client = stream_socket_client('tcp://127.0.0.1:19989',$errno,$errmsg,1);

//推送的數據,包含uid字段,表示是給這個uid推送
$data = array('uid'=>'yubing','status' => 'success','msg' => date('Y-m-d H:i:s'));

//發送數據,注意19989端口是text協議的端口,text協議需要在數據末尾加上換行符
fwrite($client,json_encode($data)."\n");

//讀取推送結果
echo fread($client,8192);

 切換到瀏覽器,會發現瀏覽器已經收到了,php 客戶端發送過來的消息。

 

原理

一般我們開發的WebSocket服務程序使用ws協議,明文的。但是怎樣讓它安全的通過互聯網傳輸呢?這時候可以通過nginx在客戶端和服務端直接做一個轉發了, 客戶端通過wss訪問,然後nginx和服務端通過ws協議通信。如下圖所示:

 nginx代理websocket服務配置文件

upstream websocket1{
    ip_hash;
    server localhost:19988 weight=50 fail_timeout=10s;
    server localhost:29988 weight=50 fail_timeout=10s;
}
 
server
{
	listen 80;
	listen 443;
	#listen [::]:80;
	server_name test.com.cn;
	index index.html index.htm index.php;
	root  /data/www/web/public;

	charset utf-8;

	ssl on;
	ssl_certificate /usr/local/nginx/conf/cert/test.com.cn.crt;
	ssl_certificate_key /usr/local/nginx/conf/cert/test.com.cn.key;
	ssl_session_timeout 5m;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
	ssl_prefer_server_ciphers on;



	location /wss
	{
		proxy_pass http://websocket1;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_set_header X-real-ip $remote_addr;
		proxy_set_header HOST $host;
		proxy_set_header X-Forwarded-For $remote_addr;
	}
}

這時候客戶端通過wss://test.com.cn/wss,就可以加密連接了。

<script>
ws = new WebSocket("wss://test.com.cn/wss");
ws.onopen = function() {
}
</script>

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章