IM即时聊天项目

目录

  • IM即时聊天项目
    • WebSocket 原理
    • 搭建环境
      • 设置代理
      • 创建环境
      • 配置驱动(搭建环境需要的驱动)
      • conf(配置信息)
      • cache(redis)
      • model(数据库连接)
    • 用户注册
      • serializer
      • model
      • service
      • api
      • router
      • main.go
    • 升级成WebSocket协议
      • service
      • router
    • WebSocket连接
      • pkg文件夹
        • e文件夹
      • service
        • ws.go
        • start.go
      • main
    • 写信息到ws通道中
      • model
        • ws
      • service
        • find.go
        • start.go
    • 获取历史消息
      • model
        • ws
      • service
        • find.go
        • ws.go

IM即时聊天项目

基于 WebSocket + MongoDB + MySQL + Redis

  • MySQL 用来存储用户基本信息
  • MongoDB 用来存放用户聊天信息
  • Redis 用来存储处理过期信息

WebSocket 原理

WebSocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手。
握手成功后,数据就直接从TCP通道传输,与HTTP无关了。即:WebSocket分为握手和数据传输阶段。
即进行了 HTTP 握手 + 双工的 TCP 连接。

  • WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
  • WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

在这里插入图片描述

  • 像左图这样的不断发送 http 请求,轮询的效率是非常低,非常浪费资源,所以就有了 websocket 协议了,建立在 TCP 协议之上,服务器端的实现比较容易。
  • WebSocket 协议一旦建立之后,互相沟通所消耗的请求头是很小的,服务器向客户端推送消息的功耗就小了。

搭建环境

设置代理

https://goproxy.cn,direct

在这里插入图片描述

创建环境

创建main.go文件

创建管理依赖包文件

go mod init IM

创建文件夹

在这里插入图片描述

  • api (接收路由传过来的信息,返回给service层进行处理)
  • cache (redis)
  • conf (配置信息)
  • model(数据库连接,实体层)
  • pkg
    • e (状态码)
    • utils (工具)
  • router (路由)
  • serializer (序列化)
  • service (服务)

配置驱动(搭建环境需要的驱动)

  • ini驱动
go get gopkg.in/ini.v1
  • redis驱动
go get github.com/go-redis/redis
  • 数据库驱动
go get github.com/jinzhu/gorm/dialects/mysql
  • gorm
go get github.com/jinzhu/gorm
  • gin框架
go get github.com/gin-gonic/gin
  • MongoDB驱动
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options
  • 日志包
go get github.com/sirupsen/logrus

导入websocket

go get github.com/gorilla/websocket

conf(配置信息)

创建 conf.go文件

导入MongoDB驱动

go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options

导入ini驱动

go get gopkg.in/ini.v1

conf.go文件内容:

package conf

import (
	"IM/model"
	"context"
	"fmt"
	logging "github.com/sirupsen/logrus"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"gopkg.in/ini.v1"
	"strings"
)

var (
	MongoDBClient   *mongo.Client
	AppMode         string
	HttpPort        string
	Db              string
	DbHost          string
	DbPort          string
	DbUser          string
	DbPassword      string
	DbName          string
	MongoDBPort     string
	MongoDBHost     string
	MongoDBName     string
	MongoDBPassword string
)

func Init() {
	//从本地读取环境
	file, err := ini.Load("./conf/config.ini")
	if err != nil {
		fmt.Println("加载ini文件失败", err)
	}
	LoadServer(file)
	LoadMySQL(file)
	LoadMongoDB(file)
	MongoDB() //MongoDB连接
	path := strings.Join([]string{DbUser, ":", DbPassword, "@tcp(", DbHost, ":", DbPort, ")/", DbName, "?charset=utf8mb4&parseTime=true"}, "")
	model.Database(path) //数据库连接
}

// MongoDB连接
func MongoDB() {
	clientOptions := options.Client().ApplyURI("mongodb://" + MongoDBHost + ":" + MongoDBPort)
	var err error
	MongoDBClient, err = mongo.Connect(context.TODO(), clientOptions)
	if err != nil {
		logging.Info(err)
		panic(err)
	}
	logging.Info("MongoDB 连接成功")
}

func LoadServer(file *ini.File) {
	AppMode = file.Section("service").Key("AppMode").String()
	HttpPort = file.Section("service").Key("HttpPort").String()
}

func LoadMySQL(file *ini.File) {
	Db = file.Section("mysql").Key("Db").String()
	DbHost = file.Section("mysql").Key("DbHost").String()
	DbPort = file.Section("mysql").Key("DbPort").String()
	DbUser = file.Section("mysql").Key("DbUser").String()
	DbPassword = file.Section("mysql").Key("DbPassword").String()
	DbName = file.Section("mysql").Key("DbName").String()
}

