基于go+vue的多人在线聊天的im系统
文章目录
一、前端部分
打算优化一下界面,正在开发中。。。
二、后端部分
1、中间件middleware设计jwt和cors
jwt.go
go
package middlewares
import (
"crypto/rsa"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"im/global"
"io/ioutil"
"net/http"
"os"
"time"
)
func JWT() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 从请求头获取token
token := ctx.Request.Header.Get("w-token")
if token == "" {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"msg": "请登录",
})
return
}
// 打开存储公钥文件
file, _ := os.Open(global.SrvConfig.JWTInfo.PublicKeyPath)
// 读取公钥文件
bytes, _ := ioutil.ReadAll(file)
// 解析公钥
publickey, _ := jwt.ParseRSAPublicKeyFromPEM(bytes)
jwtVerier := &JWTTokenVerifier{PublicKey: publickey}
claim, err := jwtVerier.Verify(token)
if err != nil {
fmt.Println(err)
ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"msg": "请登录",
})
return
}
ctx.Set("claim", claim) //获取全部信息
ctx.Set("name", claim.Subject) // 获取用户名
ctx.Next()
}
}
func Auth(token string) (*MyClaim, error) {
if token == "" {
return nil, fmt.Errorf("ws认证失败,token为空")
}
file, _ := os.Open(global.SrvConfig.JWTInfo.PublicKeyPath)
bytes, _ := ioutil.ReadAll(file)
publickey, _ := jwt.ParseRSAPublicKeyFromPEM(bytes)
jwtVerier := &JWTTokenVerifier{PublicKey: publickey}
return jwtVerier.Verify(token)
}
type JWTTokenVerifier struct {
// 存储用于验证签名的公钥
PublicKey *rsa.PublicKey
}
type MyClaim struct {
Role int
jwt.StandardClaims
}
func (v *JWTTokenVerifier) Verify(token string) (*MyClaim, error) {
t, err := jwt.ParseWithClaims(token, &MyClaim{},
func(*jwt.Token) (interface{}, error) {
return v.PublicKey, nil
})
if err != nil {
return nil, fmt.Errorf("cannot parse token: %v", err)
}
if !t.Valid {
return nil, fmt.Errorf("token not valid")
}
clm, ok := t.Claims.(*MyClaim)
if !ok {
return nil, fmt.Errorf("token claim is not MyClaim")
}
if err := clm.Valid(); err != nil {
return nil, fmt.Errorf("claim not valid: %v", err)
}
return clm, nil
}
type JWTTokenGen struct {
privateKey *rsa.PrivateKey
issuer string
nowFunc func() time.Time
}
func NewJWTTokenGen(issuer string, privateKey *rsa.PrivateKey) *JWTTokenGen {
return &JWTTokenGen{
issuer: issuer,
nowFunc: time.Now,
privateKey: privateKey,
}
}
func (t *JWTTokenGen) GenerateToken(userName string, expire time.Duration) (string, error) {
nowSec := t.nowFunc().Unix()
tkn := jwt.NewWithClaims(jwt.SigningMethodRS512, &MyClaim{
StandardClaims: jwt.StandardClaims{
Issuer: t.issuer,
IssuedAt: nowSec,
ExpiresAt: nowSec + int64(expire.Seconds()),
Subject: userName,
},
})
return tkn.SignedString(t.privateKey)
}
cors.go
go
package middlewares
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Cors() gin.HandlerFnc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, w-token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
}
}
2、配置文件设计
config.yaml
yaml
jwt:
privateKeyPath: ./config/private.key
publicKeyPath: ./config/public.key
port: 9288
name: user-web
redis:
ip:
port: 6379
mysql:
ip:
username:
password:
db_name: im
config/config.go
go
package config
// 读取yaml配置文件,形成映射的相关类
type JWTconfig struct {
PrivateKeyPath string `mapstructure:"privateKeyPath" json:"privateKeyPath"`
PublicKeyPath string `mapstructure:"publicKeyPath" json:"publicKeyPath"`
}
type RedisConfig struct {
IP string `mapstructure:"ip"`
Port string `mapstructure:"port"`
}
type MysqlConfig struct {
IP string `mapstructure:"ip"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
DbName string `mapstructure:"db_name"`
}
type SrvConfig struct {
Name string `mapstructure:"name" json:"name"`
Port int `mapstructure:"port" json:"port"`
JWTInfo JWTconfig `mapstructure:"jwt" json:"jwt"`
RedisInfo RedisConfig `mapstructure:"redis" json:"redis"`
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
}
initalize/config.go
go
package Initialize
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"im/global"
)
func InitConfig() {
//从配置文件中读取出对应的配置
var configFileName = fmt.Sprintf("./config.yaml" )
v := viper.New()
//文件的路径
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
// 开启实时监控
v.WatchConfig()
//这个对象如何在其他文件中使用 - 全局变量
if err := v.Unmarshal(&global.SrvConfig); err != nil {
panic(err)
}
// 文件更新的回调函数
v.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("配置改变")
if err := v.Unmarshal(&global.SrvConfig); err != nil {
panic(err)
}
})
}
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
return viper.IsSet(env)
}
global.go 声明全局变量
go
package global
import (
"github.com/go-redis/redis/v8"
"github.com/jinzhu/gorm"
"im/config"
"sync"
)
var (
// 配置信息
SrvConfig = config.SrvConfig{}
// 分别管理存储已注册用户和在线用户
// 已注册用户map,key为name value为password
UserMap = sync.Map{}
// 在线用户map,key为name value为连接句柄list
LoginMap = sync.Map{}
// redis客户端
Redis *redis.Client
// db服务
DB *gorm.DB
)
3、Mysql和Redis连接
db.go
go
package Initialize
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"im/global"
"os"
)
var err error
func InitDB() {
// 构建数据库连接字符串
dbConfig := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
global.SrvConfig.MysqlInfo.Username,
global.SrvConfig.MysqlInfo.Password,
global.SrvConfig.MysqlInfo.IP,
global.SrvConfig.MysqlInfo.DbName)
// 连接数据库
global.DB, err = gorm.Open("mysql", dbConfig)
if err != nil {
fmt.Println("[Initialize] 数据库连接失败:%v", err)
return
}
// 设置连接池参数
global.DB.DB().SetMaxIdleConns(10) //设置数据库连接池最大空闲连接数
global.DB.DB().SetMaxOpenConns(100) //设置数据库最大连接数
global.DB.DB().SetConnMaxLifetime(100) //设置数据库连接超时时间
// 测试数据库连接
if err = global.DB.DB().Ping(); err != nil {
fmt.Printf("[Initialize] 数据库连接测试失败:%v\n", err)
os.Exit(0)
}
fmt.Println("[Initialize] 数据库连接测试成功")
}
redis.og
go
package Initialize
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"im/global"
"log"
"sync"
"time"
)
var once sync.Once
func InitRedis() {
addr := fmt.Sprintf("%v:%v", global.SrvConfig.RedisInfo.IP, global.SrvConfig.RedisInfo.Port)
// once.Do() 在一个应用程序生命周期内只会执行一次
once.Do(func() {
global.Redis = redis.NewClient(&redis.Options{
Network: "tcp",
Addr: addr,
Password: "",
DB: 0, // 指定Redis服务器的数据库索引,0为默认
PoolSize: 15, // 连接池最大连接数
MinIdleConns: 10, // 连接池最小连接数
DialTimeout: 5 * time.Second, // 连接超时时间
ReadTimeout: 3 * time.Second, // 读超时时间
WriteTimeout: 3 * time.Second, // 写超时时间
PoolTimeout: 4 * time.Second, // 连接池获取连接的超时时间
IdleCheckFrequency: 60 * time.Second,
IdleTimeout: 5 * time.Minute,
MaxConnAge: 0 * time.Second,
MaxRetries: 0,
MinRetryBackoff: 8 * time.Millisecond,
MaxRetryBackoff: 512 * time.Millisecond,
})
pong, err := global.Redis.Ping(context.Background()).Result()
if err != nil {
log.Fatal(err)
}
log.Println(pong)
})
}
4、路由设计
go
// 注册
r.POST("/api/register", handle.Register)
// 已注册用户列表
r.GET("/api/list", handle.UserList)
// 登录
r.POST("/api/login", handle.Login)
// ws连接
r.GET("/api/ws", handle.WS)
// 获取登录列表(目前没用到)
r.GET("/api/loginlist", handle.LoginList)
// JWT
r.Use(middlewares.JWT())
// 获取用户名
r.GET("/api/user", handle.UserInfo)
5、核心功能设计
handle/handle.go
go
package handle
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"im/global"
"im/middlewares"
"im/mysql"
"io/ioutil"
"net/http"
"os"
"time"
)
type Reg struct {
Name string `json:"name"`
Password string `json:"password"`
}
type UList struct {
Names []string `json:"names"`
}
type LoginStruct struct {
Name string `json:"name" `
Password string `json:"password" `
}
func Register(c *gin.Context) {
var reg Reg
err := c.Bind(®)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusOK, gin.H{
"msg": "用户名或密码格式错误,请重试",
"code": "4001",
})
return
}
mysql.StorageUserToMap()
_, ok := global.UserMap.Load(reg.Name)
if ok {
fmt.Println("用户已存在")
c.JSON(http.StatusOK, gin.H{
"msg": "用户已存在,请登录或更换用户名注册",
"code": "4000",
})
return
}
if err := mysql.AddUserToMysql(reg.Name, reg.Password); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "内部错误",
"code": "5000",
})
return
}
mysql.StorageUserToMap()
c.JSON(http.StatusOK, gin.H{
"msg": "创建用户成功,请登录",
"code": "2000",
})
}
func Login(c *gin.Context) {
var loginData LoginStruct
err := c.Bind(&loginData)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusOK, gin.H{
"msg": "用户名或密码格式错误,请重试",
"code": "4001",
})
return
}
psw, ok := global.UserMap.Load(loginData.Name)
if !ok {
fmt.Println("用户不存在")
c.JSON(http.StatusOK, gin.H{
"msg": "用户不存在,请注册",
"code": "4003",
})
return
}
if loginData.Password != psw.(string) {
c.JSON(http.StatusOK, gin.H{
"msg": "密码错误,请重新输入",
"code": "4005",
})
return
}
file, err := os.Open(global.SrvConfig.JWTInfo.PrivateKeyPath)
if err != nil {
fmt.Println(err)
return
}
pkBytes, err := ioutil.ReadAll(file)
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(pkBytes))
tokenGen := middlewares.NewJWTTokenGen("user", privateKey)
token, err := tokenGen.GenerateToken(loginData.Name, time.Hour*24*20)
if err != nil {
fmt.Println(err)
return
}
c.JSON(http.StatusOK, &gin.H{
"msg": "登录成功",
"code": "2000",
"name": loginData.Name,
"token": token,
})
}
func LoginList(c *gin.Context) {
var users UList
global.LoginMap.Range(func(key, value interface{}) bool {
users.Names = append(users.Names, key.(string))
return true
})
c.JSON(http.StatusOK, &users)
}
func getLoginList() *UList {
var users UList
global.LoginMap.Range(func(key, value interface{}) bool {
users.Names = append(users.Names, key.(string))
return true
})
return &users
}
func UserInfo(c *gin.Context) {
name, _ := c.Get("name")
userName := name.(string)
c.JSON(http.StatusOK, gin.H{
"msg": "成功",
"code": "2000",
"name": userName,
})
}
func UserList(c *gin.Context) {
var users UList
global.UserMap.Range(func(key, value interface{}) bool {
users.Names = append(users.Names, key.(string))
return true
})
c.JSON(http.StatusOK, &users)
}
ws.go
go
// websocket 通信
package handle
import (
"container/list"
"context"
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"im/global"
"im/middlewares"
"log"
"net/http"
)
type WsInfo struct {
Type string `json:"type"`
Content string `json:"content"`
To []string `json:"to"`
From string `json:"from"`
}
func WS(ctx *gin.Context) {
var claim *middlewares.MyClaim
wsConn, _ := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
for {
_, data, err := wsConn.ReadMessage()
if err != nil {
wsConn.Close()
if claim != nil {
RemoveWSConnFromMap(claim.Subject, wsConn)
r, _ := json.Marshal(gin.H{
"type": "loginlist",
"content": getLoginList(),
"to": []string{},
})
SendMsgToAllLoginUser(r)
}
fmt.Println(claim.Subject, "出错,断开连接:", err)
fmt.Println("当前在线用户列表:", getLoginList().Names)
return
}
var wsInfo WsInfo
json.Unmarshal(data, &wsInfo)
if wsInfo.Type == "auth" {
claim, err = middlewares.Auth(wsInfo.Content)
if err != nil {
// 认证失败
fmt.Println(err)
rsp := WsInfo{
Type: "no",
Content: "认证失败,请重新登录",
To: []string{},
}
r, _ := json.Marshal(rsp)
wsConn.WriteMessage(websocket.TextMessage, r)
wsConn.Close()
continue
}
// 认证成功
// 将连接加入map记录
AddWSConnToMap(claim.Subject, wsConn)
fmt.Println(claim.Subject, " 加入连接")
fmt.Println("当前在线用户列表:", getLoginList().Names)
rsp := WsInfo{
Type: "ok",
Content: "连接成功,请发送消息",
To: []string{},
}
r, _ := json.Marshal(rsp)
// 更新登录列表
wsConn.WriteMessage(websocket.TextMessage, r)
r, _ = json.Marshal(gin.H{
"type": "loginlist",
"content": getLoginList(),
"to": []string{},
})
SendMsgToAllLoginUser(r)
// 发送离线消息
cmd := global.Redis.LRange(context.Background(), claim.Subject, 0, -1)
msgs, err := cmd.Result()
if err != nil {
log.Println(err)
continue
}
for _, msg := range msgs {
wsConn.WriteMessage(websocket.TextMessage, []byte(msg))
}
global.Redis.Del(context.Background(), claim.Subject)
} else {
rsp, _ := json.Marshal(gin.H{
"type": "normal",
"content": wsInfo.Content,
"to": []string{},
"from": claim.Subject,
})
SendMsgToOtherUser(rsp, claim.Subject, wsInfo.To...)
}
}
wsConn.Close()
}
var (
Upgrader = websocket.Upgrader{
//允许跨域
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)
func AddWSConnToMap(userName string, wsConn *websocket.Conn) {
// 同一用户可以有多个ws连接(登录多次)
loginListInter, ok := global.LoginMap.Load(userName)
if !ok {
// 之前没登录
loginList := list.New()
loginList.PushBack(wsConn)
global.LoginMap.Store(userName, loginList)
} else {
// 多次登录
loginList := loginListInter.(*list.List)
loginList.PushBack(wsConn)
global.LoginMap.Store(userName, loginList)
}
}
func RemoveWSConnFromMap(userName string, wsConn *websocket.Conn) {
loginListInter, ok := global.LoginMap.Load(userName)
if !ok {
fmt.Println("没有连接可以关闭")
} else {
// 有连接
loginList := loginListInter.(*list.List)
if loginList.Len() <= 1 {
global.LoginMap.Delete(userName)
} else {
for e := loginList.Front(); e != nil; e = e.Next() {
if e.Value.(*websocket.Conn) == wsConn {
loginList.Remove(e)
break
}
}
global.LoginMap.Store(userName, loginList)
}
}
}
func SendMsgToOtherUser(data []byte, myName string, otherUserName ...string) {
for _, otherName := range otherUserName {
if otherName != myName {
v, ok := global.LoginMap.Load(otherName)
if ok {
// 在线,发送给目标用户的所有客户端
l := v.(*list.List)
for e := l.Front(); e != nil; e = e.Next() {
conn := e.Value.(*websocket.Conn)
conn.WriteMessage(websocket.TextMessage, data)
}
} else {
_, ok := global.UserMap.Load(otherName)
if ok {
//离线消息缓存到redis
global.Redis.LPush(context.Background(), otherName, data)
}
}
}
}
}
func SendMsgToAllLoginUser(data []byte) {
global.LoginMap.Range(func(key, value interface{}) bool {
l := value.(*list.List)
for e := l.Front(); e != nil; e = e.Next() {
conn := e.Value.(*websocket.Conn)
conn.WriteMessage(websocket.TextMessage, data)
}
return true
})
}
mysql数据读取 mysql.go
go
package mysql
import (
"fmt"
"im/global"
)
type User struct {
UserName string `gorm:"column:username"`
Password string `gorm:"column:password"`
}
func StorageUserToMap() {
var users []User
err := global.DB.Find(&users).Error
if err != nil {
fmt.Printf("[mysql] 查询用户失败:%v\n", err)
return
}
// 将查询到的用户名和密码存储到 UserMap 中
for _, user := range users {
global.UserMap.Store(user.UserName, user.Password)
}
}
func AddUserToMysql(userName, psw string) error {
// 创建用户模型
user := User{
UserName: userName,
Password: psw,
}
// 插入用户记录
err := global.DB.Create(&user).Error
if err != nil {
fmt.Printf("[mysql] 注册失败:%v\n", err)
return err
}
fmt.Printf("[mysql] 注册成功\n")
return nil
}