当前位置: 首页 > news >正文

Golang实现完整聊天室(内附源码)

项目github地址:
由于我们项目的需要,我就研究了一下关于websocket的相关内容,去实现一个聊天室的功能。
经过几天的探索,现在使用Gin框架实现了一个完整的聊天室+消息实时通知系统。有什么不完善的地方还请大佬指正。

用到的技术

websocket、gin、mysql、redis、协程、通道

实现思路

说到聊天室可以有多种方法实现,例如:使用单纯的MySQL也可以实现,但是为什么要选择使用websocket去实现呢?有什么优势呢?
websocket是基于TCP/IP,独立的HTTP协议的双向通信协议,这就使实时的消息通知成为可能, 同时又符合Go高效处理高并发的语言特点,结合聊天室又是高并发的,所以采取的室websocket进行消息的转接,MySQL持久化聊天消息,redis用于做一些判断。
首先用户在进入App时,客户端和服务端建立一个websocket连接,并开启一个通道。
当服务端收到客户端的消息后,将消息写入通道里,服务端监听通道的消息,并将消息取出,使用接收人的websocket连接将消息广播到接收人那里。

实现代码

下面开始实现:
创建模型,用于关系的确立及数据的传输

//数据库存储消息结构体,用于持久化历史记录
type ChatMessage struct {gorm.ModelDirection   string //这条消息是从谁发给谁的SendID      int    //发送者idRecipientID int    //接受者idGroupID     string //群id,该消息要发到哪个群里面去Content     string //内容Read        bool   //是否读了这条消息
}//群聊结构体
type Group struct {ID           string ` gorm:"primaryKey"` //群idCreatedAt    time.TimeUpdatedAt    time.TimeDeletedAt    gorm.DeletedAt `gorm:"index"`GroupName    string         `json:"group_name"`    //群名GroupContent string         `json:"group_content"` //群签名GroupIcon    string         `json:"group_icon"`    //群头像GroupNum     int            //群人数GroupOwnerId int            //群主idUsers        []User         `gorm:"many2many:users_groups;"` //群成员
}type UsersGroup struct {GroupId string `json:"group_id"`UserId  int    `json:"user_id"`
}// 用于处理请求后返回一些数据
type ReplyMsg struct {From    string `json:"from"`Code    int    `json:"code"`Content string `json:"content"`
}// 发送消息的类型
type SendMsg struct {Type        int    `json:"type"`RecipientID int    `json:"recipient_id"` //接受者idContent     string `json:"content"`
}// 用户类
type Client struct {ID          string          //消息的去向RecipientID int             //接受者idSendID      int             //发送人的idGroupID     string          //群聊idSocket      *websocket.Conn //websocket连接对象Send        chan []byte     //发送消息用的管道
}// 广播类,包括广播内容和源用户
type Broadcast struct {Client  *ClientMessage []byteType    int
}// 用户管理,用于管理用户的连接及断开连接
type ClientManager struct {Clients    map[string]*ClientBroadcast  chan *BroadcastReply      chan *ClientRegister   chan *ClientUnregister chan *Client
}//创建一个用户管理对象
var Manager = ClientManager{Clients:    make(map[string]*Client), // 参与连接的用户,出于性能的考虑,需要设置最大连接数Broadcast:  make(chan *Broadcast),Register:   make(chan *Client), //新建立的连接访放入这里面Reply:      make(chan *Client),Unregister: make(chan *Client), //新断开的连接放入这里面
}

创建连接

