介绍

示例仓库

  • 官方例子:Chat example

    • https://github.com/gorilla/websocket/tree/master/examples/chat

  • 为上更改过的例子:cloud-native-game-server/2-gorilla-websocket-chat

    • https://github.com/Hacker-Linner/cloud-native-game-server/tree/master/demo/2-gorilla-websocket-chat

为啥要再熟悉下这个例子?

通过通信共享内存,通过通信共享内存,通过通信共享内存

分析 Nano 之前,再过一遍 Golang 的并发编程。

示例分析

这里我整理下这个例子的官方 README.md

一句话描述业务

  1. 客户端可以连接服务器

  2. 客户端可以发送消息,然后服务端立即广播消息

技术描述业务

websocket
websocket读写
Clientwebsocket读写
websocket
HubClientClientwebsocket写

Server

ClientHubClientClientHubHub
HubClientHubClient

核心源码解释:

......
func main() {
......
// 应用一运行,就初始化 `Hub` 管理工作
hub := newHub()
// 开个 goroutine,后台运行监听三个 channel
// register:注册客户端 channel
// unregister:注销客户端 channel
// broadcast:广播客户端 channel
go hub.run()

http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
.....
}
......
// serveWs 处理来自每一个客户端的 "/ws" 请求。
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
// 升级这个请求为 `websocket` 协议
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
// 初始化当前的客户端实例,并与 `hub` 中心管理勾搭上,
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register
// 通过在新的goroutines中完成所有工作,允许调用者引用内存的集合。
// 其实对当前 `websocket` 连接的 `I/O` 操作
// 写操作(发消息到客户端)-> 这里 `Hub` 会统一处理
go client.writePump()
// 读操作(对消息到客户端)-> 读完当前连接立即发 -> 交由 `Hub` 分发消息到所有连接
go client.readPump()
}

Hub

Hubmainrunregisterunregisterbroadcast
clients
clientssend
sendsend

核心源码解释:

func (h *Hub) run() {
for {
select {
// 注册 channel
case client := // 键值对操作,没啥好说的
h.clients[client] = true
// 注销 channel
case client := // 键值对操作,没啥好说的
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
// 广播 channel
case message := for client := range h.clients {
select {
// 直接送入各个连接的 send channel
case client.send // 卡住,这里直接踢掉
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}

Client

Client
serveWsmain
writePump
readPump
readPumpwritePump
writePumpsend

核心源码解释:

// readPump 从 Websocket 连接用泵将消息输送到 hub。
// 应用程序在每个连接 goroutine 中运行 readPump。
// 应用程序通过执行此 goroutine 中的所有读取来确保连接上最多有一个 reader。
func (c *Client) readPump() {
defer func() {
c.hub.unregister c.conn.Close()
}()
// SetReadLimit 设置从对等方读取的消息的最大大小。如果消息超出限制,则连接会将关闭消息发送给对等方,然后将ErrReadLimit返回给应用程序。
c.conn.SetReadLimit(maxMessageSize)
// SetReadDeadline 设置基础网络连接上的读取期限。读取超时后,websocket 连接状态已损坏,以后所有读取将返回错误。参数值为零表示读取不会超时。
c.conn.SetReadDeadline(time.Now().Add(pongWait))
// SetPongHandler 为从 peer 接收到的 pong 消息设置处理程序。处理程序的参数是 PONG 消息应用程序数据。默认的 pong 处理程序不执行任何操作。
// handler函数从 NextReader、ReadMessage 和 message reader Read方法处被调用。
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
// 读取消息
_, message, err := c.conn.ReadMessage()
if err != nil {
// 错误处理
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
// 整理 message 内容
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))

// 广播
c.hub.broadcast }
}

// writePump 将消息从 hub pump到 websocket 连接。

// 为每个连接启动运行 writePump 的 goroutine。
// 通过执行这个 goroutine 中的所有写操作,应用程序确保连接最多只有一个 writer。
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
// 写消息到当前的 websocket 连接
case message, ok := c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// hub 关闭这个 channel
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
// NextWriter 为要发送的下一条消息返回一个写入器。写入器的Close方法将完整的消息刷新到网络。
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// 将排队聊天消息添加到当前的 websocket 消息中。
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write( }
if err := w.Close(); err != nil {
return
}
// 定时检测下客户端的状态
case c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}

API 的相关细节,大家可以直接查文档,思想才是最重要的

Frontend

前端代码在 home.html 中。

在加载文档时,脚本在浏览器中检查 websocket 功能。如果 websocket 功能可用,那么脚本打开一个到服务器的连接,并注册一个回调函数来处理来自服务器的消息。回调函数使用 appendLog 函数将消息追加到聊天日志中。

appendLog

表单处理程序将用户输入写入websocket并清除输入字段。

Docker 搭建开发调试环境

Image
docker build -f Dockerfile.dev -t cloud-native-game-server:dev .

启动开发环境(支持 live reload)

DEMO=2-gorilla-websocket-chat docker-compose up demo
#docker-compose down

进入 localhost:3250 可以看到效果。

启动调式环境

DEMO=2-gorilla-websocket-chat docker-compose up demo-debug