第四章节:WebSocket实现用户一对一聊天

前言

上一章节中我们实现了在WebSocket Server端管理用户和WebSocket连接的绑定和释放,本章节我们尝试实现两个client端之间的消息收发。

消息实体

首先我们需要定义一个统一的消息模型

在models.go中添加Message结构体

go 复制代码
type Message struct {
    ID         int64
    Type       string
    FromUserID int64
    ToUserID   int64
    Data       interface{}
}

消息发送服务

在conn.go中定义接口Sender

go 复制代码
type Sender interface {
    Send(msg model.Message)
}

编写服务实现LocalSender

go 复制代码
type LocalSender struct {
}

func (sender *LocalSender) Send(msg model.Message) error {
    // 获取对方的WebSocket连接
    conn := GetConnManager().FindConn(msg.ToUserID)
    if conn == nil {
        return fmt.Errorf("%v does not online", msg.ToUserID)
    }
    // 发送JSON消息
    return conn.WriteJSON(msg)
}

WebSocket Server端

修改server.go

go 复制代码
package main

import (
    "log"
    "net/http"

    "aoki.com/go-im/src/conn"
    "aoki.com/go-im/src/model"
    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    // Allow Cross Origin
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

var ConnManager *conn.ConnManager = conn.GetConnManager()
var Sender = conn.LocalSender{}

func ws(c *gin.Context) {
    conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        panic(err)
    }
    defer conn.Close()
    user := model.ResolveUser(c.Request)
    if user == nil {
        log.Println("ResolveUser failed...")
        return
    }
    ConnManager.AddConn(user.ID, conn)
    log.Printf("%s connected... \n", user.Name)
    defer OnDisconnect(*user)
    for {
        v := &model.Message{}
        err = conn.ReadJSON(v)
        if err != nil {
            log.Println("Read Json Message Failed", err)
        }
        err = Sender.Send(*v)
        if err != nil {
            log.Println("Send Message Failed", err)
        }
    }
}

func OnDisconnect(user model.User) {
    log.Printf("%v disconnect...\n", user.Name)
    ConnManager.DelConn(user.ID)
}

func main() {
    server := gin.Default()
    server.GET("/ws", ws)
    server.Run("localhost:8848")
}

WebSocket Client端

修改client.go

go 复制代码
package main

import (
    "bufio"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"

    "aoki.com/go-im/src/model"
    "github.com/gorilla/websocket"
)

func main() {
    uri := "ws://localhost:8848/ws"
    // 从命令行获取登录用户ID和Name
    args := os.Args[1:]
    userID, er := strconv.ParseInt(args[0], 10, 64)
    if er != nil {
        log.Fatal("Parse UserId Failed:", er)
    }
    u := model.User{
        ID:   userID,
        Name: args[1],
    }
    header := http.Header{
        "X-TOKEN": []string{u.GenToken()},
    }
    conn, _, err := websocket.DefaultDialer.Dial(uri, header)
    if err != nil {
        log.Fatal("dial failed:", err)
    }
    defer conn.Close()

    go func() {
        for {
            // 读取JSON消息
            m := &model.Message{}
            err = conn.ReadJSON(m)
            if err != nil {
                log.Println("Read WS message failed:", err)
                return
            }
            log.Printf("Received Message %v From %v \n", m.Data, m.FromUserID)
        }
    }()

    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        // 从控制台读取内容,前一段为收信人的用户ID,后一段为消息内容
        str := strings.Split(scanner.Text(), " ")
        ToUserID, er := strconv.ParseInt(str[0], 10, 64)
        if er != nil {
            log.Println("cannot recognize ToUserId ", err)
            break
        }
        m := model.Message{
            FromUserID: u.ID,
            ToUserID:   ToUserID,
            Type:       "Text",
            Data:       str[1],
        }
        if err := conn.WriteJSON(m); err != nil {
            log.Println("Send WS message failed:", err)
        }
    }
    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

模拟交互

在Terminal中启动server.go, 然后新开3个Terminal,分别启动3个client进程

shell 复制代码
go run src/main/client.go 1001 AOKI
go run src/main/client.go 1002 Tony
go run src/main/client.go 1003 Frank

模拟3个用户登录IM服务,然后分别使用3个Terminal向其他用户发送消息,并在Terminal中确认收到的消息

发送方和接收方消息都能对上,消息都能正常接收、发送

小结

本章节我们实现了使用WebSocket让用户间能够正常收发消息,但是还存在一些问题,比如当对方不在线时,消息将会丢失,这是不合常理的,正常应该等对方上线时会立即收到这条未读消息,所以在下一章节我们探讨将消息持久化并做已读未读状态标记。

相关推荐
Walter先生7 小时前
中金所股指期货主力合约自动识别:一个接口搞定 IF/IC/IH 连续合约合成
后端·websocket·架构·实时行情数据源
Kiyra1 天前
Query Rewrite 不是越智能越好:RAG 检索的精确词保护与动态召回
redis·websocket·junit·单元测试·json
DavidTaozhe1 天前
美股api接口的WebSocket订阅如何实现自动重连
网络·websocket·网络协议
派大星的日常1 天前
Java项目使用webSocket给前端推送数据(Java项目使用WebSocket接口给前端传输数据,通道连接未关闭,但是没有数据返回)
网络·websocket·网络协议
想取一个与众不同的名字好难2 天前
QT webSocket接收客户端发送的双目摄像头数据并显示
开发语言·qt·websocket
蜡台2 天前
Vue + SpringBoot 实现 WebSocket 基于 Sec-WebSocket-Protocol 传参鉴权(避坑指南)
vue.js·spring boot·websocket·sec
晓杰'2 天前
从0到1实现 Balatro 游戏后端(1):项目规划与牌型判断实现
后端·websocket·typescript·node.js·游戏开发·项目实战·nestjs
AIFQuant3 天前
贵金属 API 避坑:黄金/白银行情接口常见陷阱(数据漂移、断点、延迟)
开发语言·python·websocket·金融·restful·贵金属
Qt程序员4 天前
从协议到实战:HTTP 反向代理
linux·c++·websocket·nginx·http·反向代理·正向代理
专注VB编程开发20年4 天前
轻量级多进程消息收发模型WEBSOCKET,MQTT
网络·websocket·网络协议