Skip to content

基于 websocket的聊天室 mongodb+redis(Chat room based on tcp protocol Chat room based on websocket)

Notifications You must be signed in to change notification settings

Loveyless/gin_websocket_test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

88cfd15 · Oct 8, 2022

History

13 Commits
Sep 27, 2022
Oct 8, 2022
Oct 6, 2022
Sep 29, 2022
Oct 4, 2022
Sep 25, 2022
Sep 29, 2022
Oct 8, 2022
Oct 1, 2022
Sep 29, 2022
Sep 25, 2022
Oct 4, 2022
Sep 30, 2022
Sep 30, 2022
Oct 1, 2022
Sep 25, 2022
Oct 1, 2022

Repository files navigation

websocket聊天室

前置准备

go get -u github.com/gin-gonic/gin
go get github.com/gorilla/websocket
go get go.mongodb.org/mongo-driver/mongo
go get github.com/jordan-wright/email

功能

用户模块 密码登录 邮箱注册 用户详情 通讯模块(核心) 一对一通讯 多对多通讯 消息列表聊天记录列表

集合设计

用户信息集合

user

{
    "username":"账号",
    "password":"密码",
    "nicname":"用户名",
    "sex":1, //0-未知 1-男 2-女
    "email":"邮箱",
    "avatar":"头像",
    "created_at":1,
    "updated_at":1,
}

消息集合

message

{
    "user_identity":"用户唯一标识",
    "room_identity":"房间唯一标识",
    "data":"发送的数据",
    "created_at":1,
    "updated_at":1,
}

房间集合

room

{
    "identity":"房间唯一标识",
    "number":"房间号",
    "name":"房间名",
    "info":"简介",
    "user_identity":"房间创建者的唯一标识",
    "room_type":"房间类型",    //[1独聊] [2群聊]
    "created_at":1,
    "updated_at":1,
}

房间和用户的关联

user_room

{
    "user_identity": "用户的唯一标识",
    "room_identity": "房间的唯一标识",
    "message_identity": "消息的唯一标识",
    "created_at": 1,
    "updated_at": 1
}

目录结构

gin+mongodb

目录结构

|-config      基本配置
|-controller  基础逻辑
|-cors        跨域中间件
|-email       封装发送邮箱
|-MyJwt       token中间件
|-MyUtils     工具
|-router      路由 分组/拦截
|-service     操作数据的函数
    db.go     mongo redis数据库句柄
|-test        测试函数
|-validator   request校验
|-validator   将gin验证器错误翻译成中文
|-go_test.sql mongodb导出导入文件
|-go.mod
|-gosum
|-main.go
|-websocket项目.postman_collection.json postman导出导入文件

接口规则

普通接口规则在postman

websocket协议接口规则

ws://localhost:8080/user/websocket/message
请求头
	token
接收发送消息的结构体
    type MessageStruct struct {
        RoomIdentity string `json:"room_identity"` //接收房间
        Message      string `json:"message"`       //内容
    }


新做了一个接口
ws://localhost:8080/websocket/all
请求参数
	token:xxx
接收发送消息的结构体
    type MessageStruct struct {
        Message      string `json:"message"`       //内容
    }

邮箱库的使用

发邮件

//NWNXHEUGDOYJJSYG
func TestSendEmail(t *testing.T) {
  e := email.NewEmail()                                                                                     //创建一个新的邮件
  e.From = "Loveyless <[email protected]>"                                                          // 发件人
  e.To = []string{"[email protected]"}                                                                        // 收件人 可以多个
  e.Subject = "验证码已发送,请查收"                                                                                  //邮件主题
  e.HTML = []byte("<div>Your verification code:<b>" + "6666" + "</b>.</div>\n<div>author:Loveyless.</div>") //邮件内容

  // err := e.Send("smtp.163.com:465", smtp.PlainAuth("", "[email protected]", "NWNXHEUGDOYJJSYG", "smtp.163.com"))
  // e.Send用不了EOF 只能使用SendWithTLS关闭SSL连接就可以了 还有一个函数我也发送失败->SendWithTLS通过可选的tls配置发送电子邮件。

  err := e.SendWithTLS("smtp.163.com:465", smtp.PlainAuth("", "[email protected]", "NWNXHEUGDOYJJSYG", "smtp.163.com"), &tls.Config{
    //跳过验证
    InsecureSkipVerify: true,
    ServerName:         "smtp.163.com",
  })
  if err != nil {
    t.Fatal(err)
  }
}

