我是HullQin,公众号线下团聚游戏的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者HullQin受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。
背景
第一篇文章:《为什么我选用Go重构Python版本的WebSocket服务?》,介绍了我的指标。
上篇文章讲了《你的第一个Go WebSocket服务: echo server》,明天咱们实现一个聊天室。
如果你没浏览上一篇文章,肯定要先看一下,因为这篇文章更简单,如果你不弄懂上一篇,这篇可能看不懂哦。
新建我的项目并装置依赖
可参考《你的第一个Go WebSocket服务: echo server》。
新建个我的项目文件夹,命令行执行以下,装置Go Websocket依赖:
go get github.com/gorilla/websocket
拷贝chat代码
把gorilla/websocket
的官网demo拷贝过去即可,咱们缓缓剖析:
- github.com/gorilla/websocket/tree/master/examples/chat
你须要这4个文件:
- main.go
- hub.go
- client.go
- index.html
第一步,看主函数
func main() { flag.Parse() hub := newHub() go hub.run() http.HandleFunc("/", serveHome) http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { serveWs(hub, w, r) }) err := http.ListenAndServe(*addr, nil) if err != nil { log.Fatal("ListenAndServe: ", err) }}
上篇曾经介绍了flag
和http.HandleFunc
,这里跟上篇是截然不同的。
这里还开启了一个goroutine,留神它是写在main函数里的,不是写在http.HandleFunc里的。所以不论有多少客户端连贯,这个服务只开启了一个goroutine。newHub().run()
。咱们下一步看newHub()
,在hub.go文件中。
再看下注册的2个申请处理函数:
serveHome
是一个HTTP服务,把html文件返回给申请方(浏览器)。- 针对
/ws
路由,则会调用serveWs
,咱们下下一步看serveWs
做了什么,在clent.go文件中。
第二步,看hub.go
Hub定义和newHub函数定义
type Hub struct { clients map[*Client]bool broadcast chan []byte register chan *Client unregister chan *Client}func newHub() *Hub { return &Hub{ clients: make(map[*Client]bool), register: make(chan *Client), unregister: make(chan *Client), broadcast: make(chan []byte), }}
能够看到newHub只是新建了一个空白的Hub。而1个Hub蕴含4个货色:
clients
,保留了每个客户端的援用的Map(其实这个Map的value没有用到,key是客户端的援用,能够当作是其它语言的set)。register
,用于注册客户端的channel。每当有客户端建设websocket连贯时,通过register,把客户端保留到clients援用中。unregister
,用于登记客户端的channel。每当有客户端断开websocket连贯时,通过unregister,把客户端援用从clients中删除。broadcast
,用于发送播送的channel。把音讯存到这个channel后,之后会有其它goroutine遍历clients
,把音讯发送给所有客户端。
服务开启时启动的goroutine: hub.run()
func (h *Hub) run() { for { select { case client := <-h.register: h.clients[client] = true case client := <-h.unregister: if _, ok := h.clients[client]; ok { delete(h.clients, client) close(client.send) } case message := <-h.broadcast: for client := range h.clients { select { case client.send <- message: default: close(client.send) delete(h.clients, client) } } } }}
一个死循环:一直从channel读取数据。读取到register
,就注册客户端。读取到unregister
,就断开客户端连贯,删除援用。读取到broadcast
,就遍历clients
,播送音讯(通过把音讯写入每个客户端的client.send
channel中,实现播送),正是下一步要看的逻辑。
下一步,咱们看client
。
第三步,看client.go
Client定义
type Client struct { hub *Hub conn *websocket.Conn send chan []byte}
hub
: 每个Client客户端保留了Hub
的援用。(尽管目前全局只有1个hub,然而为了可扩展性,还是保留一份吧,因为未来会有多hub,下篇文章咱们就介绍!)conn
: 即跟客户端的websocket连贯,通过这个conn
能够跟客户端交互(即收发音讯)。send
: 一个channel,在第二步曾经见识到了,broadcast时,就是把音讯写入了每个Client的send
channel中。通过从这个channel读取音讯,发送音讯给客户端。
main函数用到的serveWs函数
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println(err) return } client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)} client.hub.register <- client // Allow collection of memory referenced by the caller by doing all work in // new goroutines. go client.writePump() go client.readPump()}
在hub中,注册了一下。
随后启动了2个goroutine: client.writePump()
和client.readPump()
,而后这个函数逻辑就完结了。
这2个goroutine,别离用于解决写入音讯和读取音讯。
client.writePump
func (c *Client) writePump() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() c.conn.Close() }() for { select { case message, ok := <-c.send: c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if !ok { c.conn.WriteMessage(websocket.CloseMessage, []byte{}) return } w, err := c.conn.NextWriter(websocket.TextMessage) if err != nil { return } w.Write(message) if err := w.Close(); err != nil { return } case <-ticker.C: c.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } }}
首先开启了一个ping计时器。会固定周期发送Ping音讯给客户端。这是WebSocket协定要求的,参考《RFC6455》。你在浏览器上抓包看不到这个Ping音讯。这种形式,能够将没响应的连贯清理掉。
而后,这个goroutine,申明了defer执行的逻辑:敞开计时器,敞开连贯。
最重要的局部,这个goroutine有个死循环:一直读取client.send这个channel中的数据。只有hub.broadcast给它传了音讯,那么就由这个goroutine来解决。c.conn.NextWriter
和w.Write(message)
是真正的发消息的逻辑。
此外,每隔一段时间(定时器设置的工夫距离),服务器都会发送一个Ping给浏览器。浏览器会主动回复一个Pong(不须要客户端开发者关注,客户端开发者通常是JS开发者)。
client.readPump
func (c *Client) readPump() { defer func() { c.hub.unregister <- c c.conn.Close() }() c.conn.SetReadLimit(maxMessageSize) c.conn.SetReadDeadline(time.Now().Add(pongWait)) 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 = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) c.hub.broadcast <- message }}
readPump
就是读取音讯,收到客户端音讯后,就借助hub.broadcast
播送进来。
此外,这个goroutine有个重要的工作:敞开连贯后,负责hub.unregister
和conn.Close
。
总结!最重要的一个图!
为了帮忙大家了解,我绘制了这个图:
其中,黑白矩形示意goroutine,黑白线条是各个channel(从A指向B示意,由goroutine A写入数据,由goroutine B读取数据)。
User和Client图中只画了2个,是能够持续减少的。
写在最初
我是HullQin,公众号线下团聚游戏的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者HullQin受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。