Appearance
gorilla/websocket 实战
上一篇我们理解了 WebSocket 协议的握手过程、帧格式和生命周期,并对比了 Go 生态中的三大主流库。从本篇开始,我们将以 gorilla/websocket 为核心,进入实战阶段。本篇覆盖安装、Upgrader 配置、基本连接、消息收发、连接管理(心跳、超时、Close/Ping handler)、并发安全,最后给出一个完整的 Echo 服务器示例(服务端 + 客户端)。
一、安装 gorilla/websocket
gorilla/websocket 是一个标准的 Go module,安装很简单。在你的项目目录下执行:
bash
go get github.com/gorilla/websocket如果你用的是较新的 Go 版本(1.16+),也可以在代码中直接 import,然后 go mod tidy 自动拉取依赖。
go
package main
import (
"fmt"
"github.com/gorilla/websocket"
)
func main() {
// 仅验证依赖可用
fmt.Println("gorilla/websocket 版本:", websocket.IsUnexpectedCloseError)
}运行这段代码,如果没有报错说明依赖已经就位。gorilla/websocket 的核心 API 主要在 Conn、Upgrader、Dialer 这几个类型上,下面逐个讲解。
二、Upgrader:HTTP 升级为 WebSocket
WebSocket 连接的起点是一个普通的 HTTP 请求。服务端需要把这个 HTTP 连接「升级」为 WebSocket 连接,这件事由 Upgrader 完成。
1. Upgrader 的作用
Upgrader 封装了握手过程:校验请求头、计算 Sec-WebSocket-Accept、返回 101 响应、返回一个 *Conn 对象。你只需要调用 upgrader.Upgrade(w, r, nil) 即可。
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
// 把 HTTP 连接升级为 WebSocket 连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("升级失败:", err)
return
}
defer conn.Close()
fmt.Println("客户端已连接:", r.RemoteAddr)
// 这里可以开始收发消息
}
func main() {
http.HandleFunc("/ws", wsHandler)
fmt.Println("WebSocket 服务监听 :8080")
http.ListenAndServe(":8080", nil)
}Upgrade 方法做了几件事:检查请求方法是否为 GET、检查 Upgrade 头、检查 Connection 头、检查 Sec-WebSocket-Version 是否为 13、检查 Origin(可选)。任何一步失败都会返回错误并写入 HTTP 错误响应。
2. CheckOrigin 配置
默认情况下,Upgrader.CheckOrigin 会拒绝跨域请求(返回 false)。在浏览器中,new WebSocket("ws://...") 会带上当前页面的 Origin 头,服务端如果不放行就会握手失败。
开发环境通常直接放行所有 Origin:
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
// 开发环境放行所有来源;生产环境应配置白名单
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
fmt.Println("已连接:", r.RemoteAddr)
}
func main() {
http.HandleFunc("/ws", wsHandler)
http.ListenAndServe(":8080", nil)
}生产环境应该做白名单校验,例如只允许来自你自己域名的请求:
go
package main
import (
"net/http"
"strings"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
// 只允许自有域名
allowed := []string{"https://example.com", "https://app.example.com"}
for _, a := range allowed {
if strings.EqualFold(origin, a) {
return true
}
}
return false
},
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn != nil {
conn.Close()
}
})
http.ListenAndServe(":8080", nil)
}3. ReadBufferSize 与 WriteBufferSize
这两个参数指定读写缓冲区大小(字节)。默认都是 4096。它们影响的是底层 bufio.Reader 和 bufio.Writer 的缓冲区。
- 如果你的消息通常较小(如几 KB 的 JSON),用默认值即可。
- 如果消息较大(如传输图片、文件分片),可以适当调大,避免多次系统调用。
- 调大缓冲区会增加每个连接的内存占用,要权衡。
go
package main
import (
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096, // 读缓冲 4KB
WriteBufferSize: 4096, // 写缓冲 4KB
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn != nil {
defer conn.Close()
}
})
http.ListenAndServe(":8080", nil)
}三、基本连接:服务端和客户端
1. 服务端
服务端用 Upgrader.Upgrade 拿到 *Conn,然后就可以收发消息。最简单的服务端就是收到消息后原样返回(Echo):
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("升级失败:", err)
return
}
defer conn.Close()
for {
// 读取消息
msgType, msg, err := conn.ReadMessage()
if err != nil {
fmt.Println("读取失败:", err)
break
}
fmt.Printf("收到: %s\n", msg)
// 原样返回
if err := conn.WriteMessage(msgType, msg); err != nil {
fmt.Println("写入失败:", err)
break
}
}
})
fmt.Println("Echo 服务监听 :8080/echo")
http.ListenAndServe(":8080", nil)
}2. 客户端
gorilla/websocket 也提供了客户端能力,用 Dialer.Dial 或便捷函数 websocket.Dial 建立连接:
go
package main
import (
"fmt"
"log"
"net/url"
"github.com/gorilla/websocket"
)
func main() {
u := url.URL{Scheme: "ws", Host: "localhost:8080", Path: "/echo"}
fmt.Printf("连接到 %s\n", u.String())
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("拨号失败:", err)
}
defer c.Close()
// 发送一条消息
if err := c.WriteMessage(websocket.TextMessage, []byte("你好,WebSocket")); err != nil {
log.Fatal("写入失败:", err)
}
// 接收回显
_, msg, err := c.ReadMessage()
if err != nil {
log.Fatal("读取失败:", err)
}
fmt.Printf("收到回显: %s\n", msg)
}先启动服务端,再运行客户端,你会看到客户端发送的消息被服务端原样返回。这就是一个最小可运行的 WebSocket 通信闭环。
四、消息收发
1. ReadMessage:读取消息
ReadMessage() 返回三个值:消息类型(int)、消息内容([]byte)、错误。它会阻塞直到收到一帧完整消息。如果对方关闭连接,会返回错误。
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil {
fmt.Println("读取结束:", err)
return
}
fmt.Printf("类型=%d 内容=%s\n", msgType, msg)
}
})
http.ListenAndServe(":8080", nil)
}2. WriteMessage:发送消息
WriteMessage(msgType, data) 发送一条消息。msgType 取值为 TextMessage、BinaryMessage 等常量。
go
package main
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
// 每秒推送一次时间
for i := 0; i < 5; i++ {
text := fmt.Sprintf("第 %d 次推送: %s", i+1, time.Now().Format("15:04:05"))
if err := conn.WriteMessage(websocket.TextMessage, []byte(text)); err != nil {
fmt.Println("写入失败:", err)
return
}
time.Sleep(time.Second)
}
})
fmt.Println("推送服务监听 :8080")
http.ListenAndServe(":8080", nil)
}3. 消息类型:TextMessage、BinaryMessage
gorilla/websocket 定义了几个消息类型常量:
| 常量 | 值 | 含义 |
|---|---|---|
| TextMessage | 1 | 文本消息 |
| BinaryMessage | 2 | 二进制消息 |
| CloseMessage | 8 | 关闭帧 |
| PingMessage | 9 | Ping |
| PongMessage | 10 | Pong |
通常业务消息用 TextMessage(传 JSON 文本),二进制数据用 BinaryMessage:
go
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type ChatMsg struct {
From string `json:"from"`
Text string `json:"text"`
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
return
}
if msgType == websocket.TextMessage {
// 文本消息:解析 JSON
var m ChatMsg
if err := json.Unmarshal(data, &m); err == nil {
fmt.Printf("文本消息 from=%s text=%s\n", m.From, m.Text)
// 回复也用 JSON 文本
reply, _ := json.Marshal(ChatMsg{From: "server", Text: "已收到"})
conn.WriteMessage(websocket.TextMessage, reply)
}
} else if msgType == websocket.BinaryMessage {
// 二进制消息:直接处理字节
fmt.Printf("二进制消息 %d 字节\n", len(data))
conn.WriteMessage(websocket.BinaryMessage, data)
}
}
})
http.ListenAndServe(":8080", nil)
}
WriteJSON和ReadJSON是 gorilla/websocket 提供的便捷方法,内部就是json.Marshal+WriteMessage(TextMessage, ...),可以省去手动序列化。
五、连接管理
长连接需要管理:保活、超时、关闭回调。gorilla/websocket 提供了一组 handler 和 deadline 机制。
1. Ping/Pong 心跳实现
gorilla/websocket 默认会自动回复 Pong(收到 Ping 时),但你也可以自定义心跳逻辑。常见做法是服务端定期发 Ping,客户端回 Pong;如果一段时间没收到 Pong 就断开。
go
package main
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
maxMessageSize = 512
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
// 限制最大消息大小
conn.SetReadLimit(maxMessageSize)
// 设置读超时:60 秒内必须收到任何帧(含 Pong)
conn.SetReadDeadline(time.Now().Add(pongWait))
// 收到 Pong 时刷新读超时
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// 启动一个 goroutine 定期发 Ping
go func() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for range ticker.C {
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}()
// 主循环:读取消息
for {
_, msg, err := conn.ReadMessage()
if err != nil {
fmt.Println("连接断开:", err)
return
}
fmt.Printf("收到: %s\n", msg)
}
})
fmt.Println("带心跳的 WebSocket 服务监听 :8080")
http.ListenAndServe(":8080", nil)
}这段代码的关键点:
pingPeriod设为pongWait的 90%,保证在超时前能发出去 Ping。SetPongHandler在收到客户端回的 Pong 时刷新读超时,实现「只要还活着就一直续期」。- 如果读超时触发(60 秒没收到任何帧),
ReadMessage会返回错误,循环退出,连接关闭。
2. 设置读写超时:SetReadDeadline、SetWriteDeadline
SetReadDeadline(t) 设置读操作的最后期限,超过则读操作返回超时错误。SetWriteDeadline(t) 同理作用于写操作。
这两个 API 是「软超时」:到期后下一次 I/O 操作才会失败,不会主动中断当前操作。所以正确的用法是在每次读写前重新设置 deadline。
go
package main
import (
"net/http"
"time"
"github.com/gorilla/websocket"
)
const writeWait = 10 * time.Second
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// 安全写入:每次写之前刷新 deadline
func safeWrite(conn *websocket.Conn, msgType int, data []byte) error {
conn.SetWriteDeadline(time.Now().Add(writeWait))
return conn.WriteMessage(msgType, data)
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
for {
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
// 每次读完刷新读超时
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
if err := safeWrite(conn, msgType, msg); err != nil {
return
}
}
})
http.ListenAndServe(":8080", nil)
}3. Close handler
SetCloseHandler 设置收到对方 Close 帧时的回调。默认行为是回一个 Close 帧并返回 ErrCloseSent。你可以自定义关闭逻辑(如记录日志、通知其他连接)。
go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
// 自定义 Close handler
conn.SetCloseHandler(func(code int, text string) error {
fmt.Printf("客户端关闭连接 code=%d reason=%s\n", code, text)
// 返回 nil 表示不发送响应 Close 帧(默认会发)
// 调用默认逻辑可以发送回执
return conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(code, ""),
time.Now().Add(time.Second),
)
})
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
})
http.ListenAndServe(":8080", nil)
}注意:上面
time需要导入,这里为了示例聚焦省略了完整 import,实际运行请补全import "time"。
4. Ping handler
SetPingHandler 设置收到 Ping 时的回调。默认是自动回 Pong。你也可以自定义(如记录心跳时间)。
go
package main
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
lastHeartbeat := time.Now()
conn.SetPingHandler(func(appData string) error {
lastHeartbeat = time.Now()
fmt.Println("收到客户端 Ping, 回复 Pong")
// 默认行为:回 Pong
return conn.WriteControl(
websocket.PongMessage,
[]byte(appData),
time.Now().Add(time.Second),
)
})
for {
_, _, err := conn.ReadMessage()
if err != nil {
fmt.Println("最后心跳:", lastHeartbeat.Format("15:04:05"))
break
}
}
})
http.ListenAndServe(":8080", nil)
}六、并发安全:每个连接一个 goroutine 读取
gorilla/websocket 的 Conn 有一个重要约束:同一个连接的写操作不能并发,但读操作和写操作可以并发(即一个 goroutine 读、另一个 goroutine 写是安全的)。多个 goroutine 同时 WriteMessage 会导致数据帧交错、连接损坏。
因此推荐的模式是:每个连接固定一个 goroutine 负责读,写操作通过 channel 集中到一个 goroutine 处理。
go
package main
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
if conn == nil {
return
}
defer conn.Close()
// 用 channel 集中所有写操作,由单个 goroutine 处理
writeCh := make(chan []byte, 16)
// 写 goroutine
go func() {
defer conn.Close()
for msg := range writeCh {
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
fmt.Println("写入失败:", err)
return
}
}
}()
// 读 goroutine(在主 goroutine)
for {
_, msg, err := conn.ReadMessage()
if err != nil {
fmt.Println("读取结束:", err)
close(writeCh)
return
}
fmt.Printf("收到: %s\n", msg)
// 把要回复的消息发给写 goroutine
writeCh <- msg
}
})
http.ListenAndServe(":8080", nil)
}这种「读 goroutine + 写 goroutine + channel」模式是 gorilla/websocket 最经典的并发模式,后面的聊天室实战会在此基础上扩展。
七、完整示例:Echo 服务器(服务端 + 客户端)
把前面的知识点串起来,这里给出一个完整、可运行的 Echo 服务器,包含服务端和客户端两部分。服务端带心跳、超时、优雅关闭;客户端会自动重连。
服务端
go
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = 50 * time.Second
maxMessageSize = 4096
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
// 处理单个 WebSocket 连接
func handleWS(conn *websocket.Conn) {
defer conn.Close()
conn.SetReadLimit(maxMessageSize)
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// 写 channel + 写 goroutine
writeCh := make(chan []byte, 16)
done := make(chan struct{})
go func() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
close(done)
}()
for {
select {
case msg, ok := <-writeCh:
if !ok {
// channel 关闭,发送 Close 帧
conn.WriteControl(
websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
time.Now().Add(writeWait),
)
return
}
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return
}
case <-ticker.C:
// 定期发 Ping 保活
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}()
// 读循环
for {
_, msg, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseNormalClosure, websocket.CloseGoingAway) {
log.Printf("异常关闭: %v", err)
}
break
}
log.Printf("收到 %d 字节: %s", len(msg), string(msg))
// 回显
select {
case writeCh <- msg:
default:
// 写缓冲满,丢弃或关闭
}
}
close(writeCh)
<-done
}
func main() {
http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("升级失败:", err)
return
}
handleWS(conn)
})
fmt.Println("Echo 服务监听 ws://localhost:8080/echo")
log.Fatal(http.ListenAndServe(":8080", nil))
}客户端
go
package main
import (
"fmt"
"log"
"net/url"
"time"
"github.com/gorilla/websocket"
)
func main() {
u := url.URL{Scheme: "ws", Host: "localhost:8080", Path: "/echo"}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("连接失败:", err)
}
defer c.Close()
// 读 goroutine:打印回显
done := make(chan struct{})
go func() {
defer close(done)
for {
_, msg, err := c.ReadMessage()
if err != nil {
log.Println("读取结束:", err)
return
}
fmt.Printf("回显: %s\n", msg)
}
}()
// 主 goroutine:每秒发一条消息
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 5; i++ {
<-ticker.C
text := fmt.Sprintf("第 %d 条消息", i+1)
if err := c.WriteMessage(websocket.TextMessage, []byte(text)); err != nil {
log.Println("写入失败:", err)
break
}
}
// 优雅关闭
c.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
select {
case <-done:
case <-time.After(time.Second):
}
fmt.Println("客户端退出")
}分别编译运行服务端和客户端,你会看到客户端发送的 5 条消息都被原样回显,最后双方优雅关闭连接。这个例子涵盖了本篇讲到的所有核心 API:Upgrader、ReadMessage/WriteMessage、Ping/Pong、SetReadDeadline/SetWriteDeadline、Close handler、读写分离的并发模式。
八、小结
本篇我们完整学习了 gorilla/websocket 的核心用法。从安装依赖、用 Upgrader 完成 HTTP 到 WebSocket 的升级开始,掌握了 CheckOrigin 跨域配置和读写缓冲区调优。然后学习了 ReadMessage/WriteMessage 进行消息收发,区分了文本帧与二进制帧的使用场景。接着深入连接管理:用 Ping/Pong 实现心跳保活,用 SetReadDeadline/SetWriteDeadline 控制超时,用 SetCloseHandler/SetPingHandler 自定义回调。最后强调了并发安全的关键约束——「同一连接的写操作不能并发」,并给出了「读 goroutine + 写 goroutine + channel」的标准模式,以一个完整的 Echo 服务器收尾。
关键要点回顾:
Upgrader.Upgrade完成握手,CheckOrigin控制跨域,缓冲区大小按消息规模调整。ReadMessage/WriteMessage是收发基础,消息类型用TextMessage/BinaryMessage区分。- 心跳用 Ping/Pong + deadline 续期,超时即视为断线。
- 写操作不能并发,用 channel 集中到单个写 goroutine 是最佳实践。
WriteJSON/ReadJSON是 JSON 收发的便捷封装。
下一篇我们将把这些 API 组合成一个真正的多人聊天室系统,引入 Hub/Client 架构和广播机制。