写点什么

[Go WebSocket] 单房间的聊天室

作者:HullQin
  • 2022 年 9 月 08 日
    广东
  • 本文字数:3655 字

    阅读完需:约 12 分钟

[Go WebSocket] 单房间的聊天室

我是 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 拷贝过来即可,我们慢慢分析:



你需要这 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)   }}
复制代码


上篇已经介绍了flaghttp.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.sendchannel 中,实现广播),正是下一步要看的逻辑。


下一步,我们看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.NextWriterw.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.unregisterconn.Close

总结!最重要的一个图!

为了帮助大家理解,我绘制了这个图:



其中,彩色矩形表示 goroutine,彩色线条是各个 channel(从 A 指向 B 表示,由 goroutine A 写入数据,由 goroutine B 读取数据)。


User 和 Client 图中只画了 2 个,是可以继续增加的。

写在最后

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

发布于: 2022 年 09 月 08 日阅读数: 32
用户头像

HullQin

关注

公众号【线下聚会游戏】 2020.10.07 加入

game.hullqin.cn 我做了一些联机桌游网页:支持2-10人联机的UNO、2-4人联机的斗地主、2人联机的五子棋。无需下载,点开即玩!叫上朋友,即刻开局!不看广告,不做任务,享受「纯粹」的游戏!

评论

发布
暂无评论
[Go WebSocket] 单房间的聊天室_Go_HullQin_InfoQ写作社区