赞
踩
项目目的:
项目背景:IM对性能和体验敏感度非常高 。 大厂必备
你将获得什么:
熟悉开发流程 ,熟练相关技术栈 gin+GORM+swagger + logrus auth 等中间件,三高性能
核心功能:
发送和接受消息,文字 表情 图片 音频 ,访客,点对点,群聊 ,广播,快捷回复,撤回,心跳检测…
技术栈:
前端 后端 (webSocket ,channel/goroutine ,gin ,temlate,gorm ,sql,nosql,mq…)
系统架构:
四层:前端,接入层,逻辑层,持久层
消息发送流程:
A > 登录> 鉴权>(游客) > 消息类型 >(群/广播) > B
go version go1.17.8 windows/amd64
set GO111MODULE=on
go mod init go_exam
go mod tidy
完成用户模块基本的
加入修改电话号码和邮箱 并校验
先引入
- get github.com/asaskevich/govalidator
- 结构体字段后面 加检验规则
- 最后service govalidator.ValidatorStrut(user)
-
1.router包 app.go
- r.GET("/user/getUserList", service.GetUserList)
- r.GET("/user/createUser", service.CreateUser)
- r.GET("/user/deleteUser", service.DeleteUser)
- r.POST("/user/updateUser", service.UpdateUser)
2.service 包 userservice.go
- // GetUserList
- // @Summary 所有用户
- // @Tags 用户模块
- // @Success 200 {string} json{"code","message"}
- // @Router /user/getUserList [get]
- func GetUserList(c *gin.Context) {
- data := make([]*models.UserBasic, 10)
- data = models.GetUserList()
-
- c.JSON(200, gin.H{
- "message": data,
- })
- }
-
- // CreateUser
- // @Summary 新增用户
- // @Tags 用户模块
- // @param name query string false "用户名"
- // @param password query string false "密码"
- // @param repassword query string false "确认密码"
- // @Success 200 {string} json{"code","message"}
- // @Router /user/createUser [get]
- func CreateUser(c *gin.Context) {
- user := models.UserBasic{}
- user.Name = c.Query("name")
- password := c.Query("password")
- repassword := c.Query("repassword")
- if password != repassword {
- c.JSON(-1, gin.H{
- "message": "两次密码不一致!",
- })
- return
- }
- user.PassWord = password
- models.CreateUser(user)
- c.JSON(200, gin.H{
- "message": "新增用户成功!",
- })
- }
-
- // DeleteUser
- // @Summary 删除用户
- // @Tags 用户模块
- // @param id query string false "id"
- // @Success 200 {string} json{"code","message"}
- // @Router /user/deleteUser [get]
- func DeleteUser(c *gin.Context) {
- user := models.UserBasic{}
- id, _ := strconv.Atoi(c.Query("id"))
- user.ID = uint(id)
- models.DeleteUser(user)
- c.JSON(200, gin.H{
- "message": "删除用户成功!",
- })
- }
-
- // UpdateUser
- // @Summary 修改用户
- // @Tags 用户模块
- // @param id formData string false "id"
- // @param name formData string false "name"
- // @param password formData string false "password"
- // @param phone formData string false "phone"
- // @param email formData string false "email"
- // @Success 200 {string} json{"code","message"}
- // @Router /user/updateUser [post]
- func UpdateUser(c *gin.Context) {
- user := models.UserBasic{}
- id, _ := strconv.Atoi(c.PostForm("id"))
- user.ID = uint(id)
- user.Name = c.PostForm("name")
- user.PassWord = c.PostForm("password")
- user.Phone = c.PostForm("phone")
- user.Email = c.PostForm("email")
- fmt.Println("update :", user)
-
- _, err := govalidator.ValidateStruct(user)
- if err != nil {
- fmt.Println(err)
- c.JSON(200, gin.H{
- "message": "修改参数不匹配!",
- })
- } else {
- models.UpdateUser(user)
- c.JSON(200, gin.H{
- "message": "修改用户成功!",
- })
- }
-
- }
3.modesl包 user_basic.go
- Phone string `valid:"matches(^1[3-9]{1}\\d{9}$)"`
- Email string `valid:"email"`
4,然后测试
重复注册校验:
- func FindUserByName(name string) UserBasic {
- user := UserBasic{}
- utils.DB.Where("name = ?", name).First(&user)
- return user
- }
- func FindUserByPhone(phone string) *gorm.DB {
- user := UserBasic{}
- return utils.DB.Where("Phone = ?", phone).First(&user)
- }
- func FindUserByEmail(email string) *gorm.DB {
- user := UserBasic{}
- return utils.DB.Where("email = ?", email).First(&user)
- }
-
-
- 再到service层 加入判断
- data := models.FindUserByName(user.Name)
- if data.Name != "" {
- c.JSON(-1, gin.H{
- "message": "用户名已注册!",
- })
- return
- }
-
-
-
-
注册 加密操作
- package utils
-
- import (
- "crypto/md5"
- "encoding/hex"
- "fmt"
- "strings"
- )
-
- //小写
- func Md5Encode(data string) string {
- h := md5.New()
- h.Write([]byte(data))
- tempStr := h.Sum(nil)
- return hex.EncodeToString(tempStr)
- }
-
- //大写
- func MD5Encode(data string) string {
- return strings.ToUpper(Md5Encode(data))
- }
-
- //加密
- func MakePassword(plainpwd, salt string) string {
- return Md5Encode(plainpwd + salt)
- }
-
- //解密
- func ValidPassword(plainpwd, salt string, password string) bool {
- md := Md5Encode(plainpwd + salt)
- fmt.Println(md + " " + password)
- return md == password
- }
-
-
-
- service层 判断之后加入
- //user.PassWord = password
- user.PassWord = utils.MakePassword(password, salt)
- user.Salt = salt //表更新了字段 db.AutoMigrate(&models.UserBasic{})
- fmt.Println(user.PassWord)
- models.CreateUser(user)
-
-
-
登录解密 :
- //dao层
- func FindUserByNameAndPwd(name string, password string) UserBasic {
- user := UserBasic{}
- utils.DB.Where("name = ? and pass_word=?", name, password).First(&user)
- return user
- }
-
-
-
- // GetUserList
- // @Summary 所有用户
- // @Tags 用户模块
- // @param name query string false "用户名"
- // @param password query string false "密码"
- // @Success 200 {string} json{"code","message"}
- // @Router /user/findUserByNameAndPwd [get]
- func FindUserByNameAndPwd(c *gin.Context) {
- data := models.UserBasic{}
-
- name := c.Query("name")
- password := c.Query("password")
- user := models.FindUserByName(name)
- if user.Name == "" {
- c.JSON(200, gin.H{
- "message": "该用户不存在",
- })
- return
- }
-
- flag := utils.ValidPassword(password, user.Salt, user.PassWord)
- if !flag {
- c.JSON(200, gin.H{
- "message": "密码不正确",
- })
- return
- }
- pwd := utils.MakePassword(password, user.Salt)
- data = models.FindUserByNameAndPwd(name, pwd)
-
- c.JSON(200, gin.H{
- "message": data,
- })
- }
-
-
-
- router层 :
-
- r.POST("/user/findUserByNameAndPwd", service.FindUserByNameAndPwd)
-
token的加入对返回的结构调整 。
修改登录的方法:
- func FindUserByNameAndPwd(name string, password string) UserBasic {
- user := UserBasic{}
- utils.DB.Where("name = ? and pass_word=?", name, password).First(&user)
-
- //token加密
- str := fmt.Sprintf("%d", time.Now().Unix())
- temp := utils.MD5Encode(str)
- utils.DB.Model(&user).Where("id = ?", user.ID).Update("identity", temp)
- return user
- }
-
-
- // 返回的结果:
- c.JSON(200, gin.H{
- "code": 0, // 0成功 -1失败
- "message": "修改用户成功!",
- "data": user,
- })
-
加入Redis
go get github.com/go-redis/redis
配置redis
- redis:
- addr: "192.168.137.131:6379"
- password: ""
- DB: 0
- poolSize: 30
- minIdleConn: 30
然后main方法中
utils.InitRedis()
最后再 utils
- func InitRedis() {
- Red = redis.NewClient(&redis.Options{
- Addr: viper.GetString("redis.addr"),
- Password: viper.GetString("redis.password"),
- DB: viper.GetInt("redis.DB"),
- PoolSize: viper.GetInt("redis.poolSize"),
- MinIdleConns: viper.GetInt("redis.minIdleConn"),
- })
- pong, err := Red.Ping().Result()
- if err != nil {
- fmt.Println("init redis 。。。。", err)
- } else {
- fmt.Println(" Redis inited 。。。。", pong)
- }
- }
测试看是否正常
通过WebSocket通信
- go get github.com/gorilla/websocket
- go get github.com/go-redis/redis/v8
-
-
-
- package utils
-
- import (
- "context"
- "fmt"
- "log"
- "os"
- "time"
-
- "github.com/go-redis/redis/v8"
- "github.com/spf13/viper"
- "gorm.io/driver/mysql"
- "gorm.io/gorm"
- "gorm.io/gorm/logger"
- )
-
- var (
- DB *gorm.DB
- Red *redis.Client
- )
-
- func InitConfig() {
- viper.SetConfigName("app")
- viper.AddConfigPath("config")
- err := viper.ReadInConfig()
- if err != nil {
- fmt.Println(err)
- }
- fmt.Println("config app inited 。。。。")
- }
-
- func InitMySQL() {
- //自定义日志模板 打印SQL语句
- newLogger := logger.New(
- log.New(os.Stdout, "\r\n", log.LstdFlags),
- logger.Config{
- SlowThreshold: time.Second, //慢SQL阈值
- LogLevel: logger.Info, //级别
- Colorful: true, //彩色
- },
- )
-
- DB, _ = gorm.Open(mysql.Open(viper.GetString("mysql.dns")),
- &gorm.Config{Logger: newLogger})
- fmt.Println(" MySQL inited 。。。。")
- //user := models.UserBasic{}
- //DB.Find(&user)
- //fmt.Println(user)
- }
-
- func InitRedis() {
- Red = redis.NewClient(&redis.Options{
- Addr: viper.GetString("redis.addr"),
- Password: viper.GetString("redis.password"),
- DB: viper.GetInt("redis.DB"),
- PoolSize: viper.GetInt("redis.poolSize"),
- MinIdleConns: viper.GetInt("redis.minIdleConn"),
- })
- }
-
- const (
- PublishKey = "websocket"
- )
-
- //Publish 发布消息到Redis
- func Publish(ctx context.Context, channel string, msg string) error {
- var err error
- fmt.Println("Publish 。。。。", msg)
- err = Red.Publish(ctx, channel, msg).Err()
- if err != nil {
- fmt.Println(err)
- }
- return err
- }
-
- //Subscribe 订阅Redis消息
- func Subscribe(ctx context.Context, channel string) (string, error) {
- sub := Red.Subscribe(ctx, channel)
- fmt.Println("Subscribe 。。。。", ctx)
- msg, err := sub.ReceiveMessage(ctx)
- if err != nil {
- fmt.Println(err)
- return "", err
- }
- fmt.Println("Subscribe 。。。。", msg.Payload)
- return msg.Payload, err
- }
-
-
-
-
-
- userservice.go中加入
-
- //防止跨域站点伪造请求
- var upGrader = websocket.Upgrader{
- CheckOrigin: func(r *http.Request) bool {
- return true
- },
- }
-
- func SendMsg(c *gin.Context) {
- ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
- if err != nil {
- fmt.Println(err)
- return
- }
- defer func(ws *websocket.Conn) {
- err = ws.Close()
- if err != nil {
- fmt.Println(err)
- }
- }(ws)
- MsgHandler(c, ws)
- }
- func MsgHandler(c *gin.Context, ws *websocket.Conn) {
- for {
- msg, err := utils.Subscribe(c, utils.PublishKey)
- if err != nil {
- fmt.Println(" MsgHandler 发送失败", err)
- }
-
- tm := time.Now().Format("2006-01-02 15:04:05")
- m := fmt.Sprintf("[ws][%s]:%s", tm, msg)
- err = ws.WriteMessage(1, []byte(m))
- if err != nil {
- log.Fatalln(err)
- }
- }
- }
-
-
-
- router层 app.go
- //发送消息
- r.GET("/user/sendMsg", service.SendMsg)
-
-
测试: http://www.jsons.cn/websocket/
ws://localhost:8081/user/sendMsg
设计 关系表 ,群信息表 ,消息表
- package models
-
- import "gorm.io/gorm"
-
- //消息
- type Message struct {
- gorm.Model
- FormId uint //发送者
- TargetId uint //接受者
- Type string //消息类型 群聊 私聊 广播
- Media int //消息类型 文字 图片 音频
- Content string //消息内容
- Pic string
- Url string
- Desc string
- Amount int //其他数字统计
- }
-
- func (table *Message) TableName() string {
- return "message"
- }
-
-
- package models
-
- import "gorm.io/gorm"
-
- //群信息
- type GroupBasic struct {
- gorm.Model
- Name string
- OwnerId uint
- Icon string
- Type int
- Desc string
- }
-
- func (table *GroupBasic) TableName() string {
- return "group_basic"
- }
-
- package models
-
- import "gorm.io/gorm"
-
- //人员关系
- type Contact struct {
- gorm.Model
- OwnerId uint //谁的关系信息
- TargetId uint //对应的谁
- Type int //对应的类型 0 1 3
- Desc string
- }
-
- func (table *Contact) TableName() string {
- return "contact"
- }
-
发送消息 接受消息
需要 :发送者ID ,接受者ID ,消息类型,发送的内容,发送类型
校验token ,关系 ,
- package models
-
- import (
- "encoding/json"
- "fmt"
- "net"
- "net/http"
- "strconv"
- "sync"
-
- "github.com/gorilla/websocket"
- "gopkg.in/fatih/set.v0"
- "gorm.io/gorm"
- )
-
- //消息
- type Message struct {
- gorm.Model
- FormId int64 //发送者
- TargetId int64 //接受者
- Type int //发送类型 群聊 私聊 广播
- Media int //消息类型 文字 图片 音频
- Content string //消息内容
- Pic string
- Url string
- Desc string
- Amount int //其他数字统计
- }
-
- func (table *Message) TableName() string {
- return "message"
- }
-
- type Node struct {
- Conn *websocket.Conn
- DataQueue chan []byte
- GroupSets set.Interface
- }
-
- //映射关系
- var clientMap map[int64]*Node = make(map[int64]*Node, 0)
-
- //读写锁
- var rwLocker sync.RWMutex
-
- // 需要 :发送者ID ,接受者ID ,消息类型,发送的内容,发送类型
- func Chat(writer http.ResponseWriter, request *http.Request) {
- //1. 获取参数 并 检验 token 等合法性
- //token := query.Get("token")
- query := request.URL.Query()
- Id := query.Get("userId")
- userId, _ := strconv.ParseInt(Id, 10, 64)
- //msgType := query.Get("type")
- //targetId := query.Get("targetId")
- // context := query.Get("context")
- isvalida := true //checkToke() 待.........
- conn, err := (&websocket.Upgrader{
- //token 校验
- CheckOrigin: func(r *http.Request) bool {
- return isvalida
- },
- }).Upgrade(writer, request, nil)
- if err != nil {
- fmt.Println(err)
- return
- }
- //2.获取conn
- node := &Node{
- Conn: conn,
- DataQueue: make(chan []byte, 50),
- GroupSets: set.New(set.ThreadSafe),
- }
- //3. 用户关系
- //4. userid 跟 node绑定 并加锁
- rwLocker.Lock()
- clientMap[userId] = node
- rwLocker.Unlock()
- //5.完成发送逻辑
- go sendProc(node)
- //6.完成接受逻辑
- go recvProc(node)
- sendMsg(userId, []byte("欢迎进入聊天系统"))
-
- }
-
- func sendProc(node *Node) {
- for {
- select {
- case data := <-node.DataQueue:
- err := node.Conn.WriteMessage(websocket.TextMessage, data)
- if err != nil {
- fmt.Println(err)
- return
- }
- }
- }
- }
-
- func recvProc(node *Node) {
- for {
- _, data, err := node.Conn.ReadMessage()
- if err != nil {
- fmt.Println(err)
- return
- }
- broadMsg(data)
- fmt.Println("[ws] <<<<< ", data)
- }
- }
-
- var udpsendChan chan []byte = make(chan []byte, 1024)
-
- func broadMsg(data []byte) {
- udpsendChan <- data
- }
-
- func init() {
- go udpSendProc()
- go udpRecvProc()
- }
-
- //完成udp数据发送协程
- func udpSendProc() {
- con, err := net.DialUDP("udp", nil, &net.UDPAddr{
- IP: net.IPv4(192, 168, 0, 255),
- Port: 3000,
- })
- defer con.Close()
- if err != nil {
- fmt.Println(err)
- }
- for {
- select {
- case data := <-udpsendChan:
- _, err := con.Write(data)
- if err != nil {
- fmt.Println(err)
- return
- }
- }
- }
-
- }
-
- //完成udp数据接收协程
- func udpRecvProc() {
- con, err := net.ListenUDP("udp", &net.UDPAddr{
- IP: net.IPv4zero,
- Port: 3000,
- })
- if err != nil {
- fmt.Println(err)
- }
- defer con.Close()
- for {
- var buf [512]byte
- n, err := con.Read(buf[0:])
- if err != nil {
- fmt.Println(err)
- return
- }
- dispatch(buf[0:n])
- }
- }
-
- //后端调度逻辑处理
- func dispatch(data []byte) {
- msg := Message{}
- err := json.Unmarshal(data, &msg)
- if err != nil {
- fmt.Println(err)
- return
- }
- switch msg.Type {
- case 1: //私信
- sendMsg(msg.TargetId, data)
- // case 2: //群发
- // sendGroupMsg()
- // case 3://广播
- // sendAllMsg()
- //case 4:
- //
- }
- }
-
- func sendMsg(userId int64, msg []byte) {
- rwLocker.RLock()
- node, ok := clientMap[userId]
- rwLocker.RUnlock()
- if ok {
- node.DataQueue <- msg
- }
- }
-
集成html 登录和注册
- //app.go 加入
- //首页
- r.GET("/", service.GetIndex)
- r.GET("/index", service.GetIndex)
- r.GET("/toRegister", service.ToRegister)
-
-
- // index.go
- package service
-
- import (
- "text/template"
-
- "github.com/gin-gonic/gin"
- )
-
- // GetIndex
- // @Tags 首页
- // @Success 200 {string} welcome
- // @Router /index [get]
- func GetIndex(c *gin.Context) {
- ind, err := template.ParseFiles("index.html", "views/chat/head.html")
- if err != nil {
- panic(err)
- }
- ind.Execute(c.Writer, "index")
- // c.JSON(200, gin.H{
- // "message": "welcome !! ",
- // })
- }
-
- func ToRegister(c *gin.Context) {
- ind, err := template.ParseFiles("views/user/register.html")
- if err != nil {
- panic(err)
- }
- ind.Execute(c.Writer, "register")
- // c.JSON(200, gin.H{
- // "message": "welcome !! ",
- // })
- }
-
然后页面 :
- <!DOCTYPE html>
- <html>
-
- <head>
- <!--js include-->
- {
- {template "/chat/head.shtml"}}
- </head>
- <body>
-
- <header class="mui-bar mui-bar-nav">
- <h1 class="mui-title">登录</h1>
- </header>
- {
- {.}}
- <div class
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。