基于 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系列的文章层次 ...
随机推荐
- C# 方法与参数 常见命名空间汇总 using的使用 main方法参数
本文主要讲 C# 常见命名空间 using static 指令 && 调用静态方法 嵌套命名空间&&作用域 别名 Main() 方法 C# 常见命名空间 命名空间 作用 ...
- lambdas vs. method groups
Update: Due to a glitch in my code I miscalculated the difference. It has been updated. See full his ...
- U-Mail:邮件营销如何大量获取并筛选有效地址
工欲善其事必先利其器,在所有的邮件推广中,最基础的工作就是收集到足够多的有效邮箱地址,俗话说韩信将兵多多益善,邮箱地址越多,也就意味着潜在的转化为消费者的数量众多,但是如果大量滥竽充数的地址被搜罗进来 ...
- Angular2.0知识架构图
知识架构图:
- 【RabbitMQ】4、三种Exchange模式——订阅、路由、通配符模式
前两篇博客介绍了两种队列模式,这篇博客介绍订阅.路由和通配符模式,之所以放在一起介绍,是因为这三种模式都是用了Exchange交换机,消息没有直接发送到队列,而是发送到了交换机,经过队列绑定交换机到达 ...
- iOS js
[webView stringByEvaluatingJavaScriptFromString:@"document.getElementById(\"idNumber\" ...
- CSS(层叠样式表)基础知识
CSS 指层叠样式表 (Cascading Style Sheets).样式定义怎样显示 HTML 元素.它通常存储在样式表中,把样式加入到 HTML 4.0 中,解决内容与表现分离的问题. 当同一 ...
- luogu P1272 重建道路
嘟嘟嘟 这好像是一种树上背包. 我们令dp[i][j] 表示在 i 所在的子树中(包括节点 i)分离出一个大小为 j 的子树最少需割多少条边. 那么转移方程就是 dp[u][j] = min(dp[u ...
- 在Windows 10中更改网络连接优先级
查看接口列表 (也可使用 如下) 选择网络连接,然后单击右侧的箭头以更改网络连接优先级. 可以参考之前的部分 链接在此 更改单个wi-fi连接顺序可以使用如下
- linux内存管理--用户空间和内核空间
关于虚拟内存有三点需要注意: 4G的进程地址空间被人为的分为两个部分--用户空间与内核空间.用户空间从0到3G(0xc0000000),内核空间占据3G到4G.用户进程通常情况下只能访问用户空间的虚拟 ...