gin+vue实现多人在线聊天室

o_o_0_o_0 2022-01-15 19:03:28

一、项目概述

1 项目简介

​ 本项目主要使用Gin+Vue框架实现了一个多人在线聊天室,用户可以通过刷新、长轮询以及Websocket三种方式从聊天室获取消息。

2 框架介绍

2.1 Gin

​ Gin框架是Go世界里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。Gin是一个用Go语言编写的Web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。

2.2 Vue.js

​ Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

二、项目演示

1 登录页面

QQ截图20220115103654

​ 登陆页面,用户可以输入用户名,选择登陆方式。登陆方式主要分为Websocket、Long Polling以及Refresh,点击下方Login进行登录。

2 聊天页面

​ 成功登录后进入聊天页面。页面左上角显示当前聊天室用户人数和登陆方式。

QQ截图20220115105742

​ 主体页面显示所有用户发送的信息以及输入框。输入消息按下回车即可发送消息。

QQ截图20220115105845

三、具体实现

1 实体类

1.1 聊天室
// 聊天室
type Room struct {
    users       map[uid]chan Event     // 当前房间订阅者
    userCount   int                    // 当前房间总人数
    publishChn  chan Event             // 聊天室的消息推送入口
    archive     *list.List             // 历史记录 todo 未持久化 重启失效
    archiveChan chan chan []Event      // 通过接受chan来同步聊天内容
    joinChn     chan chan Subscription // 接收订阅事件的通道 用户加入聊天室后要把历史事件推送给用户
    leaveChn    chan uid               // 用户取消订阅通道 把通道中的历史事件释放并把用户从聊天室用户列表中删除
}
1.2 聊天室事件
// 聊天室事件定义
type Event struct {
    Type      string `json:"type"`      // 事件类型
    User      string `json:"user"`      // 用户名
    Timestamp int64  `json:"timestamp"` // 时间戳
    Text      string `json:"text"`      // 事件内容
    UserCount int    `json:"userCount"` // 房间用户数
}
1.3 用户事件
// 用户订阅
type Subscription struct {
    Id       string       // 用户在聊天室中的ID
    Username string       // 用户名
    Pipe     <-chan Event // 事件接收通道 用户从这个通道接收消息
    EmitCHn  chan Event   // 用户消息推送通道
    LeaveChn chan uid     // 用户离开事件推送
}

2 通信方式

2.1 go channel

​ 聊天室主要通过channel来同步信息。服务端维护一个通道来与用户进行交互,用户加入聊天室即订阅消息通道,聊天室需要把事件放入channel来让用户接收。

// 处理聊天室中的事件
func (r *Room) Serve() {
    for {
        select {
        // 用户加入房间
        case ch := <-r.joinChn:
            chn := make(chan Event, chanSize)
            r.userCount++
            uid := uuid.New().String()
            r.users[uid] = chn
            ch <- Subscription{
                Id:       uid,
                Pipe:     chn,
                EmitCHn:  r.publishChn,
                LeaveChn: r.leaveChn,
            }
            ev := NewEvent(EventTypeSystem, "", "")
            ev.UserCount = r.userCount
            for _, v := range r.users {
                v <- ev
            }
        case arch := <-r.archiveChan:
            var events []Event
            //历史事件
            for e := r.archive.Front(); e != nil; e = e.Next() {
                events = append(events, e.Value.(Event))
            }
            arch <- events
        // 有新的消息
        case event := <-r.publishChn:
            // 推送给所有用户
            event.UserCount = r.userCount
            for _, v := range r.users {
                v <- event
            }
            // 推送消息后,限制本地只保存指定条历史消息
            if r.archive.Len() >= archiveSize {
                r.archive.Remove(r.archive.Front())
            }
            r.archive.PushBack(event)
        // 用户退出房间
        case k := <-r.leaveChn:
            if _, ok := r.users[k]; ok {
                delete(r.users, k)
                r.userCount--
            }
            ev := NewEvent(EventTypeSystem, "", "")
            ev.UserCount = r.userCount
            for _, v := range r.users {
                v <- ev
            }
        }
    }
}
2.2 Long poll

