golang 实时用户聊天 原来如此简单?

255 阅读4分钟

技术:MongoDB、mysql、golang

有如下功能

  1. 单聊
  2. 群聊
  3. 获取历史消息
  • 获取历史,发消息都是用post请求,服务端使用把post请求的过来的消息用websocket转发出去

数据库表设计如下

mysql

type Friend struct {
   Id        uint32 `gorm:"autoIncrement;primaryKey;comment:''" `
   Me        uint32
   He        uint32
   Class     uint   `gorm:"comment:'1是人,2是群'" `
   NeverRead uint32 `gorm:"comment:'未读消息'" `
}

MongoDB

  • 用于存储历史聊天记录

  • 用 @区别谁发给谁,如1用户发给666,code判断是单聊还是群聊 群聊也是相当于一个朋友,在数据库存储中 图片.png

思路
  • 1、首先,我们需要一个管理websocket连接的连接管理者,不然深千上万的连接,无人管理,岂不是乱套了。
type Client struct {
   Id             string  //给连接取个别名
   LastOnlineTime time.Time //用于标记这个连接最右一次发消息的时间,太长时间活跃,我们服务端就可以主动关闭它拉
   Socket         *websocket.Conn  //保存websocket连接的地址地址
   Send           chan []byte  // 用户缓冲,可要可不要
}
// 把上面的用户用map存起来
type ClientManager struct {
   Clients map[string]*Client
}
//初始化
var Manager = ClientManager{
   Clients: make(map[string]*Client),
}
  • 2、接下来就是处理get连接,升级成websocket连接啦
func WsHandler(g *gin.Context) {
   sendFrom := g.Query("s")

   conn, err := (&websocket.Upgrader{
      CheckOrigin: func(r *http.Request) bool { // CheckOrigin解决跨域问题
         return true
      }}).Upgrade(g.Writer, g.Request, nil) // 升级成ws协议
   if err != nil {
      http.NotFound(g.Writer, g.Request)
      return
   }

   mdId := sendFrom
   client := &Client{
      Id:             mdId,
      LastOnlineTime: time.Now(),
      Socket:         conn,
      Send:           make(chan []byte),
   }

   Manager.Clients[mdId] = client

   common.Log.Infof("用户连接成功:第" + sendFrom + "用户")

   go client.Read()
}


//循环监听websocket连接
func (c *Client) Read() {
   defer func() {
      _ = c.Socket.Close()
   }()
   for {
      messageType, _, err := c.Socket.ReadMessage()
      if err != nil || messageType == websocket.CloseMessage {
         common.Log.Infof("socket连接失败:" + err.Error())
         break
      }

      c.Socket.PongHandler()
   }
}

  • 3、接下来是发发消息了
type WsReq struct {
   Class uint   `json:"class"` //1发消息对人,2发消息对群,3获取历史
   S     string `json:"s"`     //发送人
   R     string `json:"r"`     //接收人
   C     string `json:"c"`     //内容
   HC    int64  `json:"hc"`    //获取多少条历史消息
}
//mongodb持久化的对象
type ContractChatMsgInfo struct {
   SendFromTo string `json:"send_from_to" binding:"required"` // 通话记录
   MsgContent string `json:"msg_content" binding:"required"`  // 内容
   MsgDate    string `json:"msg_date"`                        // 创建时间
   Code       uint   `json:"code"`
}



  • 4、发消息