邮箱验证注册

time.Duration的使用

https://blog.csdn.net/taoshihan/article/details/125924020?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-125924020-blog-82706022.t0_edu_mix&spm=1001.2101.3001.4242.1&utm_relevant_index=1

redis存一个 key为邮箱30秒过期 和 value验证码

到时候拿出来比对

set验证码就是Set                                                                这里拿到全局config里面的过期时间 用time.Duration处理一下
err = service.Rdb.Set(ctx, config.RegisterPrefix+emailInfo.Email, emailCode, time.Second*time.Duration(config.RegisterLowTime)).Err()


验证就是Getredis
emailCode, err := service.Rdb.Get(context.Background(), config.RegisterPrefix+info.Email).Result()

websocket服务

官方包里有示例

echo简易版

比官方的还简易

package test

import (
  "flag"
  "log"
  "net/http"
  "testing"

  "github.com/gorilla/websocket"
)

//
var addr = flag.String("addr", "localhost:8080", "http service address")

// Upgrader指定升级HTTP连接为WebSocket连接。
// 并发调用Upgrader的方法是安全的。
var upgrader = websocket.Upgrader{}

//这个里面先不用管
func echo(w http.ResponseWriter, r *http.Request) {

  c, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Print("upgrade:", err)
    return
  }
  defer c.Close()
  for {
    mt, message, err := c.ReadMessage()
    if err != nil {
      log.Println("read:", err)
      break
    }
    log.Printf("recv: %s", message)
    err = c.WriteMessage(mt, message)
    if err != nil {
      log.Println("write:", err)
      break
    }
  }
}


func TestWebsocket(t *testing.T) {
  // /echo接口
  http.HandleFunc("/echo", echo)
  log.Fatal(http.ListenAndServe(*addr, nil))
}

切片存句柄 一对多

但是会给自己也发一条

package test

import (
  "flag"
  "log"
  "net/http"
  "testing"

  "github.com/gorilla/websocket"
)

//
var addr = flag.String("addr", "localhost:8080", "http service address")

// Upgrader指定升级HTTP连接为WebSocket连接。
// 并发调用Upgrader的方法是安全的。
var upgrader = websocket.Upgrader{}

//一对多 定义一个wsList切片
var wsList = make([]*websocket.Conn, 0)

func echo(w http.ResponseWriter, r *http.Request) {

  // Conn 类型表示一个 WebSocket 连接。服务器应用程序从 HTTP 请求处理程序调用 Upgrader.Upgrade 方法以获取 *Conn:
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Print("upgrade:", err)
    return
  }
  defer conn.Close()

  //如果有人连接 放到切片里
  wsList = append(wsList, conn)

  for {
    mt, message, err := conn.ReadMessage()
    if err != nil {
      log.Println("read:", err)
      break
    }

    //循环切片 一个一个发送 一对多 上面的逻辑先别管
    for _, conn := range wsList {
      conn.WriteMessage(mt, message)
    }
    if err != nil {
      log.Println("write:", err)
      break
    }
  }
}

func TestWebsocket(t *testing.T) {
  http.HandleFunc("/echo", echo)
  log.Fatal(http.ListenAndServe(*addr, nil))
}

gin中搭建websocket

func TestGinWebsocket(t *testing.T) {
  r := gin.Default()
  //websocket
  r.GET("/echo", func(c *gin.Context) {
    //c.Writer实现了http.ResponseWriter接口
    //c.Request实现了*http.Request接口
    echo(c.Writer, c.Request)
  })
  r.Run()
}


//
var addr = flag.String("addr", "localhost:8080", "http service address")

// Upgrader指定升级HTTP连接为WebSocket连接。
// 并发调用Upgrader的方法是安全的。
var upgrader = websocket.Upgrader{}

//一对多 定义一个wsList切片
var wsList = make([]*websocket.Conn, 0)

func echo(w http.ResponseWriter, r *http.Request) {

  // Conn 类型表示一个 WebSocket 连接。服务器应用程序从 HTTP 请求处理程序调用 Upgrader.Upgrade 方法以获取 *Conn:
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Print("upgrade:", err)
    return
  }
  defer conn.Close()

  //如果有人连接 放到切片里
  wsList = append(wsList, conn)

  for {
    mt, message, err := conn.ReadMessage()
    if err != nil {
      log.Println("read:", err)
      break
    }

    //循环切片 一个一个发送 一对多 上面的逻辑先别管
    for _, conn := range wsList {
      conn.WriteMessage(mt, message)
    }
    if err != nil {
      log.Println("write:", err)
      break
    }
  }
}

