WebSocket 協議解決了瀏覽器和服務器之間的全雙工通訊問題。在WebSocket出現以前,瀏覽器若是須要從服務器及時得到更新,則須要不停的對服務器主動發起請求,也就是 Web 中經常使用的 poll 技術。這樣的操做很是低效,這是由於每發起一次新的 HTTP 請求,就須要單獨開啓一個新的 TCP 連接,同時 HTTP 協議自己也是一種開銷很是大的協議。爲了解決這些問題,因此出現了 WebSocket 協議。WebSocket 使得瀏覽器和服務器之間能經過一個持久的 TCP 連接就能完成數據的雙向通訊。關於 WebSocket 的 RFC 提案,能夠參看 RFC6455。javascript
WebSocket 和 HTTP 協議通常狀況下都工做在瀏覽器中,但 WebSocket 是一種徹底不一樣於 HTTP 的協議。儘管,瀏覽器須要經過 HTTP 協議的 GET 請求,將 HTTP 協議升級爲 WebSocket 協議。升級的過程被稱爲 握手(handshake)。當瀏覽器和服務器成功握手後,則能夠開始根據 WebSocket 定義的通訊幀格式開始通訊了。像其餘各類協議同樣,WebSocket 協議的通訊幀也分爲控制數據幀和普通數據幀,前者用於控制 WebSocket 連接狀態,後者用於承載數據。下面咱們將一一分析 WebSocket 協議的握手過程以及通訊幀格式。css
握手的過程也就是將 HTTP 協議升級爲 WebSocket 協議的過程。前面咱們說過,握手開始首先由瀏覽器端發送一個 GET 請求開發,該請求的 HTTP 頭部信息以下:html
Connection: Upgrade Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: lGrvj+i7B76RB3YYbScQ9g== Sec-WebSocket-Version: 13 Upgrade: websocket
當服務器端,成功驗證了以上信息後,則會返回一個形如如下信息的響應:前端
Connection: upgrade Sec-WebSocket-Accept: nImJE2gpj1XLtrOb+5cBMJn7bNQ= Upgrade: websocket
能夠看到,瀏覽器發送的 HTTP 請求中,增長了一些新的字段,其做用以下所示:java
返回的響應中,若是握手成功會返回狀態碼爲 101 的 HTTP 響應。同時其餘字段說明以下:jquery
當瀏覽器和服務器端成功握手後,就能夠傳送數據了,傳送數據是按照 WebSocket 協議的數據格式生成的。git
數據幀的定義相似於 TCP/IP 協議的格式定義,具體看下圖:github
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
以上這張圖,一行表明 32 bit (位) ,也就是 4 bytes。整體上包含兩份,幀頭部和數據內容。每一個從 WebSocket 連接中接收到的數據幀,都要按照以上格式進行解析,這樣才能知道該數據幀是用於控制的仍是用於傳送數據的。web
模塊文檔:redis
OR 的 websocket 庫已經默認安裝了, 咱們在此用到的是 resty.websocket.server
(ws服務端)模塊, server模塊提供了各類函數來處理 WebSocket 定義的幀。
local server = require "resty.websocket.server" local wb, err = server:new{ timeout = 5000, -- in milliseconds max_payload_len = 65535, } if not wb then ngx.log(ngx.ERR, "failed to new websocket: ", err) return ngx.exit(444) end
Methods
模塊文檔:
resty.redis 模塊實現了 Redis 官方全部的命令的同名方法, 這裏主要用到的是redis的發佈訂閱相關功能。
local redis = require "resty.redis" local red = redis:new() red:set_timeout(1000) -- 1 sec -- or connect to a unix domain socket file listened -- by a redis server: -- local ok, err = red:connect("unix:/path/to/redis.sock") local ok, err = red:connect("127.0.0.1", 6379) if not ok then ngx.say("failed to connect: ", err) return end
Methods
-- 簡易聊天室 local server = require "resty.websocket.server" local redis = require "resty.redis" local channel_name = "chat" local uname = "網友" .. tostring(math.random(10,99)) .. ": " -- 建立 websocket 鏈接 local wb, err = server:new{ timeout = 10000, max_payload_len = 65535 } if not wb then ngx.log(ngx.ERR, "failed to create new websocket: ", err) return ngx.exit(444) end local push = function() -- 建立redis鏈接 local red = redis:new() red:set_timeout(5000) -- 1 sec local ok, err = red:connect("172.17.0.3", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect redis: ", err) wb:send_close() return end --訂閱聊天頻道 local res, err = red:subscribe(channel_name) if not res then ngx.log(ngx.ERR, "failed to sub redis: ", err) wb:send_close() return end -- 死循環獲取消息 while true do local res, err = red:read_reply() if res then local item = res[3] local bytes, err = wb:send_text(item) if not bytes then -- 錯誤直接退出 ngx.log(ngx.ERR, "failed to send text: ", err) return ngx.exit(444) end end end end -- 啓用一個線程用來發送信息 local co = ngx.thread.spawn(push) -- 主線程 while true do -- 若是鏈接損壞 退出 if wb.fatal then ngx.log(ngx.ERR, "failed to receive frame: ", err) return ngx.exit(444) end local data, typ, err = wb:recv_frame() if not data then -- 空消息, 發送心跳 local bytes, err = wb:send_ping() if not bytes then ngx.log(ngx.ERR, "failed to send ping: ", err) return ngx.exit(444) end ngx.log(ngx.ERR, "send ping: ", data) elseif typ == "close" then -- 關閉鏈接 break elseif typ == "ping" then -- 回覆心跳 local bytes, err = wb:send_pong() if not bytes then ngx.log(ngx.ERR, "failed to send pong: ", err) return ngx.exit(444) end elseif typ == "pong" then -- 心跳回包 ngx.log(ngx.ERR, "client ponged") elseif typ == "text" then -- 將消息發送到 redis 頻道 local red2 = redis:new() red2:set_timeout(1000) -- 1 sec local ok, err = red2:connect("172.17.0.3", 6379) if not ok then ngx.log(ngx.ERR, "failed to connect redis: ", err) break end local res, err = red2:publish(channel_name, uname .. data) if not res then ngx.log(ngx.ERR, "failed to publish redis: ", err) end end end wb:send_close() ngx.thread.wait(co)
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <style> p{margin:0;} </style> <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script type="text/javascript"> var ws = null; function WebSocketConn() { if (ws != null && ws.readyState == 1) { log("已經在線"); return } if ("WebSocket" in window) { // Let us open a web socket ws = new WebSocket("ws://123.207.144.90/ws"); ws.onopen = function () { log('成功進入聊天室'); }; ws.onmessage = function (event) { log(event.data) }; ws.onclose = function () { // websocket is closed. log("已經和服務器斷開"); }; ws.onerror = function (event) { console.log("error " + event.data); }; } else { // The browser doesn't support WebSocket alert("WebSocket NOT supported by your Browser!"); } } function SendMsg() { if (ws != null && ws.readyState == 1) { var msg = document.getElementById('msgtext').value; ws.send(msg); } else { log('請先進入聊天室'); } } function WebSocketClose() { if (ws != null && ws.readyState == 1) { ws.close(); log("發送斷開服務器請求"); } else { log("當前沒有鏈接服務器") } } function log(text) { var li = document.createElement('p'); li.appendChild(document.createTextNode(text)); //document.getElementById('log').appendChild(li); $("#log").prepend(li); return false; } WebSocketConn(); </script> </head> <body> <div id="sse"> <a href="javascript:WebSocketConn()">進入聊天室</a> <a href="javascript:WebSocketClose()">離開聊天室</a> <br> <br> <input id="msgtext" type="text"> <br> <a href="javascript:SendMsg()">發送信息</a> <br> <br> <div id="log"></div> </div> </body> </html>