最近使用 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 即可

聊天截图(后台界面)

go语言聊天室 golang 聊天软件_golang websocket 聊天

小程序 界面

go语言聊天室 golang 聊天软件_go语言聊天室_02