相关文章推荐
非常酷的打火机  ·  618价格大战 ...·  2 月前    · 
很拉风的鼠标垫  ·  国家概况_中华人民共和国外交部·  10 月前    · 
爱听歌的生菜  ·  重要!落实中央一号文件最新部署→·  1 年前    · 
寂寞的炒粉  ·  我想问一下亮剑36集完整版从哪里能看_百度知道·  2 年前    · 
发财的包子  ·  青海省2022年技术合同成交额达16亿元-中新网·  2 年前    · 
Code  ›  使用Go开发WebSocket应用:单房间聊天室开发者社区
软件 聊天室 websocket
https://cloud.tencent.com/developer/article/2130041
风流的冲锋衣
1 年前
HullQin

使用Go开发WebSocket应用:单房间聊天室

原创
前往小程序,Get 更优 阅读体验!
立即前往
腾讯云
开发者社区
文档 建议反馈 控制台
首页
学习
活动
专区
工具
TVP
最新优惠活动
文章/答案/技术大牛
发布
首页
学习
活动
专区
工具
TVP 最新优惠活动
返回腾讯云官网
HullQin
首页
学习
活动
专区
工具
TVP 最新优惠活动
返回腾讯云官网
社区首页 > 专栏 > 使用Go开发WebSocket应用:单房间聊天室

使用Go开发WebSocket应用:单房间聊天室

原创
作者头像
HullQin
发布 于 2022-09-29 21:07:34
6.6K 7
发布 于 2022-09-29 21:07:34
举报
文章被收录于专栏: 教你做小游戏

背景

大家好,我是公众号「线下聚会游戏」作者,开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏。其中的核心技术就是WebSocket,快关注 HullQin 一起学习吧!

还没学过Go,要先看什么?

建议你花1天时间,看一下Go的原理简介、基础语法。什么教程都可以,知名的教程就行。

至少要明白:各种数据类型,控制流(for、if等)写法,弄懂channel和goroutine,如何加锁。

一定要自己写写goroutine和channel试一下,了解一下基础语法。

此外,还要了解常用包的用法,包括fmt、net/http。

技术选型

面对自己不熟悉的语言和不熟悉的框架,该怎么做技术选型呢?

我告诉你个小技巧,直接在Github上搜索,看Star最多的那个仓库,就可以啦~

image.png
image.png

看吧,我们搜到了 gorilla/websocket ,star数以显著差异甩开了后面几名。这就没有什么好纠结的了,果断使用它。

新建项目

在使用GoLand时,新建Go Project会有2个选项:

image.png
image.png

我们选用第一个即可。

如果你没有GoLand,也可以手动创建文件夹,在里面新建文件 go.mod (我是使用的目前最新稳定版1.18)

代码语言: text
复制
module echo
go 1.18

安装依赖

代码语言: shell
复制
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

第一步,看主函数

代码语言: go
复制
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函数定义

代码语言: go
复制
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()

代码语言: go
复制
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定义

代码语言: go
复制
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函数

代码语言: go
复制
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

代码语言: go
复制
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

代码语言: go
复制
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)
 
推荐文章
非常酷的打火机  ·  618价格大战 缺席的vivo|京东|一加|618_新浪科技_新浪网
2 月前
很拉风的鼠标垫  ·  国家概况_中华人民共和国外交部
10 月前
爱听歌的生菜  ·  重要!落实中央一号文件最新部署→
1 年前
寂寞的炒粉  ·  我想问一下亮剑36集完整版从哪里能看_百度知道
2 年前
发财的包子  ·  青海省2022年技术合同成交额达16亿元-中新网
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号