最近使用 gin 在重构一个商城项目,需要加客服聊天的功能,因此我使用了websocket来实现了,websocket 框架的选择当然是 github.com/gorilla/websocket
1.实现的思路
由于之前写python的时候接触过websocket,但是当时是使用类似于广播的机制的,没有做过一对一的聊天.但是既然知道了广播,那一对一也就不是什么难事了首先客户端连接上来时,服务器会实例化每个客户端的连接,并且保存这些连接,这时候只需要前端发送消息时,带有联系人的id 这样就可以根据id 找到联系人发送消息了
2.Demo 示例
下面是gin 实现的一个接口,使用 ws协议 连接该接口,然后就可以保存一个ws的连接
// 把 http 升级为 ws
func Login(c *gin.Context) {
tid := c.Query("tid")
userID , err := strconv.ParseInt(tid, 10, 64)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 10, "arg-err": db.InvalidArgument})
return
}
Logger.Info(" ------ 请求头 ------", c.Request.Header)
Logger.Info("ws 连接请求")
userInfo, err := db.FindUser(userID)
if err != nil {
Logger.Info("ws 连接失败,找不到用户信息", err)
c.JSON(http.StatusOK, gin.H{"code": 10, "arg-err": db.InvalidArgument})
return
}
conn, err := Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
Logger.Error("http 协议升级为ws 协议失败:", err)
return
}
uniqueID := utils.UUID()
wsClient := &Client{ID: utils.UUID(), Socket: conn, Send: make(chan []byte), UserInfo:userInfo, ConnID:uniqueID}
go wsClient.Read()
Manager.Register <- wsClient
Logger.Infof("ws 登陆成功 ip %v 用户id %v", conn.RemoteAddr(), *wsClient.UserInfo.TID)
}
聊天功能demo
聊天功能主要是每个连接开了一个go 协程去检查该连接是否有消息,如果有消息 则检查它要发给哪个用户,找到那个用户的连接,发过去即可
func (c *Client) Read() {
defer func() {
Manager.Unregister <- c
c.Socket.Close()
}()
for {
Logger.Infof("用户 %v 等待消息中....", *c.UserInfo.TID)
_, message, err := c.Socket.ReadMessage()
if err != nil {
Logger.Error("读取消息失败 ", err)
Manager.Unregister <- c
c.Socket.Close()
break
}
Logger.Info("====== ws 消息 ====== ", string(message))
var content Message
err = json.Unmarshal(message, &content)
if err != nil {
Logger.Error("接收消息失败 ", err)
continue
}
if content.Content == "logout" {
Manager.Unregister <- c
c.Socket.Close()
break
}
if (content.Recipient == 0 || len(content.Content) == 0) && len(content.Image) < 0 {
Logger.Error("无效的消息 ")
continue
}
// 保存聊天记录
unRead := db.MessageUnRead
msgType := content.Type
chat := &db.Chat{}
fromUser := c.UserInfo.TID
chat.FromUser = fromUser
chat.ToUser = &content.Recipient
chat.Content = &content.Content
chat.Image = &content.Image
chat.Status = &unRead
chat.Type = &msgType
db.SaveChat(chat)
now := time.Now()
nowTimeStamp := db.JSONTime{Time:now}
content.MsgID = *chat.TID
content.Timestamp = nowTimeStamp
content.Sender = *fromUser
// 转发给另外一端
Logger.Infof("用户 %v 发消息 %v 给另外一个用户 %v ", *fromUser, content.Content, content.Recipient)
Manager.ChatTo(content.Recipient, &content)
}
}
func (manager *ClientManager) ChatTo(userID int64, msg *Message) {
manager.Clients.Range(func(k, v interface{}) bool {
if v != nil {
conn := v.(*Client)
if *conn.UserInfo.TID == userID {
message, err := json.Marshal(msg)
if err != nil {
Logger.Error("聊天失败")
return true
}
Logger.Info("发送给 ", *conn.UserInfo.TID)
err = conn.Socket.WriteMessage(websocket.TextMessage, message)
if err != nil {
Logger.Error("消息转发失败 ", err)
}
}
}
return true
})
}
关于离线消息的处理
当联系人不在线时,我的处理方案是将聊天数据保存下来标记该消息未读,提供一个接口查询用户未读消息,当下次用户登陆时前端发http 请求未读消息即可
关于图片发送
这里我的处理方案是,写一个接口提供给前端上传图片上传成功时返回图片路径,前端发来的聊天内容只有图片的url,后续聊天图片显示通过url 请求图片即可
上线遇到的一个坑
由于我们是使用了nginx 做反向代理,刚上线时会遇到错误 the client is not using websocket protocol : 'upgrade' token not found in Conntion header 这里只需要改一下nginx 配置加入 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 然后reload 一下nginx 即可
聊天截图(后台界面)
小程序 界面