// SendMsg
// @description:发消息
// @param c
// @2022-10-31 18:03:47
func SendMsg(c *gin.Context) {
   var req webReq.WsReq
   if err := c.ShouldBind(&req); err != nil {
      response.Fail(c, nil, err.Error())
      return
   }

   if err := common.Validate.Struct(&req); err != nil {
      errStr := err.(validator.ValidationErrors)[0].Translate(common.Trans)
      response.Fail(c, nil, errStr)
      return
   }
   //用于判断是否是陌生人,返回状态1好友 2 陌生人 3 超过3条
   sFlag := SendMsgStranger(req)

   if sFlag != 1 {
      if sFlag == 2 {
         response.Success(c, nil, "成功")
      }
      if sFlag == 3 {
         response.Success(c, nil, "超过3条了")
      }
      return
   }

   //无论是人还是群,先持久化
   var repo model.ContractChatMsgInfo
   repo.Code = req.Class
   repo.MsgContent = req.C
   repo.MsgDate = time.Unix(time.Now().Unix(), 0).Format("2006-01-02 15:04:05")
   repo.SendFromTo = req.S + "@" + req.R
   common.AddOne(repo, "school_chat_history")
   common.Log.Infof("持久化成功:" + req.S + "->" + req.R)

   var RIds []web.Friend

   //判断发的是群还是人
   if req.Class == 1 {
      var r web.Friend
      a, _ := strconv.Atoi(req.R)
      r.Me = uint32(a)
      RIds = append(RIds, r)
   } else {
      reqR, _ := strconv.Atoi(req.R)
      e := common.DB.Where("he =? ", reqR).Find(&RIds).Error
      if e != nil {
         common.Log.Error("sql查询失败:" + e.Error())
      }
   }

   for i := 0; i < len(RIds); i++ {
      mid := strconv.Itoa(int(RIds[i].Me))
      //说明不在线
      if Manager.Clients[mid] == nil {
         reqS, _ := strconv.Atoi(req.S)
         reqR, _ := strconv.Atoi(req.R)
         //我-ta 未读消息加一
         if req.Class == 1 {
            common.DB.Exec("update friend set never_read = never_read+1 where me = ? and he =?", reqS, reqR)
         }
         //我-群 所在的群未读消息加一
         if req.Class == 2 {
            common.DB.Exec("update friend set never_read = never_read+1 where me = ? and he =?", RIds[i].Me, reqR)
         }
         continue
      }
        //更新websocket自己的活跃时间
      Manager.Clients[mid].LastOnlineTime = time.Now()
      var t = Manager.Clients[mid]
      //序列化发送
      msg, _ := json.Marshal(req.C)
      err := t.Socket.WriteMessage(websocket.TextMessage, msg)
      if err != nil {
         common.Log.Infof("socket写入失败:" + err.Error())
      }
   }

   common.Log.Infof("用户发送消息成功:" + req.S + "->" + req.R)
   response.Success(c, nil, "成功")

}
// SendMsgStranger
// @description: 用于判断是否是陌生人,返回状态1好友 2 陌生人 3 超过3条  
// @param req
// @return bool
// @2022-11-01 15:45:01
func SendMsgStranger(req webReq.WsReq) int {
   var c1, c2 int64
   //查表,双方有加好友吗
   common.DB.Table("friend").Where("me =? and he = ?", req.S, req.R).Count(&c1)
   common.DB.Table("friend").Where("me =? and he = ?", req.R, req.S).Count(&c2)
   if c1 != 0 || c2 != 0 {
      //是好友
      return 1
   } else {
      //查MongoDB,陌生人只能发3条
      c := common.MongoCount(req.S, req.R)
      if c == 3 {
         return 3
      }
      if c < 3 {
         if Manager.Clients[req.R] != nil {
            var t = Manager.Clients[req.R]
            msg, _ := json.Marshal(req.C)
            err := t.Socket.WriteMessage(websocket.TextMessage, msg)
            if err != nil {
               common.Log.Infof("socket写入失败:" + err.Error())
            }
         }
         var repo model.ContractChatMsgInfo
         repo.Code = req.Class
         repo.MsgContent = req.C
         repo.MsgDate = time.Unix(time.Now().Unix(), 0).Format("2006-01-02 15:04:05")
         repo.SendFromTo = req.S + "@" + req.R
         common.AddOne(repo, "school_chat_history")

      }
      common.Log.Infof("陌生人消息持久化成功:" + req.S + "->" + req.R)
      return 2
   }
}
  • 5、获取历史消息
