在本系列文章中,将会使用在 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 请求头部可能包含类似以下的字段:
请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
响应:
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 即可。
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
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 代码:
let ws = new WebSocket('ws://127.0.0.1:8181/ws')
不出意外的话,我们可以在浏览器控制台的 Network -> WS 中看到由服务端发送的 Hello, World!。
精彩文章
发表评论