介绍
示例仓库
官方例子: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
一句话描述业务
客户端可以连接服务器
客户端可以发送消息,然后服务端立即广播消息
技术描述业务
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