// GetHistoryMsg
// @description: 获取历史消息
// @param c
// @2022-11-10 15:14:45
func GetHistoryMsg(c *gin.Context) {
   var req webReq.WsReq
   if err := c.ShouldBind(&req); err != nil {
      response.Fail(c, nil, err.Error())
      return
   }

   if err := common.Validate.Struct(&req); err != nil {
      errStr := err.(validator.ValidationErrors)[0].Translate(common.Trans)
      response.Fail(c, nil, errStr)
      return
   }

   var RIds web.Friend
   e := common.DB.Where("he =? ", req.R).Find(&RIds).Error
   if e != nil {

   }
   //判断是查个人还是群
   var item interface{}
   if req.Class == 1 {
      item = common.GetList(bson.M{"$or": []bson.M{{"sendfromto": req.S + "@" + req.R}, {"sendfromto": req.R + "@" + req.S}}}, req.HC)
   }
   //群聊
   if req.Class == 2 {
      item = common.GetList(bson.M{"sendfromto": bson.M{"$regex": "@" + req.R + "$"}}, req.HC)
   }

   // 多少未读
   var r model.HRep
   var hc model.HC
   reqS, _ := strconv.Atoi(req.S)
   reqR, _ := strconv.Atoi(req.R)
   common.DB.Table("friend").Where("me = ? and he = ?", reqS, reqR).Find(&hc)

   r.Date = item
   r.NeverRead = hc.NeverRead

   // 查完置为0
   common.DB.Exec("update friend set never_read = 0 where me = ? and he =?", reqS, reqR)
   response.Response(c, 200, 200, r, "ok")

}
  • 6、MongoDB的方法
// GetList 获取多条数据
func GetList(m bson.M, size int64) (rep []ContractChatMsgInfo) {
   collection = db.Collection("school_chat_history")
   findOptions := options.Find()
   findOptions.SetLimit(size)
   //倒序查询
   m2 := make(map[string]int)
   m2["msgdate"] = -1
   findOptions.SetSort(m2)
   cur, err := collection.Find(context.Background(), m, findOptions)
   if err != nil {
      Log.Infof(err.Error())
   }
   if err := cur.Err(); err != nil {
      Log.Infof(err.Error())
   }
   err = cur.All(context.Background(), &rep)
   if err != nil {
      Log.Infof(err.Error())
   }
   _ = cur.Close(context.Background())
   return rep
}


func MongoCount(s, r string) int64 {
   collection = db.Collection("school_chat_history")
   var b = bson.M{"sendfromto": s + "@" + r}
   count, err := collection.CountDocuments(context.Background(), b)
   if err != nil {
      Log.Error(err.Error())
   }
   return count

}
  • 7、服务端主动关闭连接,节省资源
import (
   "github.com/robfig/cron"
   "go-web-mini/common"
   "sync"
   "time"
)

var lock sync.Mutex

// Time
// @description: 用于给全局的websocket主动关闭
// @2022-10-18 13:53:53

func WebsocketRegularCleaning() {
   c := cron.New()
   // 给对象增加定时任务
   err := c.AddFunc("@every 50000s", func() {
      myCron()
   })
   if err != nil {
      return
   }
   c.Start()
}

func myCron() {
   if len(Manager.Clients) == 0 {
   } else {
      lock.Lock()
      for k, v := range Manager.Clients {

         timeFlag, _ := time.ParseDuration("3000s")
         timeLength := time.Now().Sub(v.LastOnlineTime)

         if timeLength > timeFlag {
            err := v.Socket.Close()
            if err != nil {
               common.Log.Infof("socker关闭失败:" + err.Error())
            } else {
               delete(Manager.Clients, k)
               Manager.Clients[k] = nil
            }
         }
      }
      lock.Unlock()
   }
}
预览

用apifox发送post,postman测试接收websocket推过来的消息

图片.png

图片.png