Skip to content

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 主要在 ConnUpgraderDialer 这几个类型上,下面逐个讲解。

二、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.Readerbufio.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 取值为 TextMessageBinaryMessage 等常量。

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 定义了几个消息类型常量:

常量含义
TextMessage1文本消息
BinaryMessage2二进制消息
CloseMessage8关闭帧
PingMessage9Ping
PongMessage10Pong

通常业务消息用 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)
}

WriteJSONReadJSON 是 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 架构和广播机制。