玩转 Go 生态|Hertz WebSocket 扩展简析
WebSocket 是一种可以在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
Hertz 提供了 WebSocket 的支持,参考 gorilla/websocket 库使用 hijack 的方式在 Hertz 进行了适配,用法和参数基本保持一致。
安装
go get github.com/hertz-contrib/websocket
示例代码
package main
import (
"context"
"flag"
"html/template"
"log"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/hertz-contrib/websocket"
)
var addr = flag.String("addr", "localhost:8080", "http service address")
var upgrader = websocket.HertzUpgrader{} // use default options
func echo(_ context.Context, c *app.RequestContext) {
err := upgrader.Upgrade(c, func(conn *websocket.Conn) {
for {
mt, message, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
err = conn.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
})
if err != nil {
log.Print("upgrade:", err)
return
}
}
func home(_ context.Context, c *app.RequestContext) {
c.SetContentType("text/html; charset=utf-8")
homeTemplate.Execute(c, "ws://"+string(c.Host())+"/echo")
}
func main() {
flag.Parse()
h := server.Default(server.WithHostPorts(*addr))
// https://github.com/cloudwego/hertz/issues/121
h.NoHijackConnPool = true
h.GET("/", home)
h.GET("/echo", echo)
h.Spin()
}
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
window.addEventListener("load", function(evt) {
var output = document.getElementById("output");
var input = document.getElementById("input");
var ws;
var print = function(message) {
var d = document.createElement("div");
d.textContent = message;
output.appendChild(d);
output.scroll(0, output.scrollHeight);
};
document.getElementById("open").onclick = function(evt) {
if (ws) {
return false;
}
ws = new WebSocket("{{.}}");
ws.onopen = function(evt) {
print("OPEN");
}
ws.onclose = function(evt) {
print("CLOSE");
ws = null;
}
ws.onmessage = function(evt) {
print("RESPONSE: " + evt.data);
}
ws.onerror = function(evt) {
print("ERROR: " + evt.data);
}
return false;
};
document.getElementById("send").onclick = function(evt) {
if (!ws) {
return false;
}
print("SEND: " + input.value);
ws.send(input.value);
return false;
};
document.getElementById("close").onclick = function(evt) {
if (!ws) {
return false;
}
ws.close();
return false;
};
});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server,
"Send" to send a message to the server and "Close" to close the connection.
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output" style="max-height: 70vh;overflow-y: scroll;"></div>
</td></tr></table>
</body>
</html>
`))
运行 server:
go run server.go
上述示例代码中,服务器包括一个简单的网络客户端。要使用该客户端,在浏览器中打开 http://127.0.0.1:8080,并按照页面上的指示操作。
Upgrade
websocket.Conn 类型代表一个 WebSocket 连接。服务器应用程序从 HTTP 请求处理程序中调用 HertzUpgrader.Upgrade 方法,将 HTTP 协议的连接请求升级为 WebSocket 协议的连接请求。
这部分逻辑对应着示例代码的 echo() 函数,此处着重介绍 HertzUpgrader.Upgrade。
函数签名:
func (u *HertzUpgrader) Upgrade(ctx *app.RequestContext, handler HertzHandler) error
内部处理逻辑:
func (u *HertzUpgrader) Upgrade(ctx *app.RequestContext, handler HertzHandler) error {
if !ctx.IsGet() {
return u.returnError(ctx, consts.StatusMethodNotAllowed, fmt.Sprintf("%s request method is not GET", badHandshake))
}
// 校验 requsetHeader 中与 websocket 相关的字段(此处省略部分逻辑代码)
subprotocol := u.selectSubprotocol(ctx)
compress := u.isCompressionEnable(ctx)
ctx.SetStatusCode(consts.StatusSwitchingProtocols)
// 构造协议升级后的响应头部信息
ctx.Response.Header.Set("Upgrade", "websocket")
ctx.Response.Header.Set("Connection", "Upgrade")
ctx.Response.Header.Set("Sec-WebSocket-Accept", computeAcceptKeyBytes(challengeKey))
// “无上下文接管”模式
if compress {
ctx.Response.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
}
if subprotocol != nil {
ctx.Response.Header.SetBytesV("Sec-WebSocket-Protocol", subprotocol)
}
// 通过 Hijack 的方式,实现 websocket 全双工的通信
ctx.Hijack(func(netConn network.Conn) {
writeBuf := poolWriteBuffer.Get().([]byte)
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, nil, writeBuf)
if subprotocol != nil {
c.subprotocol = b2s(subprotocol)
}
if compress {
c.newCompressionWriter = compressNoContextTakeover
c.newDecompressionReader = decompressNoContextTakeover
}
netConn.SetDeadline(time.Time{})
handler(c)
writeBuf = writeBuf[0:0]
poolWriteBuffer.Put(writeBuf)
})
return nil
}
HertzHandler
HertzHandler 是上述 HertzUpgrader.Upgrade 函数的第二个参数。HertzHandler 在握手完成后接收一个 websocket 连接,通过劫持这个连接,完成全双工的通信。
HertzHandler 必须由用户提供,内部定义了 WebSocket 请求和响应的具体流程。
函数签名:
type HertzHandler func(*Conn)
上述 echo 服务器的 websocket 处理流程:
err := upgrader.Upgrade(c, func(conn *websocket.Conn) {
for {
// 读取客户端发送的信息
mt, message, err := conn.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
// 向客户端发送信息
err = conn.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
})
配置
上述文档已经讲述了Hertz WebSocket 最核心的协议升级与连接劫持的逻辑,下面将罗列 Hertz WebSocket 使用过程中可选的配置参数。
这部分将围绕 websocket.HertzUpgrader 结构展开说明。
| 参数 | 介绍 |
|---|---|
ReadBufferSize |
用于设置输入缓冲区的大小,单位为字节。如果缓冲区大小为零,那么就使用 HTTP 服务器分配的大小。输入缓冲区大小并不限制可以接收的信息的大小。 |
WriteBufferSize |
用于设置输出缓冲区的大小,单位为字节。如果缓冲区大小为零,那么就使用 HTTP 服务器分配的大小。输出缓冲区大小并不限制可以发送的信息的大小。 |
WriteBufferPool |
用于设置写操作的缓冲池。 |
Subprotocols |
用于按优先顺序设置服务器支持的协议。如果这个字段不是 nil,那么 Upgrade 方法通过选择这个列表中与客户端请求的协议的第一个匹配来协商一个子协议。如果没有匹配,那么就不协商协议(Sec-Websocket-Protocol 头不包括在握手响应中)。 |
Error |
用于设置生成 HTTP 错误响应的函数。 |
CheckOrigin |
用于设置针对请求的 Origin 头的校验函数, 如果请求的 Origin 头是可接受的,CheckOrigin 返回 true。 |
EnableCompression |
用于设置服务器是否应该尝试协商每个消息的压缩(RFC 7692)。将此值设置为 true 并不能保证压缩会被支持。 |
WriteBufferPool
如果该值没有被设置,则额外初始化写缓冲区,并在当前生命周期内分配给该连接。当应用程序在大量的连接上有适度的写入量时,缓冲池是最有用的。
应用程序应该使用一个单一的缓冲池来为不同的连接分配缓冲区。
接口签名:
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
// interface. The type of the value stored in a pool is not specified.
type BufferPool interface {
// Get gets a value from the pool or returns nil if the pool is empty.
Get() interface{}
// Put adds a value to the pool.
Put(interface{})
}
示例代码:
type simpleBufferPool struct {
v interface{}
}
func (p *simpleBufferPool) Get() interface{} {
v := p.v
p.v = nil
return v
}
func (p *simpleBufferPool) Put(v interface{}) {
p.v = v
}
var upgrader = websocket.HertzUpgrader{
WriteBufferPool: &simpleBufferPool{},
}
Subprotocols
WebSocket 只是定义了一种交换任意消息的机制。这些消息是什么意思,客户端在任何特定的时间点可以期待什么样的消息,或者他们被允许发送什么样的消息,完全取决于实现应用程序。
所以你需要在服务器和客户端之间就这些事情达成协议。子协议参数只是让客户端和服务端正式地交换这些信息。你可以为你想要的任何协议编造任何名字。服务器可以简单地检查客户在握手过程中是否遵守了该协议。
Error
如果 Error 为 nil,则使用 Hertz 提供的 API 来生成 HTTP 错误响应。
函数签名:
func(ctx *app.RequestContext, status int, reason error)
示例代码:
var upgrader = websocket.HertzUpgrader{
Error: func(ctx *app.RequestContext, status int, reason error) {
ctx.Response.Header.Set("Sec-Websocket-Version", "13")
ctx.AbortWithMsg(reason.Error(), status)
},
}
CheckOrigin
如果 CheckOrigin 为nil,则使用一个安全的默认值:如果Origin请求头存在,并且源主机不等于请求主机头,则返回false。CheckOrigin 函数应该仔细验证请求的来源,以防止跨站请求伪造。
函数签名:
func(ctx *app.RequestContext) bool
默认实现:
func fastHTTPCheckSameOrigin(ctx *app.RequestContext) bool {
origin := ctx.Request.Header.Peek("Origin")
if len(origin) == 0 {
return true
}
u, err := url.Parse(b2s(origin))
if err != nil {
return false
}
return equalASCIIFold(u.Host, b2s(ctx.Host()))
}
EnableCompression
服务端接受一个或者多个扩展字段,这些扩展字段是包含客户端请求的 Sec-WebSocket-Extensions 头字段扩展中的。当 EnableCompression 为 true 时,服务端根据当前自身支持的扩展与其进行匹配,如果匹配成功则支持压缩。
校验逻辑:
var strPermessageDeflate = []byte("permessage-deflate")
func (u *HertzUpgrader) isCompressionEnable(ctx *app.RequestContext) bool {
extensions := parseDataHeader(ctx.Request.Header.Peek("Sec-WebSocket-Extensions"))
// Negotiate PMCE
if u.EnableCompression {
for _, ext := range extensions {
if bytes.HasPrefix(ext, strPermessageDeflate) {
return true
}
}
}
return false
}
目前仅支持“无上下文接管”模式,详见上述 HertzUpgrader.Upgrade 代码部分。
Set Deadline
当使用 websocket 进行读写的时候,可以通过类似如下方式设置超时时间(在每次读写过程中都会生效)。
示例代码:
func echo(_ context.Context, c *app.RequestContext) {
err := upgrader.Upgrade(c, func(conn *websocket.Conn) {
defer conn.Close()
// "github.com/cloudwego/hertz/pkg/network"
conn.NetConn().(network.Conn).SetReadTimeout(1 * time.Second)
...
})
if err != nil {
log.Print("upgrade:", err)
return
}
}
更多用法示例详见 examples 。
玩转 Go 生态|Hertz WebSocket 扩展简析的更多相关文章
- Websocket通讯简析
什么是Websocket Websocket是一种全新的协议,不属于HTTP无状态协议,协议名为"ws",这意味着一个Websocket连接地址会是这样的写法:ws://**.We ...
- DiskGenius注册算法简析
初次接触DiskGenius已经成为遥远的记忆,那个时候还只有DOS版本.后来到Windows版,用它来处理过几个找回丢失分区的案例,方便实用.到现在它的功能越来越强大,成为喜好启动技术和桌面支持人员 ...
- 简析.NET Core 以及与 .NET Framework的关系
简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...
- AFNetworking封装思路简析
http://blog.csdn.net/qq_34101611/article/details/51698473 一.AFNetworking的发展 1. AFN 1.0版本 AFN 的基础部分是 ...
- [转载] Thrift原理简析(JAVA)
转载自http://shift-alt-ctrl.iteye.com/blog/1987416 Apache Thrift是一个跨语言的服务框架,本质上为RPC,同时具有序列化.发序列化机制:当我们开 ...
- Linux 磁盘分区方案简析
Linux 磁盘分区方案简析 by:授客 QQ:1033553122 磁盘分区 任何硬盘在使用前都要进行分区.硬盘的分区有两种类型:主分区和扩展分区.一个硬盘上最多只能有4个主分区,其中一个主分区 ...
- [转帖]简析数据中心三大Overlay技术
简析数据中心三大Overlay技术 http://www.jifang360.com/news/20161010/n225987768.html 搭建大规模的云计算环境需要数据中心突破多种技术难题,其 ...
- 0002 - Spring MVC 拦截器源码简析:拦截器加载与执行
1.概述 Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理.例如通过拦截器可以进行权限验证.记录请求信息的日 ...
- SIFT特征原理简析(HELU版)
SIFT(Scale-Invariant Feature Transform)是一种具有尺度不变性和光照不变性的特征描述子,也同时是一套特征提取的理论,首次由D. G. Lowe于2004年以< ...
- 3D文件压缩库——Draco简析
3D文件压缩库——Draco简析 今年1月份时,google发布了名为“Draco”的3D图形开源压缩库,下载了其代码来看了下,感觉虽然暂时用不到,但还是有前途的,故简单做下分析. 注:Draco 代 ...
随机推荐
- Kibana:如何周期性地为 Dashboard 生成 PDF Report
转载自:https://blog.csdn.net/UbuntuTouch/article/details/108449775 按照上面的方式填写.记得把之前的 URL 拷贝到 webhook 下的 ...
- Git Review + Gerrit 安装及使用完成 Code-Review
转载自:https://cloud.tencent.com/developer/article/1010615 1.Code Review 介绍 Code Review 代码评审是指在软件开发过程中, ...
- 改善C#程序的方法-1 操作字符串
正确操作字符串 引言: 字符串是使用很频繁的一种数据类型. 如果使用不慎,则会为一次字符串操作所带来的额外性能开销而付出代价. 下面从这几个方面来探讨如何正确操作字符串: 1.确保尽量少的装箱,尽可能 ...
- GC plan_phase二叉树挂接的一个算法
楔子 在看GC垃圾回收plan_phase的时候,发现了一段特殊的代码,仔细研究下得知,获取当前数字bit位里面为1的个数. 通过这个bit位为1的个数(count),来确定挂接当前二叉树子节点的一个 ...
- emqx启用JWT令牌认证(包含hmac-based和public-key)
emqx连接启用jwt令牌认证 jwt令牌 概述 JWT 即 JSON Web Tokens 是一种开放的,用于在两方之间安全地表示声明的行业标准的方法(RFC 7519). 组成 令牌的形式 xxx ...
- 2022.2.26A组总结&反思
今天的发挥比较奇妙.. T1:一眼dp+高斯消元,但是感觉细节比较多,然后先去做了T2,写完后回来推了一下就做出来了.比较裸,但是细节确实多,但是很可惜的一点是最后提交的代码没有判不合法,到手的100 ...
- String简介
String:字符串,使用一对""引起来表示. 1.String声明为final的,不可被继承 2.String实现了Serializable接口:表示字符串是支持序列化的.实现了 ...
- 知识图谱-生物信息学-医学顶刊论文(Bioinformatics-2021)-MSTE: 基于多向语义关系的有效KGE用于多药副作用预测
MSTE: 基于多向语义关系的有效KGE用于多药副作用预测 论文标题: Effective knowledge graph embeddings based on multidirectional s ...
- Codeforces Round #829 (Div. 2)/CodeForces1754
CodeForces1754 注:所有代码均为场上所书 Technical Support 解析: 题目大意 给定一个只包含大写字母 \(\texttt{Q}\) 和 \(\texttt{A}\) 的 ...
- Linux的挖矿木马病毒清除(kswapd0进程)
1.top查看资源使用情况 看到这些进程一直在变化,但是,主要是由于kswapd0进程在作怪,占据了99%以上的CUP,查找资料后,发现它就是挖矿进程 2.排查kswapd0进程 执行命令netsta ...