先搞清楚幾個概念
1、WebSocket是什麼?
WebScoket是一種讓客戶端和服務器之間能進行雙向實時通信的技術。它是HTML最新標準HTML5的一個協議規範,本質上是個基於TCP的協議,它通過HTTP/HTTPS協議發送一條特殊的請求進行握手後創建了一個TCP連接,此後瀏覽器/客戶端和服務器之間便可以通過此連接來進行雙向實時通信。
2、爲什麼要用WebSocket?
1)一直以來,HTTP協議是無狀態、單向通信的,即客戶端請求一次,服務器回覆一次。如果想讓服務器消息及時下發到客戶端,需要採用類似於輪詢的機制,即客戶端定時頻繁的向服務器發出請求,這樣效率很低,而且HTTP數據包頭本身的字節量較大,浪費了大量帶寬和服務器資源;
2)爲提高效率,出現了AJAX/Comet技術,它實現了雙向通信且節省了一定帶寬,但仍然需要發出請求,本質上仍然是輪詢;
3)新一代HTML標準HTML5推出了WebSocket技術,它使客戶端和服務器之間能通過HTTP協議建立TCP連接,之後便可以隨時隨地進行雙向通信,且交換的數據包頭信息量很小;
3、如何使用WebSocket?
在支持WebSocket的瀏覽器中,創建Socket之後,通過onopen、onmessage、onclose、onerror四個事件的實現來處理Socket的響應;
4、WebSocket與HTTP、TCP的關係
WebSocket和HTTP都屬於應用層協議,且都是基於TCP的,它們的send函數最終也是通過TCP系統接口來做數據傳輸。那麼WebSocket和HTTP的關係呢?WebSocket在建立握手連接時,數據是通過HTTP協議傳輸的,但是在連接建立後,真正的數據傳輸階段則不需要HTTP協議的參與。中間重疊的部分是表示升級到websocket協議,它們之間的關係如下圖:
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>