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
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/38019.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/38019.shtml
英文地址,請注明出處:http://en.pswp.cn/news/38019.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用自己的數據利用pytorch搭建全連接神經網絡進行回歸預測

使用自己的數據利用pytorch搭建全連接神經網絡進行回歸預測 1、導入庫2、數據準備3、數據拆分4、數據標準化5、數據轉換6、模型搭建7、模型訓練8、模型預測9、完整代碼 1、導入庫 引入必要的庫&#xff0c;包括PyTorch、Pandas等。 import numpy as np import pandas as pd f…

tp6 RabbitMQ

1、composer 安裝 AMQP 擴展 composer require php-amqplib/php-amqplib 2、RabbitMQ 配置 在 config 目錄下創建 rabbitmq.php 文件 <?php return [host>,port>5672,user>,password>,vhost>,exchange_name > ,queue_name > ,route_key > ,cons…

中國生產了5.07億臺,庫存高達近4億臺?國產手機徹底賣不動了?

統計數據顯示今年上半年中國的手機產量達到5.07億臺&#xff0c;國內市場手機出貨量僅有1.24億臺&#xff0c;都出現了下滑&#xff0c;那么中國手機的產量比銷量多出了3.83億臺&#xff0c;這些手機都成為了庫存&#xff1f; 中國手機市場確實不如早年那么輝煌&#xff0c;201…

【FAQ】安防監控視頻EasyCVR平臺分發的FLV視頻流在VLC中無法播放

眾所周知&#xff0c;TSINGSEE青犀視頻匯聚平臺EasyCVR可支持多協議方式接入&#xff0c;包括主流標準協議國標GB28181、RTSP/Onvif、RTMP等&#xff0c;以及廠家私有協議與SDK接入&#xff0c;包括海康Ehome、海大宇等設備的SDK等。在視頻流的處理與分發上&#xff0c;視頻監控…

P12-Retentive NetWork-RetNet挑戰Transformer

論文地址:https://arxiv.org/abs/2307.08621 目錄 Abstract 一.Introduction 二.Retentive Networks 2.1Retention 2.2Gated Multi-Scale Retention 2.3Overall Architecture of Retention Networks 2.4Relation to and Differences from Previous Methods 三.Experime…

Codeforces Round 892 (Div. 2)(VP)

A //b里放最小值&#xff0c;其他值放c。如果最大值最小值&#xff0c;則無解。 void solve() {int n; cin >> n;vi a(n); liter(x, a) cin >> x; sort(all(a));if (a[0] a[n - 1]){print(-1); return;}vi b, c;for (int i 0; i < sz(a); i){if (a[i] a[0])…

小米基于 Flink 的實時計算資源治理實踐

摘要&#xff1a;本文整理自小米高級軟件工程師張蛟&#xff0c;在 Flink Forward Asia 2022 生產實踐專場的分享。本篇內容主要分為四個部分&#xff1a; 發展現狀與規模框架層治理實踐平臺層治理實踐未來規劃與展望 點擊查看原文視頻 & 演講PPT 一、發展現狀與規模 如上圖…

【03】基礎知識:typescript中的函數

一、typescript 中定義函數的方法 函數聲明法 function test1(): string {return 返回類型為string }function test2(): void {console.log(沒有返回值的方法) }函數表達式/匿名函數 const test3 function(): number {return 1 }二、typescript 中 函數參數寫法 1、typesc…

helm安裝harbor + nerdctl 制作push 鏡像

參考 文章&#xff1a;Helm部署Harbor_helm harbor_風向決定發型丶的博客-CSDN博客 安裝好后使用 nerd containerd對接harbor_containerd 容器 insecure-registries 配置_檸是檸檬的檬的博客-CSDN博客 推送鏡像 Containerd 對接私有鏡像倉庫 Harbor - 知乎 接下來我們來…

麒麟系統相關

創建虛擬機 鏡像下載地址 選擇合適的鏡像&#xff0c;進入引導后注意不要選擇默認的第一條&#xff0c;選擇第二條進入安裝程序。 root密碼修改 使用命令 sudo passwd root 開啟ssh 配置好網絡后發現能ping通&#xff0c;但無法ssh連接&#xff0c;ps -ef | grep ssh 得…

01 qt快速入門

一 qt介紹 1.基本概念 1991年由Qt Company(奇趣)開發的跨平臺C++圖形用戶界面應用程序開發框架,GUI程序和非GUI程序。優點:一套源碼在不同的平臺通過不同的編譯器進行編譯,就可以運行到該平臺上目標機。面向對象的封裝機制來對其接口封裝。 GUI —圖形用戶界面(Graphic…

軟件測試面試題【2023整理版(含答案)】

01、您所熟悉的測試用例設計方法都有哪些&#xff1f;請分別以具體的例子來說明這些方法在測試用例設計工作中的應用。 答&#xff1a;有黑盒和白盒兩種測試種類&#xff0c;黑盒有等價類劃分方法 邊界值分析方法 錯誤推測方法 因果圖方法 判定表驅動分析方法 正交實驗設…

Vue組件之間的傳值匯總

組件之間的傳值 1、父傳子 props 2、父傳子 slot 3、父傳子 不建議用 attrs 4、 子傳父 ref 5、子傳父 emit 6、povide/inject只能在setup的時候用。 7、利用vuex和pinia去實現數據的交互 1、實現代碼App.vue <script setup>import TestProps from ./components/T…

stable-diffusion 模型效果+prompt

摘自個人印象筆記&#xff0c;圖不完整可查看原筆記&#xff1a;https://app.yinxiang.com/fx/55cda0c6-2af5-4d66-bd86-85da79c5574ePrompt運用規則及技巧 &#xff1a; 1. https://publicprompts.art/&#xff08;最適用于OpenArt 線上模型 https://openart.ai/&#xff09;…

【Vue-Router】別名

后臺返回來的路徑名不合理&#xff0c;但多個項目在使用中了&#xff0c;不方便改時可以使用別名。可以有多個或一個。 First.vue <template><h1>First Seciton</h1> </template>Second.vue&#xff0c;Third.vue代碼同理 UserSettings.vue <tem…

R語言生存分析(機器學習)(2)——Enet(彈性網絡)

彈性網絡&#xff08;Elastic Net&#xff09;:是一種用于回歸分析的統計方法&#xff0c;它是嶺回歸&#xff08;Ridge Regression&#xff09;和lasso回歸&#xff08;Lasso Regression&#xff09;的結合&#xff0c;旨在克服它們各自的一些限制。彈性網絡能夠同時考慮L1正則…

mysql 索引 區分字符大小寫

mysql 建立索引&#xff0c;特別是unique索引&#xff0c;是跟字符集、字符排序規則有關的。 對于utf8mb4_0900_ai_ci來說&#xff0c;0900代表Unicode 9.0的規范&#xff0c;ai表示accent insensitivity&#xff0c;也就是“不區分音調”&#xff0c;而ci表示case insensitiv…

wsl2安裝docker引擎(Install Docker Engine on Debian)

安裝 1.卸載舊版本 在安裝 Docker 引擎之前&#xff0c;您必須首先確保卸載任何沖突的軟件包。 發行版維護者在他們的存儲庫。必須先卸載這些軟件包&#xff0c;然后才能安裝 Docker 引擎的正式版本。 要卸載的非官方軟件包是&#xff1a; docker.iodocker-composedocker-…

問道管理:旅游酒店板塊逆市拉升,桂林旅游、華天酒店漲停

游覽酒店板塊14日盤中逆市拉升&#xff0c;到發稿&#xff0c;桂林游覽、華天酒店漲停&#xff0c;張家界漲超8%&#xff0c;君亭酒店漲超5%&#xff0c;眾信游覽、云南游覽漲逾4%。 音訊面上&#xff0c;8月10日&#xff0c;文旅部辦公廳發布康復出境團隊游覽第三批名單&#…

Profibus-DP轉modbus RTU網關modbus rtu和tcp的區別

捷米JM-DPM-RTU網關在Profibus總線側實現主站功能&#xff0c;在Modbus串口側實現從站功能。可將ProfibusDP協議的設備&#xff08;如&#xff1a;EH流量計、倍福編碼器等&#xff09;接入到Modbus網絡中&#xff1b;通過增加DP/PA耦合器&#xff0c;也可將Profibus PA從站接入…