基于 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系列的文章层次 ...
随机推荐
- 宝塔面板 + Rancher + 阿里云镜像仓库 + Docker + Kubernetes,添加集群、部署 web 应用
目录 一,安装宝塔面板(V 6.8) 二,使用宝塔安装 Docker,配置阿里云容器服务 三,安装 Rancher (Server) 四,管理 Rancher.添加集群 五,添加 Rancher 应用 ...
- Azure 负载均衡器的多个 VIP
您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn. Azure 负载均衡器 ...
- Java面试基本知识
Java基本知识 基本知识 服务器:Tomcat 支持Servlet jsp JBoss 开源应用服务器 Apache:最广泛的http服务器,只支持静态网页 String是长度不可变,用+=的时候会 ...
- MySQL 事务没有提交导致 锁等待 Lock wait timeout exceeded
java.lang.Exception: ### Error updating database. Cause: java.sql.SQLException: Lock wait timeout e ...
- hdu 1874 Dijkstra算法
先贴个网上找的比较通俗易懂的教程: 2.1Dijkstra算法(非负权,使用于有向图和无向图) Dijkstra算法是典型最短路算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心 ...
- Vue实现购物小球抛物线
.shop{ position: fixed; top: 300px; left: 40px; } .ball{ position: fixed; left: 32px; bottom: 22px; ...
- 关于SpringMVC整合freemarker报错问题
错误信息: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'FreeMa ...
- Spring(五)之Bean定义继承和依赖注入
一.Bean定义继承 bean定义可以包含许多配置信息,包括构造函数参数,属性值和特定于容器的信息,例如初始化方法,静态工厂方法名称等. 子bean定义从父定义继承配置数据.子定义可以根据需要覆盖某些 ...
- 链表推导式 【list comprehension】
x for x in x 链表推导式 [list comprehension]链表推导式提供了一个创建链表的简单途径,无需使用 map(), filter() 以及 lambda.返回链表的定义通常要 ...
- JDK(一)JDK集合框架
JDK中的集合框架分为两大类:Collection和Map.Collection以一组Object的形式保存元素,Map以Key-Value对的形式保存元素. 上图列出的类并不完整,只列举了平时比较常 ...