​ 首先,ajax轮询的原理就是让浏览器每隔一定时间就向服务器发送一次请求,询问是否有新的信息。长轮询的原理类似,都是采用轮询的方式,不过采取了阻塞模型,也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回或超时,返回完之后,客户端再次建立连接,周而复始,基于事件的触发,一个事件接一个事件。

// 轮询获取指定时间戳之后的聊天记录
func (longPolling) Archive() gin.HandlerFunc {
    return func(c *gin.Context) {
        lastReceived, _ := strconv.ParseInt(c.Query("ts"), 10, 64)

        var events []core.Event
        // filter archive
        for _, event := range Room.GetArchive() {
            if event.Timestamp > lastReceived {
                events = append(events, event)
            }
        }
        c.JSON(http.StatusOK, events)
    }
}
2.3 WebSocket

​ WebSocket是html5一种新的协议,实现了浏览器与服务器之间的全双工通信,能很好的节省服务器资源与带宽,并在服务器端与浏览器端实现实时通行,他建立在TCP之上, 同http一样,通过tcp来传输数据。只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,服务器端会知道连接的信息,知道客户端关闭请求,同时由服务器主动推送,当有信息需要发送时,直接发送。客户端的连接通过session对象存储,能够实现实时推送。

func (s *ws) Handle() gin.HandlerFunc {
    return func(c *gin.Context) {
        name := c.Query("name")
        conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
        if err != nil {
            panic(err)
        }

        // 加入房间
        evs := Room.GetArchive()
        Room.MsgJoin(name)
        control := Room.Join(name)
        defer control.Leave()

        // 先把历史消息推送出去
        for _, event := range evs {
            if conn.WriteJSON(&event) != nil {
                // 用户断开连接
                return
            }
        }

        // 开启通道监听用户事件然后发送给聊天室
        newMessages := make(chan string)
        go func() {
            var res = struct {
                Msg string `json:"msg"`
            }{}
            for {
                err := conn.ReadJSON(&res)
                if err != nil {
                    // 用户断开连接
                    close(newMessages)
                    return
                }
                newMessages <- res.Msg
            }
        }()

        // 接收消息,在这里阻塞请求,循环退出就表示用户已经断开
        for {
            select {
            case event := <-control.Pipe:
                if conn.WriteJSON(&event) != nil {
                    // 用户断开连接
                    return
                }
            case msg, ok := <-newMessages:
                // 断开连接
                if !ok {
                    return
                }
                control.Say(msg)
            }
        }
    }
}
2.4 前端核心
methods: {
          setUpSocket(socket) {
            socket.onopen = () => {
              this.$message({
                type: "success",
                message: "聊天室连接成功"
              });
            };
            socket.onclose = () => {
              this.$message({
                type: "warning",
                message: "连接断开"
              });
              this.socket = null;
            };
            socket.onmessage = event => {
              let dt = JSON.parse(event.data);
              switch (dt.type) {
                case EventTypeMsg:
                  this.receiveMsg(dt);
                  this.userCount = dt.userCount;
                  console.log(this.userCount);
                  break;
                case EventTypeSystem:
                  this.userCount = dt.userCount;
                  break;
              }
            };
          },
          onExit() {
            window.location.href = "/";
          },
          clearInput() {
            this.msg = "";
            this.$refs.input.focus();
          },
          sendMessage() {
            if (!this.msg) {
              this.$refs.input.focus();
              return;
            }
            const req = JSON.stringify({
              msg: this.msg
            });
            this.socket &&
              (this.socket.send(req),
              (this.msg = ""),
              this.$refs.input.focus());
          },
          receiveMsg(data) {
            this.chatData.push(data);
          }
        }

作者:NP415

...全文
68 回复 点赞 打赏 收藏 举报
写回复
回复
切换为时间正序
请发表友善的回复…
发表回复
相关推荐
发帖
代码中的软件工程
加入

201

社区成员

软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitcode.net/weixin_43549265/se
帖子事件
创建了帖子
2022-01-15 19:03
社区公告
暂无公告