websocket核心 发送/接收 消息

简易版

router

  //用户相关的分组 需要验证token
  user := r.Group("/user", MyJwt.FilterToken())
  
  // 发送接收消息
  user.GET("/websocket/message", controller.WebsocketMessage)

websocket controller

核心就是发送接收消息

后期在协议前 存入了用户句柄 判读用户是否在带过来的房间 遍历出在线用户的 conn 在write发送消息

package controller

import (
  "fmt"
  "gin_websocket_test/MyJwt"
  "log"

  "github.com/gin-gonic/gin"
  "github.com/gorilla/websocket"
)

// Upgrader指定升级HTTP连接为WebSocket连接。
// 并发调用Upgrader的方法是安全的。
var upgrader = websocket.Upgrader{}

//存储conn句柄的map key是用户id value是conn句柄
var wsMap = make(map[string]*websocket.Conn)

//接收发送消息的结构体
type MessageStruct struct {
  RoomIdentity string `json:"room_identity"` //接收房间
  Message      string `json:"message"`       //内容
}

// 发送接收消息
func WebsocketMessage(c *gin.Context) {

  //Upgrade将HTTP服务器连接升级到WebSocket协议。返回句柄
  conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
  if err != nil {
    c.JSON(200, gin.H{
      "status":  400,
      "message": "wensocket升级失败",
      "data":    err,
    })
    c.Abort()
    return
  }
  defer conn.Close()

  // token取出数据
  claims := c.MustGet("claims").(*MyJwt.MyCustomClaims)
  fmt.Println(claims.Identity)
  //存入map集合
  wsMap[claims.Identity] = conn

  //核心 for一直接收消息
  for {
    messaageInfo := new(MessageStruct)
    err := conn.ReadJSON(messaageInfo) //readjson格式化到结构体 ReadMessage是原始的 单测用的message接收的可以去看 发送可以用message
    if err != nil {
      log.Println("读取数据失败", err.Error())
      return
    }

    //发送消息 目前是给所有再线用户发 后面优化成给所有在线 并且在同一个房间的用户发
    for _, conn := range wsMap {
      //TextMessage表示文本数据消息。文本消息有效负载被解释为UTF-8编码的文本数据。
      //消息类型是一个整数,表示消息的类型,它的值可以是下面几个常量之一: TextMessage对应1、BinaryMessage、CloseMessage、PingMessage、PongMessage
      err := conn.WriteMessage(websocket.TextMessage, []byte(messaageInfo.Message))
      if err != nil {
        log.Println("发送数据失败", err.Error())
        return
      }
    }
  }

}

websocket遇到的问题

//自己从请求头解析 发现可以解析 但是下面接收消息的时候 会报错 websocket: close 1006 (abnormal closure): unexpected EOF //报错会导致 连接后秒断

//自己从参数解析 一切正常!

package controller

import (
  "encoding/json"
  "gin_websocket_test/MyJwt"
  "log"
  "net/http"
  "time"

  "github.com/gin-gonic/gin"
  "github.com/gorilla/websocket"
)

// Upgrader指定升级HTTP连接为WebSocket连接。
// 并发调用Upgrader的方法是安全的。
var upgrader = websocket.Upgrader{
  // 解决跨域问题
  CheckOrigin: func(r *http.Request) bool {
    return true
  },
}

// 在线用户的句柄 存储conn句柄的map
// key是用户Identity value是conn句柄
var wsMap = make(map[string]*websocket.Conn)

//接收发送消息的结构体
type MessageStruct struct {
  RoomIdentity string `json:"room_identity"` //接收房间
  Message      string `json:"message"`       //内容
  // Token        string `json:"token"`
  Username string `json:"username"`
  Time     string `json:"time"`
}

