场景:单个群聊实现
用户操作:加入群聊,退出群聊,发送消息,拉取历史信息
思路:
- 结构:每个用户是一个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