goWebSocket构建最简单的群聊后台

场景:单个群聊实现
用户操作:加入群聊,退出群聊,发送消息,拉取历史信息

思路:

  • 结构:每个用户是一个conn,使用map将用户WebSocketConn存储,key为用户id
  • 加入:当用户发来的连接请求自动加入用户组
  • 退出:当用户监听返回异常时或用户关闭连接自动退出用户组
  • 群发:检测到用户发来信息的Type字段为1时,for遍历map写给每个在线用户
  • 历史信息:实际使用可用redis的FIFO数据结构,这里为了构造方便使用数组模拟队列实现,Type字段为2即拉取历史信息,该文代码仅拉取最近十条

准备工作:

安装Gorilla Websocket和Gin


go get -u github.com/gorilla/websocket 

go get -u github.com/gin-gonic/gin

代码

项目结构

│  go.mod
│  go.sum
│  main.go
│
└─service
        chat.go 
        continuousListening.go

main.go: 程序入口

chat.go: 包含处理聊天相关的逻辑,如消息处理、用户加入/退出等,并且监听客户端消息。

continuousListening.go: 持续监听并处理事件。

chat.go

// 用户发送
type SendMsg struct {
    Type    int    `json:"type"`
    Content string `json:"content"`
}

// 用户回复
type ReplyMsg struct {
    From    string `json:"from"`
    Code    int    `json:"code"`
    Content string
}

// 返回给用户的历史消息
type HistoryMsg struct {
    Msgs  []Message `json:"msgs"`
    Total int       `json:"total"`
    Type  int       `json:"type"`
}

// 用户实例
type Client struct {
    ID     string
    SendID string
    Socket *websocket.Conn
    Send   chan []byte
}

// 广播
type Broadcast struct {
    Client  *Client
    Message []byte
    Type    int
}

// 用户管理
type ClientManager struct {
    Clients    map[string]*Client //用户管理
        //用通道实现事件触发
    Broadcast  chan *Broadcast    //广播通道
    Register   chan *Client       //用户注册通道
    Unregister chan *Client       //用户注销通道
}

type Message struct {
    Sender  string `json:"sender,omitempty"`  //发送人
    Content string `json:"content,omitempty"` //内容
}
type HistoryStruct struct {
    msgMutex sync.RWMutex //读写锁
    Msgs     []Message    `json:"msgs"` //历史消息
}

var HMsg = &HistoryStruct{Msgs: make([]Message, 0), msgMutex: sync.RWMutex{}}
var Manager = &ClientManager{  //实例
    Clients:   make(map[string]*Client),
    Broadcast: make(chan *Broadcast),
    Register:   make(chan *Client),
    Unregister: make(chan *Client),
}

func ChatHandler(ctx *gin.Context) {

    uid := ctx.Query("uid")  

    conn, err := (&websocket.Upgrader{
       CheckOrigin: func(r *http.Request) bool {
          return true
       }}).Upgrade(ctx.Writer, ctx.Request, nil) //升级ws协议
    if err != nil {
       log.Println("err:", err)
       http.NotFound(ctx.Writer, ctx.Request)
       return
    }
    println("uid:", uid)

    //创建用户实例
    client := &Client{
       ID:     uid,
       Socket: conn,
       Send:   make(chan []byte),
    }
    //用户注册到用户管理
    Manager.Register <- client
    go client.Read()
    go client.Write()
}

