今天介绍如何用 Go
语言创建 WebSocket
服务,文章的前两部分简要介绍了 WebSocket
协议以及用 Go
标准库如何创建 WebSocket
服务。第三部分实践环节我们使用了 gorilla/websocket
库帮助我们快速构建 WebSocket
服务,它帮封装了使用 Go
标准库实现 WebSocket
服务相关的基础逻辑,让我们能从繁琐的底层代码中解脱出来,根据业务需求快速构建 WebSocket
服务。
Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复 gohttp10
获取本文源代码
WebSocket介绍
WebSocket
通信协议通过单个 TCP
连接提供全双工通信通道。与 HTTP
相比, WebSocket
不需要你为了获得响应而发送请求。它允许双向数据流,因此您只需等待服务器发送的消息即可。当 Websocket
可用时,它将向您发送一条消息。 对于需要连续数据交换的服务(例如即时通讯程序,在线游戏和实时交易系统), WebSocket
是一个很好的解决方案。 WebSocket
连接由浏览器请求,并由服务器响应,然后建立连接,此过程通常称为握手。 WebSocket
中的特殊标头仅需要浏览器与服务器之间的一次握手即可建立连接,该连接将在其整个生命周期内保持活动状态。 WebSocket
解决了许多实时 Web
开发的难题,并且与传统的 HTTP
相比,具有许多优点:
- 轻量级报头减少了数据传输开销。
- 单个Web客户端仅需要一个TCP连接。
- WebSocket服务器可以将数据推送到Web客户端。
WebSocket协议实现起来相对简单。它使用 HTTP
协议进行初始握手。握手成功后即建立连接, WebSocket
实质上使用原始 TCP
读取/写入数据。
客户端请求如下所示:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
这是服务器响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
如何在Go中创建WebSocket应用
要基于Go 语言内置的 net/http
库编写 WebSocket
服务器,你需要:
- 发起握手
- 从客户端接收数据帧
- 发送数据帧给客户端
- 关闭握手
发起握手
首先,让我们创建一个带有 WebSocket
端点的 HTTP
处理程序:
// HTTP server with WebSocket endpoint func Server() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ws, err := NewHandler(w, r) if err != nil { // handle error } if err = ws.Handshake(); err != nil { // handle error } …
然后初始化 WebSocket
结构。
初始握手请求始终来自客户端。服务器确定了 WebSocket
请求后,需要使用握手响应进行回复。
请记住,你无法使用 http.ResponseWriter
编写响应,因为一旦开始发送响应,它将关闭其基础的 TCP
连接(这是 HTTP
协议的运行机制决定的,发送响应后即关闭连接)。
因此,您需要使用 HTTP
劫持( hijack
)。通过劫持,可以接管基础的 TCP
连接处理程序和 bufio.Writer
。这使可以在不关闭 TCP
连接的情况下读取和写入数据。
// NewHandler initializes a new handler func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) { hj, ok := w.(http.Hijacker) if !ok { // handle error } ..... }
要完成握手,服务器必须使用适当的头进行响应。
// Handshake creates a handshake header func (ws *WS) Handshake() error { hash := func(key string) string { h := sha1.New() h.Write([]byte(key)) h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) return base64.StdEncoding.EncodeToString(h.Sum(nil)) }(ws.header.Get("Sec-WebSocket-Key")) ..... }
客户端发起 WebSocket
连接请求时用的 Sec-WebSocket-key
是随机生成的,并且是Base64编码的。接受请求后,服务器需要将此密钥附加到固定字符串。假设秘钥是 x3JJHMbDL1EzLkh9GBhXDw==
。在这个例子中,可以使用 SHA-1
计算二进制值,并使用 Base64
对其进行编码。得到 HSmrc0sMlYUkAGmm5OPpG2HaGWk=
。然后使用它作为 Sec-WebSocket-Accept
响应头的值。
传输数据帧
握手成功完成后,您的应用程序可以从客户端读取数据或向客户端写入数据。WebSocket规范 定义了的一个客户机和服务器之间使用的特定帧格式。这是框架的位模式:
图:传输数据帧的位模式
使用以下代码对客户端有效负载进行解码:
// Recv receives data and returns a Frame func (ws *WS) Recv() (frame Frame, _ error) { frame = Frame{} head, err := ws.read(2) if err != nil { // handle error }
反过来,这些代码行允许对数据进行编码:
// Send sends a Frame func (ws *WS) Send(fr Frame) error { // make a slice of bytes of length 2 data := make([]byte, 2) // Save fragmentation & opcode information in the first byte data[0] = 0x80 | fr.Opcode if fr.IsFragment { data[0] &= 0x7F } .....
关闭握手
当各方之一发送状态为关闭的关闭帧作为有效负载时,握手将关闭。可选的,发送关闭帧的一方可以在有效载荷中发送关闭原因。如果关闭是由客户端发起的,则服务器应发送相应的关闭帧作为响应。
// Close sends a close frame and closes the TCP connection func (ws *Ws) Close() error { f := Frame{} f.Opcode = 8 f.Length = 2 f.Payload = make([]byte, 2) binary.BigEndian.PutUint16(f.Payload, ws.status) if err := ws.Send(f); err != nil { return err } return ws.conn.Close() }
使用第三方库快速构建WebSocket服务
通过上面的章节可以看到用 Go
自带的 net/http
库实现 WebSocket
服务还是太复杂了。好在有很多对 WebSocket
支持良好的第三方库,能减少我们很多底层的编码工作。这里我们使用 gorilla web toolkit
家族的另外一个库 gorilla/websocket
来实现我们的 WebSocket
服务,构建一个简单的 Echo
服务( echo
意思是回音,就是客户端发什么,服务端再把消息发回给客户端)。
我们在 http_demo
项目的 handler
目录下新建一个 ws
子目录用来存放 WebSocket
服务相关的路由对应的请求处理程序。
增加两个路由:
/ws/echo
echo
应用的WebSocket 服务的路由/ws/echo_display
echo
应用的客户端页面的路由。 创建WebSocket服务端
// handler/ws/echo.go package ws import ( "fmt" "github.com/gorilla/websocket" "net/http" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func EchoMessage(w http.ResponseWriter, r *http.Request) { conn, _ := upgrader.Upgrade(w, r, nil) // 实际应用时记得做错误处理 for { // 读取客户端的消息 msgType, msg, err := conn.ReadMessage() if err != nil { return } // 把消息打印到标准输出 fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg)) // 把消息写回客户端,完成回音 if err = conn.WriteMessage(msgType, msg); err != nil { return } } }
conn
变量的类型是*websocket.Conn
,websocket.Conn
类型用来表示WebSocket
连接。服务器应用程序从HTTP
请求处理程序调用Upgrader.Upgrade
方法以获取*websocket.Conn
- 调用连接的
WriteMessage
和ReadMessage
方法发送和接收消息。上面的msg
接收到后在下面又回传给了客户端。msg
的类型是[]byte
。
创建WebSocket客户端
前端页面路由对应的请求处理程序如下,直接返回 views/websockets.html
给到浏览器渲染页面即可。
// handler/ws/echo_display.go package ws import "net/http" func DisplayEcho(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "views/websockets.html") }
websocket.html
里我们需要用 JavaScript
连接 WebScoket
服务进行收发消息,篇幅原因我就只贴 JS
代码了
<form> <input id="input" type="text" /> <button onclick="send()">Send</button> <pre id="output"></pre> </form> ... <script> var input = document.getElementById("input"); var output = document.getElementById("output"); var socket = new WebSocket("ws://localhost:8000/ws/echo"); socket.onopen = function () { output.innerHTML += "Status: Connected\n"; }; socket.onmessage = function (e) { output.innerHTML += "Server: " + e.data + "\n"; }; function send() { socket.send(input.value); input.value = ""; } </script> ...
注册路由
服务端和客户端的程序都准备好后,我们按照之前约定好的路径为他们注册路由和对应的请求处理程序:
// router/router.go func RegisterRoutes(r *mux.Router) { ... wsRouter := r.PathPrefix("/ws").Subrouter() wsRouter.HandleFunc("/echo", ws.EchoMessage) wsRouter.HandleFunc("/echo_display", ws.DisplayEcho) }
测试验证
重启服务后访问 http://localhost:8000/ws/echo_display
,在输入框中输入任何消息都能再次回显到浏览器中。
服务端则是把收到的消息打印到终端中然后把调用 writeMessage
把消息再回传给客户端,可以在终端中查看到记录。
WebSocket
在现在更新频繁的应用中使用非常广泛,进行 WebSocket
编程也是我们需要掌握的一项必备技能。文章的实践练习稍微简单了一些,也没有做错误和安全性检查。主要是为了讲清楚大概的流程。关于 gorilla/websocket
更多的细节在使用时还需要查看官方文档才行。
参考链接:
https://yalantis.com/blog/how-to-build-websockets-in-go/
https://www.gorillatoolkit.org/pkg/websocket
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
RTX 5090要首发 性能要翻倍!三星展示GDDR7显存
三星在GTC上展示了专为下一代游戏GPU设计的GDDR7内存。
首次推出的GDDR7内存模块密度为16GB,每个模块容量为2GB。其速度预设为32 Gbps(PAM3),但也可以降至28 Gbps,以提高产量和初始阶段的整体性能和成本效益。
据三星表示,GDDR7内存的能效将提高20%,同时工作电压仅为1.1V,低于标准的1.2V。通过采用更新的封装材料和优化的电路设计,使得在高速运行时的发热量降低,GDDR7的热阻比GDDR6降低了70%。
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]