// 发送接收消息
func WebsocketMessage(c *gin.Context) {

  //Upgrade将HTTP服务器连接升级到WebSocket协议。返回句柄
  conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
  if err != nil {
    c.JSON(200, gin.H{
      "status":  400,
      "message": "wensocket升级失败",
      "data":    err,
    })
    c.Abort()
    return
  }
  defer conn.Close()

  // token取出数据 js里的websocket不能随便带请求头 只能自己解析
  // claims := c.MustGet("claims").(*MyJwt.MyCustomClaims)

  //自己从请求头解析 发现可以解析 但是下面接收消息的时候 会报错 websocket: close 1006 (abnormal closure): unexpected EOF
  //报错会导致 连接后秒断
  token := c.GetHeader("Sec-WebSocket-Protocol")
  if token == "" {
    c.JSON(200, gin.H{
      "status":  400,
      "message": "token不能为空",
    })
    c.Abort()
    return
  }
  claims, _ := MyJwt.ParseToken(token)

  //自己从参数解析 一切正常!
  // token := c.Query("tt")
  // if token == "" {
  //   c.JSON(200, gin.H{
  //     "status":  400,
  //     "message": "token不能为空",
  //   })
  //   c.Abort()
  //   return
  // }
  // claims, _ := MyJwt.ParseToken(token)

  // 请求头和参数都解析不出来 只能从发过来的地方解析了 所以下方的存入map集合 也做不了
  // 只能在拿到数据后存入map集合

  //存入map集合
  wsMap[claims.Identity] = conn

  //核心 for一直接收消息
  for {
    //拿到发送过来的数据
    messaageInfo := new(MessageStruct)
    err := conn.ReadJSON(messaageInfo) //readjson格式化到结构体 ReadMessage是原始的 单测用的message接收的可以去看 发送可以用message
    if err != nil {
      log.Println("拿到发送过来的数据异常", err.Error())
      return
    }

    // 1.判断用户是否属于消息体的房间
    // _, err = service.GetUserRoomByUserIdentityRoomIdentity(claims.Identity, messaageInfo.RoomIdentity)
    // if err != nil {
    //   log.Printf("user_identity:%v,room_identity:%v error:%v", claims.Identity, messaageInfo.RoomIdentity, err.Error())
    //   return
    // }
    // // 2.获取在特定房间的在线用户
    // //   查询房间的所有用户
    // userRoomList, err := service.GetUserRoomByRoomIdentity(messaageInfo.RoomIdentity)
    // if err != nil {
    //   log.Printf("room_identity:%v error:%v", messaageInfo.RoomIdentity, err.Error())
    //   return
    // }

    //  遍历出特定房间的在线用户
    // for _, userRoom := range userRoomList {
    //   // 如果房间中所有用户有在线的用户那么就发送消息 注意这里巧妙的写法!!
    //   if conn, ok := wsMap[userRoom.UserIdentity]; ok {
    //     err := conn.WriteMessage(websocket.TextMessage, []byte(messaageInfo.Message))
    //     if err != nil {
    //       log.Println("发送数据失败", err.Error())
    //       return
    //     }
    //   }
    // }
    // 3.保存消息
    // messageBasic := &service.MessageBasic{
    //   UserIdentity: claims.Identity,
    //   RoomIdentity: messaageInfo.RoomIdentity,
    //   Data:         messaageInfo.Message,
    //   CratedAt:     time.Now().Unix(),
    //   UpdatedAt:    time.Now().Unix(),
    // }
    // err = service.InsertOneMessageBasic(messageBasic)
    // if err != nil {
    //   log.Printf("保存消息错误%v", err.Error())
    //   return
    // }

    // //发送消息 目前是给所有再线用户发 后面优化成给所有在线 并且在同一个房间的用户发
    for _, conn := range wsMap {
      //TextMessage表示文本数据消息。文本消息有效负载被解释为UTF-8编码的文本数据。
      //消息类型是一个整数,表示消息的类型,它的值可以是下面几个常量之一: TextMessage对应1、BinaryMessage、CloseMessage、PingMessage、PongMessage
      // err := conn.WriteMessage(websocket.TextMessage, []byte(messaageInfo.Message))
      messaageInfo.Username = claims.Username
      messaageInfo.Time = time.Now().Format("2006-01-02 15:04:05")
      resJson, _ := json.Marshal(messaageInfo)
      err := conn.WriteMessage(websocket.TextMessage, resJson)
      if err != nil {
        log.Println("发送数据失败", err.Error())
        return
      }
    }
  }

}

查询别人个人资料

查询别人的资料并且是否为好友

get请求/query/detail

还是比较小复杂的 对比两个人有没有相同的房间

添加好友/删除好友

  1. 添加好友

初步想法

查看是否为好友

不是的话 给两个人弄用一个房间id存在room_basic表中

About

基于 websocket的聊天室 mongodb+redis(Chat room based on tcp protocol Chat room based on websocket)

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published