func LoadMongoDB(file *ini.File) {
	MongoDBPort = file.Section("MongoDB").Key("MongoDBPort").String()
	MongoDBHost = file.Section("MongoDB").Key("MongoDBHost").String()
	MongoDBName = file.Section("MongoDB").Key("MongoDBName").String()
	MongoDBPassword = file.Section("MongoDB").Key("MongoDBPassword").String()

创建 config.ini文件

#debug开发模式, release生产模式
[service]
AppMode=debug
HttpPort=:3000

[mysql]
Db=mysql
DbHost=127.0.0.1
DbPort=3306
DbUser=root
DbPassword=123456
DbName=IM

[redis]
RedisDb=redis
RedisHost=127.0.0.1
RedisPort=6379
RedisPassword=123456
RedisDbName=2

[MongoDB]
MongoDBPort=27017
MongoDBHost=localhost
MongoDBName=userV1
MongoDBPassword=root

cache(redis)

创建 common.go文件

导入redis驱动

go get github.com/go-redis/redis

导入日志包

go get github.com/sirupsen/logrus

common.go文件内容:

package cache

import (
	"fmt"
	"github.com/go-redis/redis"
	logging "github.com/sirupsen/logrus"
	"gopkg.in/ini.v1"
	"strconv"
)

var (
	RedisClient   *redis.Client
	RedisDb       string
	RedisHost     string
	RedisPort     string
	RedisPassword string
	RedisDbName   string
)

func init() {
	file, err := ini.Load("./conf/config.ini") //加载配置信息文件
	if err != nil {
		fmt.Println("加载redis ini文件失败", err)
	}

	LoadRedis(file) //读取配置信息文件内容
	Redis()         //连接redis
}

// redis加载
func LoadRedis(file *ini.File) {
	RedisDb = file.Section("redis").Key("RedisDb").String()
	RedisHost = file.Section("redis").Key("RedisHost").String()
	RedisPort = file.Section("redis").Key("RedisPort").String()
	RedisPassword = file.Section("redis").Key("RedisPassword").String()
	RedisDbName = file.Section("redis").Key("RedisDbName").String()
}

// redis连接
func Redis() {
	db, _ := strconv.ParseUint(RedisDbName, 10, 64)
	client := redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%s", RedisHost, RedisPort),
		DB:       int(db),
		Password: RedisPassword,
	})
	_, err := client.Ping().Result()
	if err != nil {
		logging.Info(err)
		panic(err)
	}
	RedisClient = client
}

model(数据库连接)

创建 init.go文件

导入数据库驱动

go get github.com/jinzhu/gorm/dialects/mysql

导入gorm

go get github.com/jinzhu/gorm

导入gin框架

go get github.com/gin-gonic/gin

init.go文件内容:

package model

import (
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"time"
)

var DB *gorm.DB

func Database(connstring string) {
	db, err := gorm.Open("mysql", connstring)
	if err != nil {
		panic("mysql数据库连接错误")
	}
	db.LogMode(true)
	//如果是发行版就不用输出日志
	if gin.Mode() == "release" {
		db.LogMode(false)
	}
	db.SingularTable(true)                       //表名不加s,user
	db.DB().SetMaxIdleConns(20)                  //设置连接池
	db.DB().SetMaxOpenConns(100)                 //最大连接数
	db.DB().SetConnMaxLifetime(time.Second * 30) //连接时间
	DB = db
}

用户注册

serializer

创建 common.go文件

common.go文件内容:

package serializer

/*
错误信息序列化
*/

// Response 基础序列化器
type Response struct {
	Status int         `json:"status"`
	Data   interface{} `json:"data"`
	Msg    string      `json:"msg"`
	Error  string      `json:"error"`
}

model

创建 user.go文件

user.go文件内容:

package model

import (
	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	gorm.Model
	UserName string `gorm:"unique"`
	PassWord string
}

const (
	PassWordCost = 12 //密码加密难度
)

// SetPassWord 设置密码
func (user *User) SetPassWord(password string) error {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), PassWordCost)
	if err != nil {
		return err
	}
	user.PassWord = string(bytes)
	return nil
}

// CheckPassWord 校验密码
func (user *User) CheckPassWord(password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(user.PassWord), []byte(password))
	return err == nil
}

创建 migration.go文件

migration.go文件内容:

package model

// 迁移
func migration() {
	DB.Set("gorm:table_options", "charset=utf8mb4").AutoMigrate(&User{})
}

在model层init.go最后加migration()

package model

import (
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"time"
)

var DB *gorm.DB

func Database(connstring string) {
	db, err := gorm.Open("mysql", connstring)
	if err != nil {
		panic("mysql数据库连接错误")
	}
	db.LogMode(true)
	//如果是发行版就不用输出日志
	if gin.Mode() == "release" {
		db.LogMode(false)
	}
	db.SingularTable(true)                       //表名不加s,user
	db.DB().SetMaxIdleConns(20)                  //设置连接池
	db.DB().SetMaxOpenConns(100)                 //最大连接数
	db.DB().SetConnMaxLifetime(time.Second * 30) //连接时间
	DB = db
	//迁移
	migration()
}

service

创建 user.go文件

user.go文件内容:

package service

import (
	"IM/model"
	"IM/serializer"
)

type UserRegisterService struct {
	UserName string `json:"user_name" form:"user_name"`
	PassWord string `json:"password" form:"password"`
}

// 用户注册
func (service *UserRegisterService) Register() serializer.Response {
	var user model.User
	count := 0
	model.DB.Model(&model.User{}).Where("user_name=?", service.UserName).First(&user).Count(&count)
	if count != 0 {
		return serializer.Response{
			Status: 400,
			Msg:    "用户名已经存在了",
		}
	}
	user = model.User{
		UserName: service.UserName,
	}
	//密码加密
	if err := user.SetPassWord(service.PassWord); err != nil {
		return serializer.Response{
			Status: 500,
			Msg:    "加密出错了",
		}
	}
	model.DB.Create(&user)
	return serializer.Response{
		Status: 200,
		Msg:    "创建成功",
	}
}

api

创建 common.go文件

common.go文件内容:

package api

/*
返回错误信息
*/

import (
	"IM/serializer"
	"encoding/json"
	"fmt"
	"github.com/go-playground/validator/v10"
)

// 返回错误信息 ErrorResponse
func ErrorResponse(err error) serializer.Response {
	if _, ok := err.(validator.ValidationErrors); ok {
		return serializer.Response{
			Status: 400,
			Msg:    "错误参数",
			Error:  fmt.Sprint(err),
		}
	}
	if _, ok := err.(*json.UnmarshalTypeError); ok {
		return serializer.Response{
			Status: 400,
			Msg:    "JSON类型不匹配",
			Error:  fmt.Sprint(err),
		}
	}

	return serializer.Response{
		Status: 400,
		Msg:    "参数错误",
		Error:  fmt.Sprint(err),
	}
}

创建 user.go文件

user.go文件内容:

package api

import (
	"IM/service"
	"github.com/gin-gonic/gin"
	logging "github.com/sirupsen/logrus"
)

