基于 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 库

模块文档:

https://github.com/openresty/lua-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

  1. new
  2. set_timeout
  3. send_text
  4. send_binary
  5. send_ping
  6. send_pong
  7. send_close
  8. send_frame
  9. recv_frame

2.2 resty.redis 模块

模块文档:

https://github.com/openresty/lua-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

  1. subscribe 订阅频道
  2. publish 发布信息
  3. read_reply 接收信息

实现代码

  1. 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)
  1. 前端页面
<!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> &nbsp;
<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 聊天室的更多相关文章

  1. 基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍。最后我们将会实现一个基于Server-Sent Event和Flask简单的在线聊天室。

    基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍.最后我们将会实现一个基于S ...

  2. 基于LINUX的多功能聊天室

    原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...

  3. Python开发一个WEB聊天室

    项目实战:开发一个WEB聊天室 功能需求: 用户可以与好友一对一聊天 可以搜索.添加某人为好友 用户可以搜索和添加群 每个群有管理员可以审批用户的加群请求,群管理员可以用多个,群管理员可以删除.添加. ...

  4. 基于EPOLL模型的局域网聊天室和Echo服务器

    一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...

  5. FastAPI(56)- 使用 Websocket 打造一个迷你聊天室

    背景 在实际项目中,可能会通过前端框架使用 WebSocket 和后端进行通信 这里就来详细讲解下 FastAPI 是如何操作 WebSocket 的 模拟 WebSocket 客户端 #!usr/b ...

  6. 基于Linux的TCP网络聊天室

    1.实验项目名称:基于Linux的TCP网络聊天室 2.实验目的:通过TCP完成多用户群聊和私聊功能. 3.实验过程: 通过socket建立用户连接并传送用户输入的信息,分别来写客户端和服务器端,利用 ...

  7. 基于websocket实现的web聊天室

    # -*- coding:utf-8 -*- import socket import base64 import hashlib def get_headers(data): "" ...

  8. JAVA基础知识之网络编程——-基于TCP通信的简单聊天室

    下面将基于TCP协议用JAVA写一个非常简单的聊天室程序, 聊天室具有以下功能, 在服务器端,可以接受客户端注册(用户名),可以显示注册成功的账户 在客户端,可以注册一个账号,并用这个账号发送信息 发 ...

  9. 《基于Node.js实现简易聊天室系列之详细设计》

    一个完整的项目基本分为三个部分:前端.后台和数据库.依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析.概要设计.详细设计.编码.测试等.由于缺乏相关知识的储备,导致这个Demo系列的文章层次 ...

随机推荐

  1. Asp.net core 项目实战 新闻网站+后台 源码、设计原理 、视频教程

    首先说明,视频教程.源码并非本人原创 本人将项目分割开,并写了一些说明. 该视频教程 地址  https://study.163.com/course/courseMain.htm?courseId= ...

  2. MapReduce Design Patterns(chapter 2(part 1))(二)

    随着每天都有更多的数据加载进系统,数据量变得很庞大.这一章专注于对你的数据顶层的,概括性意见的设计模式,从而使你能扩展思路,但可能对局部数据是不适用的.概括性的分析都是关于对相似数据的分组和执行统计运 ...

  3. 深入浅出SharePoint2010——请假系统无代码篇之权限设计

    首选我们需要区分3个跟权限相关的概念. 权限项目(Permission):最小的权限粒度.比如创建列表项.审批等. 权限级别(Permission Level):权限项目不能直接赋予用户或者用户组,只 ...

  4. [EffectiveC++]导读 default构造函数

    class B { public: explicit B(int x = 0,bool b = true); //default构造函数 }; explicit可以阻止用来执行隐式类型转换,但是可以用 ...

  5. 认识 Java(配置环境变量)

    1. Java 简介 Java由Sun Microsystems公司于1995年5月推出,是一种面向对象的编程语言.在2009年4月20号,ORACLE (甲骨文)收购了 Sun 公司,也就是说 Ja ...

  6. Java集合框架中的快速失败(fail—fast)机制

      fail-fast机制,即快速失败机制,是java集合框架中的一种错误检测机制.多线程下用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加.删除),则会抛出Concurre ...

  7. leetcode231 2的幂 leetcode342 4的幂 leetcode326 3的幂

    1.2的幂 正确写法: class Solution { public: bool isPowerOfTwo(int n) { ) return false; )) == ; } }; 错误写法1: ...

  8. jQuery Mobile中jQuery.mobile.changePage方法使用详解

    jQuery.mobile.changePage方法用的还是很多的.作为一个老手,有必要对jQuery mobile中实用方法做一些总结.系列文章请看jQuery Mobile专栏.jquery.mo ...

  9. 算法——(4)哈希、hashmap、hashtable

    1. Hash 把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值.拥有四个特性: 1. 拥有无限的输入域和固定大小的输出域 2. 如果输入值相同,返回值一样 3. 如果输入值不相同 ...

  10. Spring @Value 默认值

    @Value(value = "${etl.maxthreadcount:3}") private long MaxThreadCount;