func WsHandle(c *gin.Context) {myid := c.Query("myid")userid, err := strconv.Atoi(myid)if err != nil {zap.L().Error("转换失败", zap.Error(err))ResponseError(c, CodeParamError)}//将http协议升级为ws协议conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return true}}).Upgrade(c.Writer, c.Request, nil)if err != nil {http.NotFound(c.Writer, c.Request)return}//创建一个用户客户端实例,用于记录该用户的连接信息client := new(model.Client)client = &model.Client{ID:     myid + "->",SendID: userid,Socket: conn,Send:   make(chan []byte),}//使用管道将实例注册到用户管理上model.Manager.Register <- client//开启两个协程用于读写消息go Read(client)go Write(client)
}//用于读管道中的数据
func Read(c *model.Client) {//结束把通道关闭defer func() {model.Manager.Unregister <- c//关闭连接_ = c.Socket.Close()}()for {//先测试一下连接能不能连上c.Socket.PongHandler()sendMsg := new(model.SendMsg)err := c.Socket.ReadJSON(sendMsg)c.RecipientID = sendMsg.RecipientIDif err != nil {zap.L().Error("数据格式不正确", zap.Error(err))model.Manager.Unregister <- c_ = c.Socket.Close()return}//根据要发送的消息类型去判断怎么处理//消息类型的后端调度switch sendMsg.Type {case 1: //私信SingleChat(c, sendMsg)case 2: //获取未读消息UnreadMessages(c)case 3: //拉取历史消息记录HistoryMsg(c, sendMsg)case 4: //群聊消息广播GroupChat(c, sendMsg)}}
}//用于将数据写进管道中
func Write(c *model.Client) {defer func() {_ = c.Socket.Close()}()for {select {//读取管道里面的信息case message, ok := <-c.Send://连接不到就返回消息if !ok {_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})return}fmt.Println(c.ID+"接收消息:", string(message))replyMsg := model.ReplyMsg{Code:    int(CodeConnectionSuccess),Content: fmt.Sprintf("%s", string(message)),}msg, _ := json.Marshal(replyMsg)//将接收的消息发送到对应的websocket连接里rwLocker.Lock()_ = c.Socket.WriteMessage(websocket.TextMessage, msg)rwLocker.Unlock()}}
}

后端调度

//聊天的后端调度逻辑
//单聊
func SingleChat(c *model.Client, sendMsg *model.SendMsg) {//获取当前用户发出到固定用户的消息r1, _ := redis.REDIS.Get(context.Background(), c.ID).Result()//从redis中取出固定用户发给当前用户的消息id := CreateId(strconv.Itoa(c.RecipientID), strconv.Itoa(c.SendID))r2, _ := redis.REDIS.Get(context.Background(), id).Result()//根据redis的结果去做未关注聊天次数限制if r2 >= "3" && r1 == "" {ResponseWebSocket(c.Socket, CodeLimiteTimes, "未相互关注,限制聊天次数")return} else {//将消息写入redisredis.REDIS.Incr(context.Background(), c.ID)//设置消息的过期时间_, _ = redis.REDIS.Expire(context.Background(), c.ID, time.Hour*24*30*3).Result()}fmt.Println(c.ID+"发送消息:", sendMsg.Content)//将消息广播出去model.Manager.Broadcast <- &model.Broadcast{Client:  c,Message: []byte(sendMsg.Content),}
}//查看未读消息
func UnreadMessages(c *model.Client) {//获取数据库中的未读消息msgs, err := mysql.GetMessageUnread(c.SendID)if err != nil {ResponseWebSocket(c.Socket, CodeServerBusy, "服务繁忙")}for i, msg := range msgs {replyMsg := model.ReplyMsg{From:    msg.Direction,Content: msg.Content,}message, _ := json.Marshal(replyMsg)_ = c.Socket.WriteMessage(websocket.TextMessage, message)//发送完后将消息设为已读msgs[i].Read = trueerr := mysql.UpdateMessage(&msgs[i])if err != nil {ResponseWebSocket(c.Socket, CodeServerBusy, "服务繁忙")}}
}//拉取历史消息记录
func HistoryMsg(c *model.Client, sendMsg *model.SendMsg) {//拿到传过来的时间timeT := TimeStringToGoTime(sendMsg.Content)//查找聊天记录//做一个分页处理,一次查询十条数据,根据时间去限制次数//别人发给当前用户的direction := CreateId(strconv.Itoa(c.RecipientID), strconv.Itoa(c.SendID))//当前用户发出的id := CreateId(strconv.Itoa(c.SendID), strconv.Itoa(c.RecipientID))msgs, err := mysql.GetHistoryMsg(direction, id, timeT, 10)if err != nil {ResponseWebSocket(c.Socket, CodeServerBusy, "服务繁忙")}//把消息写给用户for _, msg := range *msgs {replyMsg := model.ReplyMsg{From:    msg.Direction,Content: msg.Content,}message, _ := json.Marshal(replyMsg)_ = c.Socket.WriteMessage(websocket.TextMessage, message)//发送完后将消息设为已读if err != nil {ResponseWebSocket(c.Socket, CodeServerBusy, "服务繁忙")}}
}//群聊消息广播
func GroupChat(c *model.Client, sendMsg *model.SendMsg) {//根据消息类型判断是否为群聊消息//先去数据库查询该群下的所有用户users, err := mysql.GetAllGroupUser(strconv.Itoa(sendMsg.RecipientID))if err != nil {ResponseWebSocket(c.Socket, CodeServerBusy, "服务繁忙")}//向群里面的用户广播消息for _, user := range users {//获取群里每个用户的连接if int(user.ID) == c.SendID {continue}c.ID = strconv.Itoa(c.SendID) + "->"c.GroupID = strconv.Itoa(sendMsg.RecipientID)c.RecipientID = int(user.ID)model.Manager.Broadcast <- &model.Broadcast{Client:  c,Message: []byte(sendMsg.Content),}}
}

转发消息

//用于在启动时进行监听
func Start(manager *model.ClientManager) {for {fmt.Println("<-----监听通信管道----->")select {//监测model.Manager.Register这个的变化,有新的东西加入管道时会被监听到,从而建立连接case conn := <-model.Manager.Register:fmt.Println("建立新连接:", conn.ID)//将新建立的连接加入到用户管理的map中,用于记录连接对象,以连接人的id为键,以连接对象为值model.Manager.Clients[conn.ID] = conn//返回成功信息controller.ResponseWebSocket(conn.Socket, controller.CodeConnectionSuccess, "已连接至服务器")//断开连接,监测到变化,有用户断开连接case conn := <-model.Manager.Unregister:fmt.Println("连接失败:", conn.ID)if _, ok := model.Manager.Clients[conn.ID]; ok {controller.ResponseWebSocket(conn.Socket, controller.CodeConnectionBreak, "连接已断开")}//关闭当前用户使用的管道//close(conn.Send)//删除用户管理中的已连接的用户delete(model.Manager.Clients, conn.ID)case broadcast := <-model.Manager.Broadcast: //广播消息message := broadcast.MessagerecipientID := broadcast.Client.RecipientID//给一个变量用于确定状态flag := falsecontentid := createId(strconv.Itoa(broadcast.Client.SendID), strconv.Itoa(recipientID))rID := strconv.Itoa(recipientID) + "->"//遍历客户端连接map,查找该用户有没有在线,判断的是对方的连接例如:1要向2发消息,我现在是用户1,那么我需要判断2->1是否存在在用户管理中for id, conn := range model.Manager.Clients {//如果找不到就说明用户不在线,与接收人的id比较if id != rID {continue}//走到这一步,就说明用户在线,就把消息放入管道里面select {case conn.Send <- message:flag = truedefault: //否则就把该连接从用户管理中删除close(conn.Send)delete(model.Manager.Clients, conn.ID)}}//判断完之后就把将消息发给用户if flag {fmt.Println("用户在线应答")controller.ResponseWebSocket(model.Manager.Clients[rID].Socket, controller.CodeConnectionSuccess, string(message))//把消息插到数据库中msg := model.ChatMessage{Direction:   contentid,SendID:      broadcast.Client.SendID,RecipientID: recipientID,GroupID:     broadcast.Client.GroupID,Content:     string(message),Read:        true,}err := mysql.DB.Create(&msg).Errorif err != nil {zap.L().Error("在线发送消息出现了错误", zap.Error(err))}} else { //如果不在线controller.ResponseWebSocket(broadcast.Client.Socket, controller.CodeConnectionSuccess, "对方不在线")//把消息插到数据库中msg := model.ChatMessage{Direction:   contentid,SendID:      broadcast.Client.SendID,RecipientID: recipientID,GroupID:     broadcast.Client.GroupID,Content:     string(message),Read:        false,}err := mysql.DB.Create(&msg).Errorif err != nil {zap.L().Error("不在线发送消息出现了错误", zap.Error(err))}}}}}func createId(uid, toUid string) string {return uid + "->" + toUid
}

相关文章:

Golang实现完整聊天室(内附源码)

项目github地址&#xff1a; 由于我们项目的需要&#xff0c;我就研究了一下关于websocket的相关内容&#xff0c;去实现一个聊天室的功能。 经过几天的探索&#xff0c;现在使用Gin框架实现了一个完整的聊天室消息实时通知系统。有什么不完善的地方还请大佬指正。 用到的技术…...

WSL2 ubuntu子系统OpenCV调用本机摄像头的RTSP视频流做开发测试

文章目录 前言一、Ubuntu安装opencv库二、启动 Windows 本机的 RTSP 视频流下载解压 EasyDarwin查看本机摄像头设备开始推流 三、在ubuntu 终端编写代码创建目录及文件创建CMakeLists.txt文件启动 cmake 配置并构建 四、结果展示启动图形界面在图形界面打开终端找到 rtsp_demo运…...

20230814让惠普(HP)锐14 新AMD锐龙电脑不联网进WIN11进系统

20230814让惠普(HP)锐14 新AMD锐龙电脑不联网进WIN11进系统 2023/8/14 17:19 win11系统无法跳过联网 https://www.xpwin7.com/jiaocheng/28499.html Win11开机联网跳过不了怎么办&#xff1f;Win11开机联网跳过不了解决方法 Win11开机联网跳过不了怎么办&#xff1f;Win11开机…...

基于ScrollView的下拉刷新

基于ScrollView的下拉刷新 组件使用 组件 import React, {useState} from react; import {ScrollView, RefreshControl, Platform} from react-native;const RefreshComponent ({children, onRefresh, onScroll}) > {const [refreshing, setRefreshing] useState(false);…...

强训第31天

选择 传输层叫段 网络层叫包 链路层叫帧 A 2^16-2 C D C 70都没收到&#xff0c;确认号代表你该从这个号开始发给我了&#xff0c;所以发70而不是71 B D C 248&123120 OSI 物理层 数据链路层 网络层 传输层 会话层 表示层 应用层 C 记一下304读取浏览器缓存 502错误网关 编…...

什么是Java中的策略模式?

Java中的策略模式是一种行为设计模式&#xff0c;它允许您在不改变客户端代码的情况下&#xff0c;在运行时动态地切换行为。这是一种非常有用的模式&#xff0c;因为它允许您在运行时根据需要更改算法或行为。 策略模式通常涉及到一个或多个策略类&#xff0c;每个策略类都实…...

【Visual Studio Code】--- Win11 安装 VS Code 超详细

Win11 安装 VS Code 超详细 概述一、下载 Vscode二、安装 Vscode 概述 一个好的文章能够帮助开发者完成更便捷、更快速的开发。书山有路勤为径&#xff0c;学海无涯苦作舟。我是秋知叶i、期望每一个阅读了我的文章的开发者都能够有所成长。 一、下载 Vscode Vscode官网 二、…...

每天一道leetcode:797. 所有可能的路径(图论中等深度优先遍历)

今日份题目&#xff1a; 给你一个有 n 个节点的 有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不要求按特定顺序&#xff09; graph[i] 是一个从节点 i 可以访问的所有节点的列表&#xff08;即从节点 i 到节…...

创建预留成本中心与指定工厂不一致

创建预留成本中心与指定工厂不一致 这种情况SAP会警告提示&#xff0c;可以强制通过。 如果公司不允许跨公司领料&#xff0c;可以将消息号 M7517的类型从W改为为E tcode&#xff1a;OMCQ SPRO->物料管理->库存管理和实际库存->定义系统消息的属性->系统信息设置...

SCF金融公链新加坡启动会 创新驱动未来

新加坡迎来一场引人瞩目的金融科技盛会&#xff0c;SCF金融公链启动会于2023年8月13日盛大举行。这一受瞩目的活动将为金融科技领域注入新的活力&#xff0c;并为广大投资者、合作伙伴以及关注区块链发展的人士提供一个难得的交流平台。 在SCF金融公链启动会上&#xff0c; Wil…...

希尔排序【Java算法】

文章目录 1. 概念2. 思路3. 代码实现 1. 概念 希尔排序也是一种插入排序&#xff0c;它是简单插入排序经过改进之后的一个更高效的版本&#xff0c;也称为缩小增量排序。希尔排序在数组中采用跳跃式分组的策略&#xff0c;通过某个增量将数组元素划分为若干组&#xff0c;然后分…...

互联网发展历程:从布线到无线,AC/AP的崭新时代

互联网的发展&#xff0c;一直在追求更便捷、更灵活的连接方式。在网络的早期&#xff0c;布线问题常常让人头疼。一项革命性的技术应运而生&#xff0c;那就是“无线AC/AP”。 布线问题的烦恼&#xff1a;繁琐的布线 早期网络的布线工作常常耗费时间和精力&#xff0c;尤其在大…...

Vue3 Axios网络请求简单应用

cd 到项目 安装Axios&#xff1a;cnpm install --save axios post传递参数 需要安装querystring 用于转换参数格式&#xff1a;cnpm install --save querystring 运行示例&#xff1a; 后台接口&#xff1a; GetTestData.java package com.csdnts.api;import java.io.IOExce…...

day-18 代码随想录算法训练营(19)二叉树 part05

513.找树左下角的值 思路一&#xff1a;层序遍历&#xff0c;每一层判断是不是最后一层&#xff0c;是的话直接返回第一个; 如何判断是不是最后一层呢&#xff0c;首先队列头部&#xff0c;其次记录左右子节点都没有的节点数是不是等于que.size()&#xff1b;或…...

【数据结构OJ题】移除链表元素

原题链接&#xff1a;https://leetcode.cn/problems/remove-linked-list-elements/description/ 1. 题目描述 2. 思路分析 我们可以定义一个结构体指针变量cur&#xff0c;让cur一开始指向头结点&#xff0c;同时定义一个结构体指针prev&#xff0c;令prev初始化为空指针NULL…...

centos 安装 virtualbox

参考 https://phoenixnap.com/kb/how-to-install-virtualbox-centos-7 遇到 Gpg Keys Failue 这样解决 将 rpm 包下载到本地 –disablerepovirtualbox sudo yum --disablerepovirtualbox localinstall VirtualBox-7.0-7.0.10_158379_el7-1.x86_64 failure: repodata/repomd…...

Java8之Optional类的基本使用

文章目录 一、简介二、常见的Optional用法&#xff1a;1、创建Optional对象&#xff1a;1.1 使用of()方法&#xff1a;1.2 使用ofNullable()方法&#xff1a;1.3 使用empty()方法&#xff1a; 2、判断Optional是否包含值&#xff1a;2.1 使用isPresent()方法&#xff1a; 3、获…...

LinuxPTP时间同步

参考文献&#xff1a; http://linuxptp.sourceforge.net/ 0、硬件支持 查看网卡是否支持软硬件时间戳&#xff1a; sudo ethtool -T eno1 Time stamping parameters for eno1: Time stamping parameters for eno1: Capabilities: hardware-transmit (SOF_TIMESTAMPIN…...

【Django】Task1安装python环境及运行项目

【Django】Task1安装python环境及运行项目 写在最前 8月份Datawhale组队学习&#xff0c;在这个群除我佬的时代&#xff0c;写一下blog记录学习过程。 参考资源&#xff1a; 学习项目github&#xff1a;https://github.com/Joe-2002/sweettalk-django4.2 队长博客&#xff1a…...

00 - 环境配置

查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;GIT常用场景- 目录 文章目录 1. 环境说明2. 安装配置2.1 配置user信息2.2 config的三个作用域 3. 建git仓库3.1 把已有的项目代码纳入git管理3.2 新建的项目直接用git管理3.3 配置local的user和email3.4 优先级&…...

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误

HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误&#xff0c;它们的含义、原因和解决方法都有显著区别。以下是详细对比&#xff1a; 1. HTTP 406 (Not Acceptable) 含义&#xff1a; 客户端请求的内容类型与服务器支持的内容类型不匹…...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

【Linux】C语言执行shell指令

在C语言中执行Shell指令 在C语言中&#xff0c;有几种方法可以执行Shell指令&#xff1a; 1. 使用system()函数 这是最简单的方法&#xff0c;包含在stdlib.h头文件中&#xff1a; #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止

<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet&#xff1a; https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...

定时器任务——若依源码分析

分析util包下面的工具类schedule utils&#xff1a; ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类&#xff0c;封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz&#xff0c;先构建任务的 JobD…...

el-switch文字内置

el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...

uniapp中使用aixos 报错

问题&#xff1a; 在uniapp中使用aixos&#xff0c;运行后报如下错误&#xff1a; AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...