// 用户注册
func UserRegister(c *gin.Context) {
	var userRegisterService service.UserRegisterService
	if err := c.ShouldBind(&userRegisterService); err == nil {
		res := userRegisterService.Register()
		c.JSON(200, res)
	} else {
		c.JSON(400, ErrorResponse(err))
		logging.Info(err)
	}
}

router

创建 router.go文件

router.go文件内容:

package router

import (
	"IM/api"
	"github.com/gin-gonic/gin"
)

func NewRouter() *gin.Engine {
	r := gin.Default()
	//Recovery 中间件会恢复(recovers) 任何恐慌(panics)
	//如果存在恐慌中间件将会写入500
	//因为当你程序里有些异常情况你没考虑到的时候,程序就退出了,服务就停止了
	//Logger日志
	r.Use(gin.Recovery(), gin.Logger())
	v1 := r.Group("/")
	{
		//测试是否成功
		v1.GET("ping", func(c *gin.Context) {
			c.JSON(200, "成功")
		})
		//用户注册
		v1.POST("user/register", api.UserRegister)
	}
	return r
}

main.go

package main

import (
	"IM/conf"
	"IM/router"
)

func main() {
	//测试初始化
	conf.Init()
    //启动路由
	r := router.NewRouter()
	_ = r.Run(conf.HttpPort)
}

升级成WebSocket协议

导入websocket

go get github.com/gorilla/websocket

service

创建 ws.go文件

ws.go文件内容:

package service

import (
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"net/http"
)

const month = 60 * 60 * 24 * 30 //一个月30天

// 发送消息的结构体
type SendMsg struct {
	Type    int    `json:"type"`
	Content string `json:"content"`
}

// 回复消息的结构体
type ReplyMsg struct {
	From    string `json:"from"`
	Code    int    `json:"code"`
	Content string `json:"content"`
}

// 用户结构体
type Client struct {
	ID     string          //用户id
	SendID string          //接收id
	Socket *websocket.Conn //Socket连接
	Send   chan []byte     //发送的信息
}

// 广播类(包括广播内容和源用户)
type Broadcast struct {
	Client  *Client
	Message []byte
	Type    int
}

// 用户管理
type ClientManager struct {
	Clients    map[string]*Client
	Broadcast  chan *Broadcast //广播
	Reply      chan *Client
	Register   chan *Client //已注册
	Unregister chan *Client //未注册
}

// 信息转JSON(包括:发送者、接收者、内容)
type Message struct {
	Sender    string `json:"sender,omitempty"`    //发送者
	Recipient string `json:"recipient,omitempty"` //接收者
	Content   string `json:"content,omitempty"`   //内容
}

// 初始化一个全局管理Manager
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 CreateID(uid, toUid string) string {
	return uid + "->" + toUid //1->2
}
func Handler(c *gin.Context) {
	uid := c.Query("uid")
	toUid := c.Query("toUid")
	conn, err := (&websocket.Upgrader{
		//跨域
		CheckOrigin: func(r *http.Request) bool {
			return true
		}}).Upgrade(c.Writer, c.Request, nil) //升级ws协议
	if err != nil {
		http.NotFound(c.Writer, c.Request)
		return
	}
	//创建一个用户实例
	client := &Client{
		ID:     CreateID(uid, toUid), //发送方 1发送给2
		SendID: CreateID(toUid, uid), //接收方 2接收到1
		Socket: conn,                 //Socket连接
		Send:   make(chan []byte),    //发送的信息
	}
	//用户注册到用户管理上
	Manager.Register <- client
	go client.Read()
	go client.Write()
}

// 读操作
func (c *Client) Read() {

}

// 写操作
func (c *Client) Write() {

}

router

router.go文件中添加:

  • //升级WebSocket协议
    v1.GET("ws", service.Handler)
    

完整内容:

package router

import (
	"IM/api"
	"IM/service"
	"github.com/gin-gonic/gin"
)

func NewRouter() *gin.Engine {
	r := gin.Default()
	//Recovery 中间件会恢复(recovers) 任何恐慌(panics)
	//如果存在恐慌中间件将会写入500
	//因为当你程序里有些异常情况你没考虑到的时候,程序就退出了,服务就停止了
	//Logger日志
	r.Use(gin.Recovery(), gin.Logger())
	v1 := r.Group("/")
	{
		//测试是否成功
		v1.GET("ping", func(c *gin.Context) {
			c.JSON(200, "成功")
		})
		//用户注册
		v1.POST("user/register", api.UserRegister)
		//升级WebSocket协议
		v1.GET("ws", service.Handler)
	}
	return r
}

WebSocket连接

pkg文件夹

e文件夹

创建 code.go文件

code.go文件内容:

package e

const (
	SUCCESS               = 200
	UpdatePasswordSuccess = 201   //密码成功
	NotExistInentifier    = 202   //未绑定
	ERROR                 = 500   //失败
	InvalidParams         = 400   //请求参数错误
	ErrorDatabase         = 40001 //数据库操作错误

	WebsocketSuccessMessage = 50001 //解析content内容信息
	WebsocketSuccess        = 50002 //请求历史纪录操作成功
	WebsocketEnd            = 50003 //请求没有更多历史记录
	WebsocketOnlineReply    = 50004 //在线应答
	WebsocketOfflineReply   = 50005 //离线回答
	WebsocketLimit          = 50006 //请求受到限制
)

创建 msg.go文件

msg.go文件内容:

package e

var MsgFlags = map[int]string{
	SUCCESS:                 "ok",
	UpdatePasswordSuccess:   "修改密码成功",
	NotExistInentifier:      "该第三方账号未绑定",
	ERROR:                   "失败",
	InvalidParams:           "请求参数错误",
	ErrorDatabase:           "数据库操作出错,请重试",
	WebsocketSuccessMessage: "解析content内容信息",
	WebsocketSuccess:        "发送信息,请求历史纪录操作成功",
	WebsocketEnd:            "请求历史纪录,但没有更多记录了",
	WebsocketOnlineReply:    "针对回复信息在线应答成功",
	WebsocketOfflineReply:   "针对回复信息离线回答成功",
	WebsocketLimit:          "请求受到限制",
}