// 读取用户传入
func (c *Client) Read() {
    defer func() {
       Manager.Unregister <- c
       _ = c.Socket.Close()
    }()

    for {

       c.Socket.PongHandler()
       sendMsg := new(SendMsg)
       //c.Socket.ReadMessage()
       err := c.Socket.ReadJSON(&sendMsg)
       if err != nil {
          log.Println("数据格式不正确", err)
          Manager.Unregister <- c
          c.Socket.Close()
          break
       }
       fmt.Println("sendMsg:", sendMsg)
       if sendMsg.Type == 1 {
          HMsg.msgMutex.Lock()

          HMsg.Msgs = append(HMsg.Msgs, Message{
             Sender:  c.ID,
             Content: fmt.Sprintf("%s", sendMsg.Content),
          })
          HMsg.msgMutex.Unlock()
          replyMsg := ReplyMsg{
             From:    c.ID,
             Code:    0,
             Content: sendMsg.Content,
          }
          data, _ := json.Marshal(replyMsg)
          Manager.Broadcast <- &Broadcast{
             Client:  c,
             Message: data, //发送过来的消息
          }

       } else if sendMsg.Type == 2 {

          results := History(10) //获取历史消息十条
          fmt.Println("id:", c.SendID, c.ID)
          if len(results) > 10 {
             results = results[10:]
          } else if len(results) == 0 {
             replyMsg := ReplyMsg{

                Code:    -1,
                Content: "上面没有消息了",
             }
             msg, _ := json.Marshal(replyMsg)
             _ = c.Socket.WriteMessage(websocket.TextMessage, msg)
             continue
          }
          replyMsg := HistoryMsg{
             Msgs:  results,
             Total: len(results),
             Type:  2,
          }

          msg, _ := json.Marshal(replyMsg)
          _ = c.Socket.WriteMessage(websocket.TextMessage, msg)
       }

    }
}
func (c *Client) Write() {
    defer func() {
       _ = c.Socket.Close()
    }()
    for {
       select {
       case message, ok := <-c.Send:
          if !ok {
             _ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
             return
          }
          replyMsg := ReplyMsg{

             Code:    200,
             Content: fmt.Sprintf("%s", string(message)),
          }

          msg, _ := json.Marshal(replyMsg)
          _ = c.Socket.WriteMessage(websocket.TextMessage, msg)
       }
    }
}

func History(n int) []Message {
    //读取n条历史消息
    HMsg.msgMutex.RLock()
    defer HMsg.msgMutex.RUnlock()
    if len(HMsg.Msgs) < n {
       return HMsg.Msgs
    }
    return HMsg.Msgs[len(HMsg.Msgs)-n:]
}
continuousListening.go
package service

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/websocket"
)

func (manager *ClientManager) HandleWebSocketEvents() {
    for { //循环监听
        select {
        case conn := <-Manager.Register: //将连接放入用户管理
            fmt.Printf("有新连接 %v\n ", conn.ID)
            Manager.Clients[conn.ID] = conn
            replyMsg := ReplyMsg{

                Code:    200,
                Content: "已经连接到服务器",
            }
            msg, _ := json.Marshal(replyMsg)
            _ = conn.Socket.WriteMessage(websocket.TextMessage, msg)
        case conn := <-Manager.Unregister: //删除连接
            fmt.Printf("注销连接:%s", conn.ID)
            if _, ok := Manager.Clients[conn.ID]; ok { //若该连接以注册
                replyMsg := ReplyMsg{

                    Code:    -1,
                    Content: "连接中断",
                }
                msg, _ := json.Marshal(replyMsg)
                _ = conn.Socket.WriteMessage(websocket.TextMessage, msg)
                close(conn.Send)
                delete(Manager.Clients, conn.ID)
            }

            //有人发消息
        case brodcast := <-Manager.Broadcast:
            data := brodcast.Message

            //Manager.Clients是用户连接表
            for _, conn := range Manager.Clients {

                conn.Socket.WriteMessage(websocket.TextMessage, data)
            }

        }
    }
}

main.go

func main() {
  g := gin.Default()
  go func() {
      service.Manager.HandleWebSocketEvents()
  }()
  g.GET("ws", service.ChatHandler)
  g.Run(":8080")
}

演示

1、启动项目

  go run main.go

2、打开postman或其他测试工具

我这里创建了两个连接,id为12与id为13
在这里插入图片描述
在这里插入图片描述

id为13发送消息
在这里插入图片描述

可以看到id为12的成功接收

在这里插入图片描述

这里我多发了几段,然后拉取历史消息
在这里插入图片描述

可以看到成功拉取历史信息

ps:本文是为了考研失败的话能够将这些技术捡起来而写,若有错误或疑问,我都会虚心聆听或指导
代码仓库:https://github.com/ygxiaobai111/miniChat

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值