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>