基于 OpenResty 实现一个 WS 聊天室
基于 OpenResty 实现一个 WS 聊天室
WebSocket
WebSocket 协议分析
WebSocket 协议解决了浏览器和服务器之间的全双工通信问题。在WebSocket出现之前,浏览器如果需要从服务器及时获得更新,则需要不停的对服务器主动发起请求,也就是 Web 中常用的 poll 技术。这样的操作非常低效,这是因为每发起一次新的 HTTP 请求,就需要单独开启一个新的 TCP 链接,同时 HTTP 协议本身也是一种开销非常大的协议。为了解决这些问题,所以出现了 WebSocket 协议。WebSocket 使得浏览器和服务器之间能通过一个持久的 TCP 链接就能完成数据的双向通信。关于 WebSocket 的 RFC 提案,可以参看 RFC6455。
WebSocket 和 HTTP 协议一般情况下都工作在浏览器中,但 WebSocket 是一种完全不同于 HTTP 的协议。尽管,浏览器需要通过 HTTP 协议的 GET 请求,将 HTTP 协议升级为 WebSocket 协议。升级的过程被称为 握手(handshake)。当浏览器和服务器成功握手后,则可以开始根据 WebSocket 定义的通信帧格式开始通信了。像其他各种协议一样,WebSocket 协议的通信帧也分为控制数据帧和普通数据帧,前者用于控制 WebSocket 链接状态,后者用于承载数据。下面我们将一一分析 WebSocket 协议的握手过程以及通信帧格式。
WebSocket 协议的握手过程
握手的过程也就是将 HTTP 协议升级为 WebSocket 协议的过程。前面我们说过,握手开始首先由浏览器端发送一个 GET 请求开发,该请求的 HTTP 头部信息如下:
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 请求中,增加了一些新的字段,其作用如下所示:
- Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
- Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
- Sec-WebSocket-Key: 必需字段,一个随机的字符串;
- Sec-WebSocket-Version: 必需字段,代表了 WebSocket 协议版本,值必需是 13, 否则握手失败;
返回的响应中,如果握手成功会返回状态码为 101 的 HTTP 响应。同时其他字段说明如下:
- Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
- Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
- Sec-WebSocket-Accept: 规定必需的字段,该字段的值是通过固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11加上请求中Sec-WebSocket-Key字段的值,然后再对其结果通过 SHA1 哈希算法求出的结果。
当浏览器和服务器端成功握手后,就可以传送数据了,传送数据是按照 WebSocket 协议的数据格式生成的。
WebSocket 协议数据帧
数据帧的定义类似于 TCP/IP 协议的格式定义,具体看下图:
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 链接中接收到的数据帧,都要按照以上格式进行解析,这样才能知道该数据帧是用于控制的还是用于传送数据的。
OpenResty
2.1 resty.websocket 库
模块文档:
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
- new
- set_timeout
- send_text
- send_binary
- send_ping
- send_pong
- send_close
- send_frame
- recv_frame
2.2 resty.redis 模块
模块文档:
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
- subscribe 订阅频道
- publish 发布信息
- read_reply 接收信息
实现代码
- websocket.lua
-- 简易聊天室
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>
基于 OpenResty 实现一个 WS 聊天室的更多相关文章
- 基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍。最后我们将会实现一个基于Server-Sent Event和Flask简单的在线聊天室。
基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍.最后我们将会实现一个基于S ...
- 基于LINUX的多功能聊天室
原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...
- Python开发一个WEB聊天室
项目实战:开发一个WEB聊天室 功能需求: 用户可以与好友一对一聊天 可以搜索.添加某人为好友 用户可以搜索和添加群 每个群有管理员可以审批用户的加群请求,群管理员可以用多个,群管理员可以删除.添加. ...
- 基于EPOLL模型的局域网聊天室和Echo服务器
一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...
- FastAPI(56)- 使用 Websocket 打造一个迷你聊天室
背景 在实际项目中,可能会通过前端框架使用 WebSocket 和后端进行通信 这里就来详细讲解下 FastAPI 是如何操作 WebSocket 的 模拟 WebSocket 客户端 #!usr/b ...
- 基于Linux的TCP网络聊天室
1.实验项目名称:基于Linux的TCP网络聊天室 2.实验目的:通过TCP完成多用户群聊和私聊功能. 3.实验过程: 通过socket建立用户连接并传送用户输入的信息,分别来写客户端和服务器端,利用 ...
- 基于websocket实现的web聊天室
# -*- coding:utf-8 -*- import socket import base64 import hashlib def get_headers(data): "" ...
- JAVA基础知识之网络编程——-基于TCP通信的简单聊天室
下面将基于TCP协议用JAVA写一个非常简单的聊天室程序, 聊天室具有以下功能, 在服务器端,可以接受客户端注册(用户名),可以显示注册成功的账户 在客户端,可以注册一个账号,并用这个账号发送信息 发 ...
- 《基于Node.js实现简易聊天室系列之详细设计》
一个完整的项目基本分为三个部分:前端.后台和数据库.依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析.概要设计.详细设计.编码.测试等.由于缺乏相关知识的储备,导致这个Demo系列的文章层次 ...
随机推荐
- ubuntu 13.04添加 flash_plugin
mv libflashplayer.so /usr/lib/mozilla/plugins
- python异常处理及内置模块
异常处理 有时候我们在写程序的时候会出现错误或者异常,导致程序终止,如下这个例子: #!/usr/bin/env python a = 2/0 print(a) 结果提示如下错误: Traceback ...
- python 中的pipe
from multiprocessing import Process,Queue,Pipe import os def f(q): # q.send([42,None,'hello']) print ...
- Mac版Mysql Workbench不展示Schema
Mac版的Mysql Workbench会不展示Schema,如下图 操作如下 cd ~/Library/Application\ Support/MySQL/Workbench/ rm wb_sta ...
- AESUtil 加密
package com.hxqc.basic.dependency.util; import org.apache.commons.lang.StringUtils; import sun.misc. ...
- UVa 11346 - Probability(几何概型)
链接: https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...
- POJ3907 Build Your Home
嘟嘟嘟 题意:按逆时针或顺时针给出一个多边形,求面积. 解法:直接套用公式:\(S = \frac{1}{2}|\sum _ {i = 1} ^ {n} {v_i \times v_{i + 1}}| ...
- 随手练——O(n)解决无序数组排序后的相邻最大差值
题目从这儿看到的 : https://mp.weixin.qq.com/s/2OXg67MfBuQjDPAJxxD8rQ,但是公众号上讲错了,问题还挺严重的. 题目知识点:桶排序. 题目:有一个无序数 ...
- [教程] macOS打开原生的NTFS读写功能
Mac本身实际上是支持原生的NTFS读写的,只不过这一功能被隐藏了,但是可以手动打开,这比第三方的的工具要安全得多,有时第三方工具可能会发生整个NTFS分区数据丢失的情况,下面是打开MAC原生NTFS ...
- sql连接查询INNER JOIN,LEFT JOIN,RIGHT JOIN区别
测试表 课程表cource 教师表teacher 查询老师教的课程 1.inner join内链接 ,只显示完全匹配的查询结果 SELECT cource.cname,teacher.tname f ...