技术:MongoDB、mysql、golang
有如下功能
- 单聊
- 群聊
- 获取历史消息
- 获取历史,发消息都是用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判断是单聊还是群聊
群聊也是相当于一个朋友,在数据库存储中
思路
- 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推过来的消息