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地址: 由于我们项目的需要,我就研究了一下关于websocket的相关内容,去实现一个聊天室的功能。 经过几天的探索,现在使用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开机联网跳过不了怎么办?Win11开机联网跳过不了解决方法 Win11开机联网跳过不了怎么办?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都没收到,确认号代表你该从这个号开始发给我了,所以发70而不是71 B D C 248&123120 OSI 物理层 数据链路层 网络层 传输层 会话层 表示层 应用层 C 记一下304读取浏览器缓存 502错误网关 编…...
什么是Java中的策略模式?
Java中的策略模式是一种行为设计模式,它允许您在不改变客户端代码的情况下,在运行时动态地切换行为。这是一种非常有用的模式,因为它允许您在运行时根据需要更改算法或行为。 策略模式通常涉及到一个或多个策略类,每个策略类都实…...

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

每天一道leetcode:797. 所有可能的路径(图论中等深度优先遍历)
今日份题目: 给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序) graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节…...

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

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

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

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

Vue3 Axios网络请求简单应用
cd 到项目 安装Axios:cnpm install --save axios post传递参数 需要安装querystring 用于转换参数格式:cnpm install --save querystring 运行示例: 后台接口: GetTestData.java package com.csdnts.api;import java.io.IOExce…...
day-18 代码随想录算法训练营(19)二叉树 part05
513.找树左下角的值 思路一:层序遍历,每一层判断是不是最后一层,是的话直接返回第一个; 如何判断是不是最后一层呢,首先队列头部,其次记录左右子节点都没有的节点数是不是等于que.size();或…...

【数据结构OJ题】移除链表元素
原题链接:https://leetcode.cn/problems/remove-linked-list-elements/description/ 1. 题目描述 2. 思路分析 我们可以定义一个结构体指针变量cur,让cur一开始指向头结点,同时定义一个结构体指针prev,令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用法:1、创建Optional对象:1.1 使用of()方法:1.2 使用ofNullable()方法:1.3 使用empty()方法: 2、判断Optional是否包含值:2.1 使用isPresent()方法: 3、获…...
LinuxPTP时间同步
参考文献: http://linuxptp.sourceforge.net/ 0、硬件支持 查看网卡是否支持软硬件时间戳: 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组队学习,在这个群除我佬的时代,写一下blog记录学习过程。 参考资源: 学习项目github:https://github.com/Joe-2002/sweettalk-django4.2 队长博客:…...

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

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

佰力博科技与您探讨热释电测量的几种方法
热释电的测量主要涉及热释电系数的测定,这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中,积分电荷法最为常用,其原理是通过测量在电容器上积累的热释电电荷,从而确定热释电系数…...

人机融合智能 | “人智交互”跨学科新领域
本文系统地提出基于“以人为中心AI(HCAI)”理念的人-人工智能交互(人智交互)这一跨学科新领域及框架,定义人智交互领域的理念、基本理论和关键问题、方法、开发流程和参与团队等,阐述提出人智交互新领域的意义。然后,提出人智交互研究的三种新范式取向以及它们的意义。最后,总结…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...

负载均衡器》》LVS、Nginx、HAproxy 区别
虚拟主机 先4,后7...
Spring Boot 中实现 HTTPS 加密通信及常见问题排查指南
Spring Boot 中实现 HTTPS 加密通信及常见问题排查指南 在金融行业安全审计中,未启用HTTPS的Web应用被列为高危漏洞。通过正确配置HTTPS,可将中间人攻击风险降低98%——本文将全面解析Spring Boot中HTTPS的实现方案与实战避坑指南。 一、HTTPS 核心原理与…...
基于Java的离散数学题库系统设计与实现:附完整源码与论文
JAVASQL离散数学题库管理系统 一、系统概述 本系统采用Java Swing开发桌面应用,结合SQL Server数据库实现离散数学题库的高效管理。系统支持题型分类(选择题、填空题、判断题等)、难度分级、知识点关联,并提供智能组卷、在线测试…...
CMake系统学习笔记
CMake系统学习笔记 基础操作 最基本的案例 // code #include <iostream>int main() {std::cout << "hello world " << std::endl;return 0; }// CMakeLists.txt cmake_minimum_required(VERSION 3.0)# 定义当前工程名称 project(demo)add_execu…...