0%

Golang 搭建 WebSocket 应用(一) - 初识 gorilla/websocket

在本系列文章中,将会使用在 Go 中一个用得比较多的 WebSocket 实现 gorilla/websocket

背景知识 - HTTP 与 WebSocket 的关系

本文会涉及到一些原理讲解,其中比较关键的一个是 HTTP 与 WebSocket 的联系与区别,了解这个可以帮助我们更好地使用 WebSocket

如果我们此前已经使用过 WebSocket,比如在 nginx 配置过 WebSocket,我们就会发现:

  1. 有个类似 upgrade 的关键字。这个关键字体现了 HTTP 与 WebSocket 的本质区别。
  2. 在 nginx 里配置,意味着 WebSocket 本质上也是通过 HTTP 协议来工作的。

我们知道,HTTP 的请求会在请求结束之后断开 TCP 连接,但 WebSocket 不一样,它在建立连接之后会一直维持着连接状态, 这样客户端与服务端就可以一直维持通信状态了。

WebSocket 建立连接的过程

在 WebSocket 协议中,初始的握手阶段使用标准的 HTTP 请求和响应:

  1. 客户端先发送一个 HTTP 请求,请求升级到 WebSocket 协议。
  2. 服务器在收到这个请求后,如果同意升级到 WebSocket,就会返回一个状态码为 101 的 HTTP 响应,指示升级成功,然后不会断开 TCP 连接。

这个过程涉及到的 HTTP 头部字段是 UpgradeConnection,具体而言,HTTP 请求头部可能包含类似以下的字段:

请求:

1
2
3
4
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade

响应:

1
2
3
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

也就是说,我们所看到的 Upgrade 实际上是把一个 HTTP 连接升级为了 WebSocket 连接,这个连接可以实现双向的通信。

这使得它非常适合实时通信的应用,例如聊天应用、在线游戏等。

gorilla/websocket 中的基本概念

WebSocket 连接 - Conn

gorilla/websocket 中使用 Conn 来表示一个 WebSocket 连接,它主要有如下作用:

  • 发送消息给客户端:Write* 方法,如 WriteJSON 发送 JSON 类型消息,又或者 WriteMessage 可以发送普通的文本消息。
  • 接收客户端发送的消息:Read* 方法,如 ReadJSONReadMessage
  • 其他功能:关闭连接、获取客户端 IP 地址等

消息

gorilla/websocket 中,消息被分为以下几种:

  • 数据消息:
    • TextMessage 文本消息:文本消息被解析为 UTF-8 编码的文本。需要应用程序来确保文本消息是有效的 UTF-8 编码文本。
    • BinaryMessage 二进制消息:二进制消息的解析留给应用程序。
  • 控制消息:可以调用 Conn 中的 WriteControlWriteMessageNextWriter 方法,将控制消息发送给对方。
    • CloseMessage 关闭连接的消息
    • PingMessage ping 消息
    • PongMessage pong 消息

注意:应用程序需要先读取连接中的消息才能处理从对等方发送的 closepingpong 消息。如果应用程序对来自对等方的消息不感兴趣, 则应用程序应启动一个 goroutine 来读取和丢弃来自对等方的消息。

并发

虽然 Golang 中有 goroutine 可以支持我们做并发操作,但是在 gorilla/websocket 中, 一个 WebSocket 连接只支持一个并发 reader 和一个并发 writer

我们的应用程序应该确保不超过一个 goroutine 同时调用写入方法(WriteMessageWriteJSON)或者读取方法(ReadMessageReadJSON)。

CloseWriteControl 方法可以与其他所有方法同时调用。

安全性

我们知道,在一般的 web 应用中,经常需要处理跨域的问题,同样的,在 gorilla/websocket 中也需要做一定的配置。

我们可以在 Upgrader 中的 CheckOrigin 字段中指定函数的 Origin 检查策略,如果 CheckOrigin 函数返回 false,则 Upgrader 方法将拒绝建立 WebSocket 连接,如果允许所有来源的连接,我们可以直接返回 true 即可。

1
2
3
4
5
6
7
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

缓冲

缓冲在 io 类操作中是一个很常见的术语,在 gorilla/websocket 中我们可以通过上面那段代码的 ReadBufferSizeWriteBufferSize 来指定连接的缓冲大小,以减少读取或写入消息时的系统调用次数。

默认大小为 4096,建议限制为最大预期消息的大小,大于最大消息最大大小的缓冲区不会带来任何好处。

Hello World

最后,让我们通过一个简单的 Hello World 程序来结束本文:

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"github.com/gorilla/websocket"
"log"
"net/http"
)

var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}

func handler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}

conn.WriteMessage(websocket.TextMessage, []byte("Hello, World!"))
conn.Close()
}

func main() {
http.HandleFunc("/ws", handler)
http.ListenAndServe(":8181", nil)
}

执行 go run main.go 启动 WebSocket 服务端,然后,我们打开一个浏览器的控制台, 在里面执行下面的 JavaScript 代码:

1
let ws = new WebSocket('ws://127.0.0.1:8181/ws')

不出意外的话,我们可以在浏览器控制台的 Network -> WS 中看到由服务端发送的 Hello, World!