// GetMsg 获取状态码对应信息
func GetMsg(code int) string {
	msg, ok := MsgFlags[code]
	if ok {
		return msg
	}
	return MsgFlags[ERROR]
}

service

ws.go

ws.go中的Read()操作:

// 读操作
func (c *Client) Read() {
	//结束时关闭Socket
	defer func() {
		//用户结构体变成未注册状态
		Manager.Unregister <- c
		//关闭Socket
		_ = c.Socket.Close()
	}()
	for {
		c.Socket.PongHandler()
		sendMsg := new(SendMsg)
		//序列化
		//如果传过来是String类型,用这个接收: c.Socket.ReadMessage()
		err := c.Socket.ReadJSON(&sendMsg)
		if err != nil {
			fmt.Println("数据格式不正确", err)
			Manager.Unregister <- c
			_ = c.Socket.Close()
			break
		}
		if sendMsg.Type == 1 { // 设置1为发送消息
			r1, _ := cache.RedisClient.Get(c.ID).Result()     //1->2  查看缓存里有没有发送方id
			r2, _ := cache.RedisClient.Get(c.SendID).Result() //2->1  查看缓存里有没有接收方id
			if r1 > "3" && r2 == "" {                         //1给2发消息,发了三条,但是2没有回,或者没有看到,就停止1发送。防止骚扰
				replyMsg := ReplyMsg{
					Code:    e.WebsocketLimit,
					Content: e.GetMsg(e.WebsocketLimit),
				}
				msg, _ := json.Marshal(replyMsg) //序列化
				_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
				continue
			} else {
				//存储到redis中
				cache.RedisClient.Incr(c.ID)
				_, _ = cache.RedisClient.Expire(c.ID, time.Hour*24*30*3).Result() //防止过快“分手”,建立连接三个月过期
			}
			log.Println(c.ID, "发送消息", sendMsg.Content)
			//广播出去
			Manager.Broadcast <- &Broadcast{
				Client:  c,
				Message: []byte(sendMsg.Content), //发送过来的消息
			}
		}
	}
}

ws.go中的Write()操作:

// 写操作
func (c *Client) Write() {
	defer func() {
		_ = c.Socket.Close()
	}()
	for {
		select {
		case message, ok := <-c.Send:
			if !ok {
				_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			log.Println(c.ID, "接受消息:", string(message))
			replyMsg := ReplyMsg{
				Code:    e.WebsocketSuccessMessage,
				Content: fmt.Sprintf("%s", string(message)),
			}
			msg, _ := json.Marshal(replyMsg)
			_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
		}
	}
}

全部:

package service

import (
	"IM/cache"
	"IM/pkg/e"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
	"time"
)

const month = 60 * 60 * 24 * 30 //一个月30天

// 发送消息的结构体
type SendMsg struct {
	Type    int    `json:"type"`
	Content string `json:"content"`
}

// 回复消息的结构体
type ReplyMsg struct {
	From    string `json:"from"`
	Code    int    `json:"code"`
	Content string `json:"content"`
}

// 用户结构体
type Client struct {
	ID     string          //用户id
	SendID string          //接收id
	Socket *websocket.Conn //Socket连接
	Send   chan []byte     //发送的信息
}

// 广播类(包括广播内容和源用户)
type Broadcast struct {
	Client  *Client
	Message []byte
	Type    int
}

// 用户管理
type ClientManager struct {
	Clients    map[string]*Client
	Broadcast  chan *Broadcast //广播
	Reply      chan *Client
	Register   chan *Client //已注册
	Unregister chan *Client //未注册
}

// 信息转JSON(包括:发送者、接收者、内容)
type Message struct {
	Sender    string `json:"sender,omitempty"`    //发送者
	Recipient string `json:"recipient,omitempty"` //接收者
	Content   string `json:"content,omitempty"`   //内容
}

// 初始化一个全局管理Manager
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 CreateID(uid, toUid string) string {
	return uid + "->" + toUid //1->2
}
func Handler(c *gin.Context) {
	uid := c.Query("uid")
	toUid := c.Query("toUid")
	conn, err := (&websocket.Upgrader{
		//跨域
		CheckOrigin: func(r *http.Request) bool {
			return true
		}}).Upgrade(c.Writer, c.Request, nil) //升级ws协议
	if err != nil {
		http.NotFound(c.Writer, c.Request)
		return
	}
	//创建一个用户实例
	client := &Client{
		ID:     CreateID(uid, toUid), //发送方 1发送给2
		SendID: CreateID(toUid, uid), //接收方 2接收到1
		Socket: conn,                 //Socket连接
		Send:   make(chan []byte),    //发送的信息
	}
	//用户注册到用户管理上
	Manager.Register <- client
	go client.Read()
	go client.Write()
}

// 读操作
func (c *Client) Read() {
	//结束时关闭Socket
	defer func() {
		//用户结构体变成未注册状态
		Manager.Unregister <- c
		//关闭Socket
		_ = c.Socket.Close()
	}()
	for {
		c.Socket.PongHandler()
		sendMsg := new(SendMsg)
		//序列化
		//如果传过来是String类型,用这个接收: c.Socket.ReadMessage()
		err := c.Socket.ReadJSON(&sendMsg)
		if err != nil {
			fmt.Println("数据格式不正确", err)
			Manager.Unregister <- c
			_ = c.Socket.Close()
			break
		}
		if sendMsg.Type == 1 { // 设置1为发送消息
			r1, _ := cache.RedisClient.Get(c.ID).Result()     //1->2  查看缓存里有没有发送方id
			r2, _ := cache.RedisClient.Get(c.SendID).Result() //2->1  查看缓存里有没有接收方id
			if r1 > "3" && r2 == "" {                         //1给2发消息,发了三条,但是2没有回,或者没有看到,就停止1发送。防止骚扰
				replyMsg := ReplyMsg{
					Code:    e.WebsocketLimit,
					Content: e.GetMsg(e.WebsocketLimit),
				}
				msg, _ := json.Marshal(replyMsg) //序列化
				_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
				continue
			} else {
				//存储到redis中
				cache.RedisClient.Incr(c.ID)
				_, _ = cache.RedisClient.Expire(c.ID, time.Hour*24*30*3).Result() //防止过快“分手”,建立连接三个月过期
			}
			log.Println(c.ID, "发送消息", sendMsg.Content)
			//广播出去
			Manager.Broadcast <- &Broadcast{
				Client:  c,
				Message: []byte(sendMsg.Content), //发送过来的消息
			}
		}
	}
}

// 写操作
func (c *Client) Write() {
	defer func() {
		_ = c.Socket.Close()
	}()
	for {
		select {
		case message, ok := <-c.Send:
			if !ok {
				_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			log.Println(c.ID, "接受消息:", string(message))
			replyMsg := ReplyMsg{
				Code:    e.WebsocketSuccessMessage,
				Content: fmt.Sprintf("%s", string(message)),
			}
			msg, _ := json.Marshal(replyMsg)
			_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
		}
	}
}

start.go

创建 start.go文件

start.go文件内容:

package service

import (
	"IM/pkg/e"
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
)

func (manager *ClientManager) Start() {
	for {
		fmt.Println("<---监听管道通信--->")
		select {
		case conn := <-Manager.Register: // 建立连接
			fmt.Printf("建立新连接: %v", conn.ID)
			Manager.Clients[conn.ID] = conn //把连接放到用户管理上
			replyMsg := ReplyMsg{
				Code:    e.WebsocketSuccess,
				Content: "已连接至服务器",
			}
			msg, _ := json.Marshal(replyMsg)
			_ = conn.Socket.WriteMessage(websocket.TextMessage, msg)
		}
	}
}

main

添加go service.Manager.Start()

package main

import (
	"IM/conf"
	"IM/router"
	"IM/service"
)

func main() {
	//测试初始化
	conf.Init()
	//监听管道
	go service.Manager.Start()
	//启动路由
	r := router.NewRouter()
	_ = r.Run(conf.HttpPort)
}

写信息到ws通道中

model

创建ws文件夹

ws

创建 trainer.go文件

trainer.go文件内容:

package ws

// 插入进MongoDB的数据类型
type Trainer struct {
	Content   string `bson:"content"`   // 内容
	StartTime int64  `bson:"startTime"` // 创建时间
	EndTime   int64  `bson:"endTime"`   // 过期时间
	Read      uint   `bson:"read"`      // 已读
}

service

find.go

创建 find.go文件

find.go文件内容:

package service

import (
	"IM/conf"
	"IM/model/ws"
	"context"
	"time"
)

func InsertMsg(database, id string, content string, read uint, expire int64) error {
	//插入到mongoDB中
	collection := conf.MongoDBClient.Database(database).Collection(id) //没有这个id集合的话,创建这个id集合
	comment := ws.Trainer{
		Content:   content,
		StartTime: time.Now().Unix(),
		EndTime:   time.Now().Unix() + expire,
		Read:      read,
	}
	_, err := collection.InsertOne(context.TODO(), comment)
	return err
}

start.go

添加断开连接和广播功能

package service

import (
	"IM/conf"
	"IM/pkg/e"
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
)

func (manager *ClientManager) Start() {
	for {
		fmt.Println("<---监听管道通信--->")
		select {
		case conn := <-Manager.Register: // 建立连接
			fmt.Printf("建立新连接: %v", conn.ID)
			Manager.Clients[conn.ID] = conn //把连接放到用户管理上
			replyMsg := &ReplyMsg{
				Code:    e.WebsocketSuccess,
				Content: "已连接至服务器",
			}
			msg, _ := json.Marshal(replyMsg)
			_ = conn.Socket.WriteMessage(websocket.TextMessage, msg)
		case conn := <-Manager.Unregister: //断开连接
			fmt.Printf("连接失败%s", conn.ID)
			if _, ok := Manager.Clients[conn.ID]; ok {
				replyMsg := &ReplyMsg{
					Code:    e.WebsocketEnd,
					Content: "连接中断",
				}
				msg, _ := json.Marshal(replyMsg)
				_ = conn.Socket.WriteMessage(websocket.TextMessage, msg)
				close(conn.Send)
				delete(Manager.Clients, conn.ID)
			}
		case broadcast := <-Manager.Broadcast: //1->2
			message := broadcast.Message
			sendId := broadcast.Client.SendID //2->1
			flag := false                     //默认对方是不在线的
			for id, conn := range Manager.Clients {
				if id != sendId {
					continue
				}
				select {
				case conn.Send <- message:
					flag = true
				default:
					close(conn.Send)
					delete(Manager.Clients, conn.ID)
				}
			}
			id := broadcast.Client.ID //1->2
			if flag {
				fmt.Println("对方在线")
				replyMsg := &ReplyMsg{
					Code:    e.WebsocketOnlineReply,
					Content: "对方在线应答",
				}
				msg, _ := json.Marshal(replyMsg)
				_ = broadcast.Client.Socket.WriteMessage(websocket.TextMessage, msg)
				/*
					把消息插入到MongoDB中:
						1代表已读(只要用户在线就判断已读)
						int64(3*month):过期时间
				*/
				err := InsertMsg(conf.MongoDBName, id, string(message), 1, int64(3*month))
				if err != nil {
					fmt.Println("插入一条消息失败", err)
				}
			} else {
				fmt.Println("对方不在线")
				replyMsg := &ReplyMsg{
					Code:    e.WebsocketOfflineReply,
					Content: "对方不在线应答",
				}
				msg, err := json.Marshal(replyMsg)
				_ = broadcast.Client.Socket.WriteMessage(websocket.TextMessage, msg)
				err = InsertMsg(conf.MongoDBName, id, string(message), 0, int64(3*month))
				if err != nil {
					fmt.Println("插入一条消息失败", err)
				}
			}
		}
	}
}

获取历史消息

model

ws

trainer.go文件内容:

package ws

// 插入进MongoDB的数据类型
type Trainer struct {
	Content   string `bson:"content"`   // 内容
	StartTime int64  `bson:"startTime"` // 创建时间
	EndTime   int64  `bson:"endTime"`   // 过期时间
	Read      uint   `bson:"read"`      // 已读
}

type Result struct {
	StartTime int64
	Msg       string
	Content   interface{}
	From      string
}

service

find.go

find.go文件内容:

package service

import (
	"IM/conf"
	"IM/model/ws"
	"context"
	"fmt"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo/options"
	"sort"
	"time"
)

// 排序用的结构体
type SendSortMsg struct {
	Content  string `json:"content"`
	Read     uint   `json:"read"`
	CreateAt int64  `json:"create_at"`
}

// 插入数据到mongoDB中
func InsertMsg(database, id string, content string, read uint, expire int64) error {
	//插入到mongoDB中
	collection := conf.MongoDBClient.Database(database).Collection(id) //没有这个id集合的话,创建这个id集合
	comment := ws.Trainer{
		Content:   content,
		StartTime: time.Now().Unix(),
		EndTime:   time.Now().Unix() + expire,
		Read:      read,
	}
	_, err := collection.InsertOne(context.TODO(), comment)
	return err
}

// 获取历史消息
func FindMany(database string, sendId string, id string, time int64, pageSize int) (results []ws.Result, err error) {
	var resultsMe []ws.Trainer  //id
	var resultsYou []ws.Trainer //sendId
	sendIdCollection := conf.MongoDBClient.Database(database).Collection(sendId)
	idCollection := conf.MongoDBClient.Database(database).Collection(id)
	sendIdTimeCursor, err := sendIdCollection.Find(context.TODO(),
		//顺序执行
		bson.D{},
		//限制大小
		options.Find().SetLimit(int64(pageSize)))
	idTimeCursor, err := idCollection.Find(context.TODO(),
		//顺序执行
		bson.D{},
		//限制大小
		options.Find().SetLimit(int64(pageSize)))
	//sort.Slice(results, func(i, j int) bool { return results[i].StartTime < results[j].StartTime })
	err = idTimeCursor.All(context.TODO(), &resultsMe)      // Id 发给对面的
	err = sendIdTimeCursor.All(context.TODO(), &resultsYou) // sendId 对面发过来的
	results, _ = AppendAndSort(resultsMe, resultsYou)
	return
}
func AppendAndSort(resultsMe, resultsYou []ws.Trainer) (results []ws.Result, err error) {
	for _, r := range resultsMe {
		sendSort := SendSortMsg{ //构造返回的msg
			Content:  r.Content,
			Read:     r.Read,
			CreateAt: r.StartTime,
		}
		result := ws.Result{ //构造返回所有的内容,包括传送者
			StartTime: r.StartTime,
			Msg:       fmt.Sprintf("%v", sendSort),
			From:      "me",
		}
		results = append(results, result)
	}
	for _, r := range resultsYou {
		sendSort := SendSortMsg{
			Content:  r.Content,
			Read:     r.Read,
			CreateAt: r.StartTime,
		}
		result := ws.Result{
			StartTime: r.StartTime,
			Msg:       fmt.Sprintf("%v", sendSort),
			From:      "you",
		}
		results = append(results, result)
	}
	// 进行排序
	sort.Slice(results, func(i, j int) bool { return results[i].StartTime < results[j].StartTime })
	return results, nil
}

ws.go

在读操作里面写历史消息

ws.go文件增加内容:

else if sendMsg.Type == 2 { //拉取历史消息
	timeT, err := strconv.Atoi(sendMsg.Content) // string转int64
	if err != nil {
		timeT = 999999999
	}
	results, _ := FindMany(conf.MongoDBName, c.SendID, c.ID, int64(timeT), 10) //获取10条历史消息
	//大于10条消息
	if len(results) > 10 {
		results = results[:10]
	} else if len(results) == 0 { //0条信息
		replyMsg := ReplyMsg{
			Code:    e.WebsocketEnd,
			Content: "到底了",
		}
		msg, _ := json.Marshal(replyMsg)
		_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
		continue
	}
	//如果是1到10条消息时
	for _, result := range results {
		replyMsg := ReplyMsg{
			From:    result.From,
			Content: result.Msg,
		}
		msg, _ := json.Marshal(replyMsg)
		_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
	}
}

ws.go文件完整内容:

package service

import (
	"IM/cache"
	"IM/conf"
	"IM/pkg/e"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
	"strconv"
	"time"
)

const month = 60 * 60 * 24 * 30 //一个月30天

// 发送消息的结构体
type SendMsg struct {
	Type    int    `json:"type"`
	Content string `json:"content"`
}

// 回复消息的结构体
type ReplyMsg struct {
	From    string `json:"from"`
	Code    int    `json:"code"`
	Content string `json:"content"`
}

// 用户结构体
type Client struct {
	ID     string          //用户id
	SendID string          //接收id
	Socket *websocket.Conn //Socket连接
	Send   chan []byte     //发送的信息
}

// 广播类(包括广播内容和源用户)
type Broadcast struct {
	Client  *Client
	Message []byte
	Type    int
}

// 用户管理
type ClientManager struct {
	Clients    map[string]*Client
	Broadcast  chan *Broadcast //广播
	Reply      chan *Client
	Register   chan *Client //已注册
	Unregister chan *Client //未注册
}

// 信息转JSON(包括:发送者、接收者、内容)
type Message struct {
	Sender    string `json:"sender,omitempty"`    //发送者
	Recipient string `json:"recipient,omitempty"` //接收者
	Content   string `json:"content,omitempty"`   //内容
}

// 初始化一个全局管理Manager
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 CreateID(uid, toUid string) string {
	return uid + "->" + toUid //1->2
}
func Handler(c *gin.Context) {
	uid := c.Query("uid")
	toUid := c.Query("toUid")
	conn, err := (&websocket.Upgrader{
		//跨域
		CheckOrigin: func(r *http.Request) bool {
			return true
		}}).Upgrade(c.Writer, c.Request, nil) //升级ws协议
	if err != nil {
		http.NotFound(c.Writer, c.Request)
		return
	}
	//创建一个用户实例
	client := &Client{
		ID:     CreateID(uid, toUid), //发送方 1发送给2
		SendID: CreateID(toUid, uid), //接收方 2接收到1
		Socket: conn,                 //Socket连接
		Send:   make(chan []byte),    //发送的信息
	}
	//用户注册到用户管理上
	Manager.Register <- client
	go client.Read()
	go client.Write()
}

// 读操作
func (c *Client) Read() {
	//结束时关闭Socket
	defer func() {
		//用户结构体变成未注册状态
		Manager.Unregister <- c
		//关闭Socket
		_ = c.Socket.Close()
	}()
	for {
		c.Socket.PongHandler()
		sendMsg := new(SendMsg)
		//序列化
		//如果传过来是String类型,用这个接收: c.Socket.ReadMessage()
		err := c.Socket.ReadJSON(&sendMsg)
		if err != nil {
			fmt.Println("数据格式不正确", err)
			Manager.Unregister <- c
			_ = c.Socket.Close()
			break
		}
		if sendMsg.Type == 1 { // 设置1为发送消息
			r1, _ := cache.RedisClient.Get(c.ID).Result()     //1->2  查看缓存里有没有发送方id
			r2, _ := cache.RedisClient.Get(c.SendID).Result() //2->1  查看缓存里有没有接收方id
			if r1 > "3" && r2 == "" {                         //1给2发消息,发了三条,但是2没有回,或者没有看到,就停止1发送。防止骚扰
				replyMsg := ReplyMsg{
					Code:    e.WebsocketLimit,
					Content: e.GetMsg(e.WebsocketLimit),
				}
				msg, _ := json.Marshal(replyMsg) //序列化
				_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
				continue
			} else {
				//存储到redis中
				cache.RedisClient.Incr(c.ID)
				_, _ = cache.RedisClient.Expire(c.ID, time.Hour*24*30*3).Result() //防止过快“分手”,建立连接三个月过期
			}
			log.Println(c.ID, "发送消息", sendMsg.Content)
			//广播出去
			Manager.Broadcast <- &Broadcast{
				Client:  c,
				Message: []byte(sendMsg.Content), //发送过来的消息
			}
		} else if sendMsg.Type == 2 { //拉取历史消息
			timeT, err := strconv.Atoi(sendMsg.Content) // string转int64
			if err != nil {
				timeT = 999999999
			}
			results, _ := FindMany(conf.MongoDBName, c.SendID, c.ID, int64(timeT), 10) //获取10条历史消息
			//大于10条消息
			if len(results) > 10 {
				results = results[:10]
			} else if len(results) == 0 { //0条信息
				replyMsg := ReplyMsg{
					Code:    e.WebsocketEnd,
					Content: "到底了",
				}
				msg, _ := json.Marshal(replyMsg)
				_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
				continue
			}
			//如果是1到10条消息时
			for _, result := range results {
				replyMsg := ReplyMsg{
					From:    result.From,
					Content: result.Msg,
				}
				msg, _ := json.Marshal(replyMsg)
				_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
			}
		}
	}
}

// 写操作
func (c *Client) Write() {
	defer func() {
		_ = c.Socket.Close()
	}()
	for {
		select {
		case message, ok := <-c.Send:
			if !ok {
				_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			log.Println(c.ID, "接受消息:", string(message))
			replyMsg := ReplyMsg{
				Code:    e.WebsocketSuccessMessage,
				Content: fmt.Sprintf("%s", string(message)),
			}
			msg, _ := json.Marshal(replyMsg)
			_ = c.Socket.WriteMessage(websocket.TextMessage, msg)
		}
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/102290.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

15、监测数据采集物联网应用开发步骤(11)

源码将于最后一遍文章给出下载 监测数据采集物联网应用开发步骤(10) 程序自动更新开发 前面章节写了部分功能模块开发&#xff1a; 日志或文本文件读写开发;Sqlite3数据库读写操作开发;定时器插件化开发;串口(COM)通讯开发;TCP/IP Client开发;TCP/IP Server 开发;modbus协议…

c语言开篇---跟着视频学C语言

标识符 标识符必须声明定义&#xff0c;可以是变量、函数或其他实体。 Int是标识符吗&#xff1f; 不是&#xff0c;int是c语言关键词&#xff0c;不是随意命名的 C语言关键词如下&#xff1a; 常量 不需要被声明&#xff0c;不能赋值更改。 printf函数 printf是由print打印…

CLIP:连接文本-图像

Contrastive Language-Image Pre-Training CLIP的主要目标是通过对比学习&#xff0c;学习匹配图像和文本。CLIP最主要的作用&#xff1a;可以将文本和图像表征映射到同一个表示空间 这是通过训练模型来预测哪个图像属于给定的文本&#xff0c;反之亦然。在训练过程中&#…

《Go 语言第一课》课程学习笔记(十二)

函数 Go 函数与函数声明 在 Go 语言中&#xff0c;函数是唯一一种基于特定输入&#xff0c;实现特定任务并可返回任务执行结果的代码块&#xff08;Go 语言中的方法本质上也是函数&#xff09;。在 Go 中&#xff0c;我们定义一个函数的最常用方式就是使用函数声明。 第一部…

软件测试/测试开发丨Python 学习笔记 之 链表

点此获取更多相关资料 本文为霍格沃兹测试开发学社学员学习笔记分享 原文链接&#xff1a;https://ceshiren.com/t/topic/26458 链表与数组的区别 复杂度分析 时间复杂度数组链表插入删除O(n)O(1)随机访问O(1)O(n) 其他角度分析 内存连续&#xff0c;利用CPU的机制&#xff0…

中间件环境搭建配置过程解读

中间件环境搭建 目录 中间件环境搭建xampp 搭建环境Tomcat环境配置安装mysql连接mysql 问题解决 xampp 搭建环境 安装xampp服务集成环境工具 官网地址下载项目压缩包&#xff0c;将项目文件夹放在xampp安装目录的htdocs文件夹下初始化xampp&#xff1a;运行目录内的setup_xamp…

idea远程debug调试

背景 有时候我们线上/测试环境出现了问题&#xff0c;我们本地跑却无法复现问题&#xff0c;使用idea的远程debug功能可以很好的解决该问题 配置 远程debug的服务&#xff0c;我们使用Springboot项目为例(SpringCloud作为微服务项目我们可以可以使用本地注册到远程项目&…

QT day1登录界面设计

要设计如下图片&#xff1a; 代码如下&#xff1a; main.cpp widget.h widget.cpp 运行效果&#xff1a; 2&#xff0c;思维导图

关于 MySQL、PostgresSQL、Mariadb 数据库2038千年虫问题

MySQL 测试时间&#xff1a;2023-8 启动MySQL服务后&#xff0c;将系统时间调制2038年01月19日03时14分07秒之后的日期&#xff0c;发现MySQL服务自动停止。 根据最新的MySQL源码&#xff08;mysql-8.1.0&#xff09;分析&#xff0c;sql/sql_parse.cc中依然存在2038年千年虫…

黑马 大事件项目 笔记

学习视频&#xff1a;黑马 Vue23 课程 后台数据管理系统 - 项目架构设计 在线演示&#xff1a;https://fe-bigevent-web.itheima.net/login 接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835 接口根路径&#xff1a; http:/…

三维点云转换为二维图像

文章目录 前言原理代码总结与反思实验结果展示 前言 目的&#xff1a;将三维点云转换为二维图像 作用&#xff1a; a.给点云赋予彩色信息&#xff0c;增强点云所表达物体或对象的辨识度&#xff1b; b.将三维点云中绘制的目标物体通过映射关系绘制到二维图像中&#xff0c;这个…

报错处理:Disk space full

报错环境&#xff1a; Linux 具体报错&#xff1a; No space left on device&#xff0c;磁盘空间已满 排错思路&#xff1a; 当磁盘空间耗尽时&#xff0c;会出现磁盘空间已满的错误。这可能是由于磁盘上的文件过多或者某个文件系统占用了过多磁盘空间。 解决方法&#xff1a;…

UE5- c++ websocket客户端写法

# 实现目标 ue5 c 实现socket客户端&#xff0c;读取服务端数据&#xff0c;并进行解析 #实现步骤 {projectName}.Build.cs里增加 "WebSockets","JsonUtilities", "Json"配置信息&#xff0c;最终输出如下&#xff1a; using UnrealBuildTool;…

深入探讨梯度下降:优化机器学习的关键步骤(二)

文章目录 &#x1f340;引言&#x1f340;eta参数的调节&#x1f340;sklearn中的梯度下降 &#x1f340;引言 承接上篇&#xff0c;这篇主要有两个重点&#xff0c;一个是eta参数的调解&#xff1b;一个是在sklearn中实现梯度下降 在梯度下降算法中&#xff0c;学习率&#xf…

Maven 基础之安装和命令行使用

Maven 的安装和命令行使用 1. 下载安装 下载解压 maven 压缩包&#xff08;http://maven.apache.org/&#xff09; 配置环境变量 前提&#xff1a;需要安装 java 。 在命令行执行如下命令&#xff1a; mvn --version如出现类似如下结果&#xff0c;则证明 maven 安装正确…

无涯教程-Android - ImageButton函数

ImageButton是一个AbsoluteLayout,可让您指定其子级的确切位置。这显示了带有图像(而不是文本)的按钮,用户可以按下或单击该按钮。 Android button style set ImageButton属性 以下是与ImageButton控件相关的重要属性。您可以查看Android官方文档以获取属性的完整列表以及可以…

webrtc 的Bundle group 和RTCP-MUX

1&#xff0c;最近调试程序的时候发现抱一个错误 max-bundle configured but session description has no BUNDLE group 最后发现是一个参数设置错误 config.bundle_policy webrtc::PeerConnectionInterface::BundlePolicy::kBundlePolicyMaxBundle; 2&#xff0c;rtcp-mu…

迈向无限可能, ATEN宏正领跑设备切换行业革命!

随着互联网在各个领域的广泛应用,线上办公这一不受时间和地点制约、不受发展空间限制的办公模式开始广受追捧,预示着经济的发展正朝着新潮与活跃的方向不断跃进。当然,在互联网时代的背景下,多线程、多设备的线上办公模式也催生了许多问题:多设备间无法进行高速传输、切换;为保…

SpringCloud(十)——ElasticSearch简单了解(一)初识ElasticSearch和RestClient

文章目录 1. 初始ElasticSearch1.1 ElasticSearch介绍1.2 安装并运行ElasticSearch1.3 运行kibana1.4 安装IK分词器 2. 操作索引库和文档2.1 mapping属性2.2 创建索引库2.3 对索引库的查、删、改2.4 操作文档 3. RestClient3.1 初始化RestClient3.2 操作索引库3.3 操作文档 1. …

A Mathematical Framework for Transformer Circuits—Part (1)

A Mathematical Framework for Transformer Circuits 前言Summary of ResultsREVERSE ENGINEERING RESULTSCONCEPTUAL TAKE-AWAYS Transformer OverviewModel SimplificationsHigh-Level ArchitectureVirtual Weights and the Residual Stream as a Communication ChannelVIRTU…