介绍
示例仓库
- 官方例子: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
一句话描述业务
- 客户端可以连接服务器 
- 客户端可以发送消息,然后服务端立即广播消息 
技术描述业务
websocketwebsocket读写Clientwebsocket读写websocketHubClientClientwebsocket写Server
ClientHubClientClientHubHubHubClientHubClient核心源码解释:
......
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
Hubmainrunregisterunregisterbroadcastclientsclientssendsendsend核心源码解释:
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
ClientserveWsmainwritePumpreadPumpreadPumpwritePumpwritePumpsend核心源码解释:
// 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 搭建开发调试环境
Imagedocker 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