在本系列文章中,将会使用在 Go 中一个用得比较多的 WebSocket
实现 gorilla/websocket。
背景知识 - HTTP 与 WebSocket 的关系
本文会涉及到一些原理讲解,其中比较关键的一个是 HTTP 与 WebSocket
的联系与区别,了解这个可以帮助我们更好地使用 WebSocket
。
如果我们此前已经使用过 WebSocket
,比如在 nginx 配置过 WebSocket
,我们就会发现:
- 有个类似
upgrade
的关键字。这个关键字体现了 HTTP 与WebSocket
的本质区别。 - 在 nginx 里配置,意味着
WebSocket
本质上也是通过 HTTP 协议来工作的。
我们知道,HTTP 的请求会在请求结束之后断开 TCP
连接,但 WebSocket
不一样,它在建立连接之后会一直维持着连接状态, 这样客户端与服务端就可以一直维持通信状态了。
WebSocket 建立连接的过程
在 WebSocket 协议中,初始的握手阶段使用标准的 HTTP 请求和响应:
- 客户端先发送一个 HTTP 请求,请求升级到
WebSocket
协议。 - 服务器在收到这个请求后,如果同意升级到
WebSocket
,就会返回一个状态码为101
的 HTTP 响应,指示升级成功,然后不会断开 TCP 连接。
这个过程涉及到的 HTTP 头部字段是 Upgrade
和 Connection
,具体而言,HTTP 请求头部可能包含类似以下的字段:
请求:
http
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
响应:
http
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*
方法,如ReadJSON
和ReadMessage
。 - 其他功能:关闭连接、获取客户端 IP 地址等
消息
在 gorilla/websocket 中,消息被分为以下几种:
- 数据消息:
TextMessage
文本消息:文本消息被解析为 UTF-8 编码的文本。需要应用程序来确保文本消息是有效的 UTF-8 编码文本。BinaryMessage
二进制消息:二进制消息的解析留给应用程序。
- 控制消息:可以调用
Conn
中的WriteControl
、WriteMessage
或NextWriter
方法,将控制消息发送给对方。CloseMessage
关闭连接的消息PingMessage
ping 消息PongMessage
pong 消息
注意:应用程序需要先读取连接中的消息才能处理从对等方发送的 close
、ping
和 pong
消息。如果应用程序对来自对等方的消息不感兴趣, 则应用程序应启动一个 goroutine
来读取和丢弃来自对等方的消息。
并发
虽然 Golang 中有 goroutine
可以支持我们做并发操作,但是在 gorilla/websocket 中, 一个 WebSocket
连接只支持一个并发 reader
和一个并发 writer
。
我们的应用程序应该确保不超过一个 goroutine
同时调用写入方法(WriteMessage
、WriteJSON
)或者读取方法(ReadMessage
、ReadJSON
)。
而 Close
和 WriteControl
方法可以与其他所有方法同时调用。
安全性
我们知道,在一般的 web 应用中,经常需要处理跨域的问题,同样的,在 gorilla/websocket 中也需要做一定的配置。
我们可以在 Upgrader
中的 CheckOrigin
字段中指定函数的 Origin
检查策略,如果 CheckOrigin
函数返回 false
,则 Upgrader
方法将拒绝建立 WebSocket
连接,如果允许所有来源的连接,我们可以直接返回 true
即可。
go
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
缓冲
缓冲在 io 类操作中是一个很常见的术语,在 gorilla/websocket 中我们可以通过上面那段代码的 ReadBufferSize
和 WriteBufferSize
来指定连接的缓冲大小,以减少读取或写入消息时的系统调用次数。
默认大小为 4096
,建议限制为最大预期消息的大小,大于最大消息最大大小的缓冲区不会带来任何好处。
Hello World
最后,让我们通过一个简单的 Hello World
程序来结束本文:
main.go
go
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
代码:
javascript
let ws = new WebSocket('ws://127.0.0.1:8181/ws')
不出意外的话,我们可以在浏览器控制台的 Network -> WS
中看到由服务端发送的 Hello, World!
。