Gin 框架之Cookie与Session

文章目录

    • 一、Cookie和Session的由来
    • 二、Cookie简介
      • 1. 什么是Cookie
      • 2. Cookie规范
      • 3. 安全性
      • 4. Cookie 关键配置
    • 三、Session简介
      • 1. 什么是Session
      • 2. Session 安全性
      • 3. 如何让客户端携带 sess_id
    • 四、使用 Gin 的 Session 插件
      • 4.1 介绍
      • 4.2 基本使用
    • 五、 session与store
      • 5.1 会话(Session)
      • 5.2 存储(Store)
    • 六、小黄书实践

一、Cookie和Session的由来

我们知道HTTP协议无连接的, 也就是不保存用户的状态信息。

早期(十几年前)的网页是静态的, 数据都是写死的, 人们访问网页只是用来查看新闻的, 没有保存用户状态的需求。

而往后出现了像论坛、博客、网购这一类需要保存用户信息的网站, 如果网站不保存用户的状态信息, 意味着用户每次访问都需要重新输入用户名和密码, 这无疑对用户的体验是极其不好的。

于是, 就出现了会话跟踪技术, 我们可以把它理解为客户端与服务端之间的一次会晤, 一次会晤包含的多次请求与响应, 每次请求都带着请求参数, 比如请求登入的请求参数是用户名和密码, 服务端就会拿着请求参数与数据库去比对, 找到相应的用户信息。

如何实现会话跟踪 : 在HTTP协议中可以使用Cookie来完成, 在Web开发中可以使用Session来完成

  • Cookie是存在浏览器中的键值对, 每次发送请求都携带者参数, 但是容易被截获, 不安全
  • 于是就出现了Session, 它是存在于服务端的键值对, key为随机字符串, 安全性提高了, 但所有的数据都存在服务器中, 服务器的压力很大
  • 之后便产生了Token的概念, 服务端签发加密后的字符串给客户端浏览器保存, 客户端每次请求携带用户名和密码, 并加上由服务端签发的用户名和密码加密的字符串, 服务端收到请求后再对用户名密码加密, 与后面携带的密文对比, 由于它也是保存在客户端浏览器上的, 所以也叫Cookie

二、Cookie简介

1. 什么是Cookie

  • Cookie是服务器保存在客户端浏览器之上的key-value键值对 : username='jarvis';password="123"
  • 它是随着服务器的响应发送给客户端, 客户端将其保存, 下一次请求时会将Cookie放在其中, 服务器通过识别Cookie就能知道是哪个客户端浏览器

2. Cookie规范

  • Cookie大小上限为4KB
  • 一个服务器最多在客户端浏览器上保存20个Cookie
  • 一个浏览器最多保存300个Cookie

上面是HTTP中Cookie的规范, 现在浏览器的竞争, 有些Cookie大小能打到8KB, 最多可以保存500个Cookie

不同浏览器之间的Cookie是不共享的

3. 安全性

  • Cookie保存在浏览器本地, 意味着很容易被窃取和篡改

4. Cookie 关键配置

在使用 Cookie 的时候,要注意“安全使用”。

  • Domain: 也就是 Cookie 可以用在什么域名下,按照最小化原则来设定。
  • Path: Cookie 可以用在什么路径下,同样按照最小化原则来设定。
  • Max-AgeExpires: 过期时间,只保留必要时间。
  • Http-Only: 设置为 true 的话,那么浏览器上的 JS 代码将无法使用这个 Cookie。永远设置为 true。
  • Secure: 只能用于 HTTPS 协议,生产环境永远设置为 true。
  • SameSite: 是否允许跨站发送 Cookie,尽量避免。

在Go语言中,标准库net/http提供了用于处理HTTP请求和响应的功能,包括处理Cookie的相关功能。可以通过http.Cookie{}查看。

对应解释如下:

type Cookie struct {Name  string      // Cookie的名称,用于唯一标识一个CookieValue string      // Cookie的值,存储在Cookie中的具体数据Path       string    // Cookie的路径,指定了哪些路径下的页面可以访问该CookieDomain     string    // Cookie的域,指定了哪些域名下可以访问该CookieExpires    time.Time // Cookie的过期时间,表示Cookie在何时之前有效RawExpires string    // 仅用于读取Cookie,保存未解析的过期时间信息// MaxAge=0 表示没有指定'Max-Age'属性// MaxAge<0 表示立即删除Cookie,相当于 'Max-Age: 0'// MaxAge>0 表示'Max-Age'属性存在,并以秒为单位给出MaxAge   intSecure   bool   // Secure属性,指定是否只在使用HTTPS协议时才发送CookieHttpOnly bool   // HttpOnly属性,设置为true时,禁止通过JavaScript访问该CookieSameSite SameSite // SameSite属性,控制是否允许跨站发送CookieRaw      string   // 保存未解析的原始Cookie字符串Unparsed []string // 保存未解析的属性-值对文本
}

三、Session简介

1. 什么是Session

  • 存放在服务器上的键值对 : {'weqweq':{'username':'jarvis','password':'123'}}
  • Cookie可以保存状态, 但本身最大只能支持4069字节, 并且不安全, 于是就出现了Session
  • 它能支持更多字节, 并且保存在服务端上, 具有较高的安全性,Session基于Cookie, 本地存放服务器返回给浏览器的随机字符串
  • 客户端浏览器请求中携带随机字符串(session_id), 服务端收到后与数据库中存储的session做对比

ps : session存储的方式多种多样, 可以是数据库、缓存、硬盘等等

2. Session 安全性

Session 关键在于服务器要给浏览器一个 sess_id,也就是 Session 的 ID。

后续每一次请求都带上这个 Session ID,服务端就知道你是谁了。

但是后端服务器是“只认 ID 不认人”的。也就是说,如果攻击者拿到了你的 ID,那么服务器就会把攻击者当成你。在下图中,攻击者窃取到了 sess_id,就可以冒充是你了。

3. 如何让客户端携带 sess_id

因为 sess_id 是标识你身份的东西,所以你需要在每一次访问系统的时候都带上。

  • 最佳方式就是用 Cookie,也就是 sess_id 放到 Cookie 里面。sess_id 自身没有任何敏感信息,所以放 Cookie 也可以。
  • 也可以考虑放 Header,比如说在 Header 里面带一个 sess_id。这就需要前端的研发记得在 Header 里面带上。
  • 还可以考虑放查询参数,也就是 ?sess_id=xxx

理论上来说还可以放 body,但是基本没人这么干。在一些禁用了 Cookie 功能的浏览器上,只能考虑后两者。

在浏览器上,你可以通过插件 cookieEditor 来查看某个网站的 Cookie 信息。

四、使用 Gin 的 Session 插件

4.1 介绍

Gin框架本身并不内置对Session的支持,但你可以使用第三方的Session中间件来实现。其中比较常用的是 github.com/gin-contrib/sessions

4.2 基本使用

首先,确保你已经安装了Session中间件:

go get github.com/gin-contrib/sessions

然后在你的Gin应用程序中使用它。以下是一个基本的使用示例:

package mainimport ("github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie""github.com/gin-gonic/gin""net/http""time"
)func main() {// 创建Gin引擎r := gin.Default()// 使用Cookie存储Sessionstore := cookie.NewStore([]byte("secret"))r.Use(sessions.Sessions("mysession", store))// 路由:设置Sessionr.GET("/set", func(c *gin.Context) {// 获取默认Sessionsession := sessions.Default(c)// 设置Session中的键值对session.Set("user", "example@example.com")session.Set("expires", time.Now().Add(24*time.Hour).Unix())// 保存Sessionsession.Save()// 返回JSON响应c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})})// 路由:获取Sessionr.GET("/get", func(c *gin.Context) {// 获取默认Sessionsession := sessions.Default(c)// 从Session中获取键值对user := session.Get("user")expires := session.Get("expires")// 返回JSON响应c.JSON(http.StatusOK, gin.H{"user": user, "expires": expires})})// 启动Gin应用,监听端口8080r.Run(":8080")
}

在上述示例中,我们使用github.com/gin-contrib/sessions/cookie包创建了一个基于Cookie的Session存储。路由"/set"用于设置Session,路由"/get"用于获取Session。请注意,这里的Session数据是存储在客户端的Cookie中的,因此在实际应用中需要注意安全性。

五、 session与store

在Web应用中,会话(session)是一种用于在不同请求之间存储和共享用户信息的机制。通常,会话用于跟踪用户的身份验证状态、存储用户首选项和其他与用户相关的数据。在Gin框架中,会话的管理通常通过sessionstore两个概念来完成。

5.1 会话(Session)

  • 概念: 会话是在服务器端存储用户状态的一种机制。每个用户访问网站时,服务器都会为其创建一个唯一的会话标识符,该标识符存储在用户的浏览器中,通常通过Cookie来实现。服务器可以根据这个标识符来识别用户,并在多个请求之间共享用户的状态信息。
  • 作用: 主要用于存储用户的身份验证状态、用户的首选项、购物车内容等用户相关的信息,以便在用户访问不同页面或进行不同请求时能够保持一致的用户状态。

5.2 存储(Store)

  • 概念: 存储是用于实际存储和检索会话数据的地方。存储可以是内存、数据库、文件系统等,具体取决于应用程序的需求。存储负责维护会话数据的持久性和安全性。
  • 作用: 存储的主要作用是提供对会话数据的 CRUD(Create、Read、Update、Delete)操作。当用户发起请求时,存储会根据会话标识符检索相应的会话数据,服务器可以通过存储来实现会话的管理。

在Gin框架中,常用的Session中间件是 github.com/gin-contrib/sessions,而存储则可以选择不同的后端,例如使用Cookie、Redis、内存等。

在Gin应用中引入该中间件,通过创建Session对象和设定存储引擎,可以方便地进行Session的处理。以下是一个基本的使用示例:

package mainimport ("github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie""github.com/gin-gonic/gin""net/http"
)func main() {r := gin.Default()// 使用Cookie存储Sessionstore := cookie.NewStore([]byte("secret"))r.Use(sessions.Sessions("mysession", store))r.GET("/set", func(c *gin.Context) {// 设置Sessionsession := sessions.Default(c)session.Set("user", "example@example.com")session.Save()c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})})r.GET("/get", func(c *gin.Context) {// 获取Sessionsession := sessions.Default(c)user := session.Get("user")c.JSON(http.StatusOK, gin.H{"user": user})})r.Run(":8080")
}
  • cookie.NewStore([]byte("secret")) 在上述示例中,使用了Cookie存储。cookie.NewStore 创建了一个基于Cookie的存储引擎,使用了一个密钥来加密Cookie中的会话数据。
  • 其他存储引擎: 除了Cookie存储,github.com/gin-contrib/sessions 还支持其他存储引擎,例如Redis、文件系统等。可以根据实际需求选择合适的存储引擎。
// 使用Redis存储Session的例子
import ("github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/redis""github.com/gin-gonic/gin"
)func main() {r := gin.Default()// 使用Redis存储Sessionstore, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))r.Use(sessions.Sessions("mysession", store))// ...
}

在实际项目中,选择存储引擎时要考虑性能、可扩展性和持久性等因素。 Cookie 存储适用于小型应用,而对于大型应用,可能需要选择支持分布式的存储引擎,如 Redis。

总体而言,使用Gin的Session中间件能够方便地管理会话,而不同的存储引擎则提供了灵活的选择,以适应不同的应用场景。

六、小黄书实践

目录结构

├── docker-compose.yaml         # Docker Compose配置文件,定义了整个应用的容器化部署
├── go.mod                      # Go模块文件,用于管理项目的依赖项
├── go.sum                      # Go模块文件,包含依赖项的版本信息
├── internal                    # 内部模块,包含应用程序的业务逻辑
│   ├── domain                  # 领域层,包含领域对象和领域逻辑
│   │   └── user.go            # 用户领域对象的定义
│   ├── repository              # 数据仓库层,用于与数据库交互
│   │   ├── dao                 # 数据访问对象,与数据库的交互接口
│   │   │   ├── init.go         # 数据库初始化
│   │   │   └── user.go         # 用户数据访问对象的定义
│   │   └── user.go             # 用户数据仓库接口的定义
│   ├── service                 # 服务层,包含业务逻辑的实现
│   │   └── user.go            # 用户服务的实现
│   └── web                     # Web层,处理HTTP请求和路由
│       ├── middleware          # 中间件,用于处理HTTP请求的中间逻辑
│       │   └── login.go       # 登录中间件的定义
│       ├── user.go             # 用户相关的HTTP处理函数和路由定义
│       └── user_test.go        # 用户相关的测试文件
├── main.go                     # 主程序入口,应用程序的启动文件
├── pkg                         # 外部可导出的包,供其他应用程序或模块使用
├── script                      # 脚本目录,包含各种初始化和辅助脚本└── mysql                   # MySQL初始化脚本目录└── init.sql            # 数据库初始化SQL脚本

internal/domain/user.go 文件代码:

package domainimport "time"// User 领域对象,是DDD 中的 entity
type User struct {Id       int64Email    stringPassword stringCtime    time.Time
}

internal/repository/user.go

package repositoryimport ("context""webook/internal/domain""webook/internal/repository/dao"
)var (ErrUserDuplicateEmail = dao.ErrUserDuplicateEmailErrUserNotFound       = dao.ErrUserNotFound
)type UserRepository struct {dao *dao.UserDAO
}func NewUserRepository(dao *dao.UserDAO) *UserRepository {return &UserRepository{dao: dao,}
}func (r *UserRepository) Create(ctx context.Context, u domain.User) error {return r.dao.Insert(ctx, dao.User{Email:    u.Email,Password: u.Password,})
}
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {u, err := r.dao.FindByEmail(ctx, email)if err != nil {return domain.User{}, err}return domain.User{Id:       u.Id,Email:    u.Email,Password: u.Password,}, nil
}

internal/repository/dao/init.go

package daoimport "gorm.io/gorm"func InitTable(db *gorm.DB) error {return db.AutoMigrate(&User{})
}

internal/repository/dao/user.go

package daoimport ("context""errors""github.com/go-sql-driver/mysql""gorm.io/gorm""time"
)var (ErrUserDuplicateEmail = errors.New("邮箱冲突")ErrUserNotFound       = gorm.ErrRecordNotFound
)type UserDAO struct {db *gorm.DB
}func NewUserDAO(db *gorm.DB) *UserDAO {return &UserDAO{db: db,}
}
func (dao *UserDAO) Insert(ctx context.Context, u User) error {// 存毫秒数now := time.Now().UnixNano()u.Ctime = nowu.Utime = nowerr := dao.db.WithContext(ctx).Create(&u).Error// 类型断言,判断是否是mysql的唯一冲突错误if mysqlErr, ok := err.(*mysql.MySQLError); ok {const uniqueConflictsErrNo uint16 = 1062if mysqlErr.Number == uniqueConflictsErrNo {// 邮箱已存在return ErrUserDuplicateEmail}}return err
}
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (User, error) {var u Usererr := dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Errorreturn u, err
}// User 直接对应数据库中的表
// 有些人叫做entity,有些人叫做model
type User struct {Id int64 `gorm:"primaryKey,autoIncrement"`// 全部用户唯一Email    string `gorm:"unique"`Password string// 创建时间,毫秒数,解决时区问题Ctime int64// 更新时间Utime int64
}

internal/service/user.go

package serviceimport ("context""errors""golang.org/x/crypto/bcrypt""webook/internal/domain""webook/internal/repository""webook/internal/repository/dao"
)var (ErrUserDuplicateEmail    = dao.ErrUserDuplicateEmailErrInvalidUserOrPassword = errors.New("账号/邮箱或密码不对")
)type UserService struct {repo *repository.UserRepository
}func NewUserService(repo *repository.UserRepository) *UserService {return &UserService{repo: repo,}
}func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {// 先加密密码hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)if err != nil {return err}u.Password = string(hash)// 然后就是,存起来return svc.repo.Create(ctx, u)
}func (svc *UserService) Login(ctx context.Context, email, password string) (domain.User, error) {// 先找用户u, err := svc.repo.FindByEmail(ctx, email)if err == repository.ErrUserNotFound {return domain.User{}, ErrInvalidUserOrPassword}if err != nil {return domain.User{}, err}// 比较密码了err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))if err != nil {// DEBUGreturn domain.User{}, ErrInvalidUserOrPassword}return u, nil
}

internal/web/user.go

package webimport (regexp "github.com/dlclark/regexp2""github.com/gin-contrib/sessions""github.com/gin-gonic/gin""net/http""webook/internal/domain""webook/internal/service"
)type UserHandler struct {svc         *service.UserServiceemailExp    *regexp.RegexppasswordExp *regexp.Regexp
}func NewUserHandler(svc *service.UserService) *UserHandler {const (emailRegexPattern    = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`)emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)return &UserHandler{svc:         svc,emailExp:    emailExp,passwordExp: passwordExp,}
}func (u *UserHandler) RegisterRoutes(server *gin.Engine) {ug := server.Group("/user")   //ug is user groupug.GET("/profile", u.Profile) // 查询用户信息接口ug.POST("/signup", u.SignUp)  // 注册接口ug.POST("/login", u.Login)    // 登录接口ug.POST("/logout", u.Logout)  // 登出接口ug.POST("/edit", u.Edit)      // 修改用户信息接口}func (u *UserHandler) RegisterRoutesV1(ug *gin.RouterGroup) {ug.GET("/profile", u.Profile) // 查询用户信息接口ug.POST("/signup", u.SignUp)  // 注册接口ug.POST("/login", u.Login)    // 登录接口ug.POST("/logout", u.Logout)  // 登出接口ug.POST("/edit", u.Edit)      // 修改用户信息接口}
func (u *UserHandler) Profile(ctx *gin.Context) {
}
func (u *UserHandler) SignUp(ctx *gin.Context) {type SignUpRequest struct {Email           string `json:"email"`Password        string `json:"password"`ConfirmPassword string `json:"confirmPassword"`}var request SignUpRequest// 如果 Bind 方法发现输入有问题,它就会直接返回一 个错误响应到前端。if err := ctx.Bind(&request); err != nil {return}ok, err := u.emailExp.MatchString(request.Email)if err != nil {ctx.String(http.StatusOK, "系统错误")return}if !ok {ctx.String(http.StatusOK, "邮箱格式错误")return}ok, err = u.passwordExp.MatchString(request.Password)if err != nil {ctx.String(http.StatusOK, "系统错误")return}if !ok {ctx.String(http.StatusOK, "密码必须包含至少一个数字、一个字母、一个特殊字符,并且长度至少为8位")return}if request.Password != request.ConfirmPassword {ctx.String(http.StatusOK, "两次密码不一致")return}// 调用以一下svc 的方法err = u.svc.SignUp(ctx, domain.User{Email:    request.Email,Password: request.Password,})if err == service.ErrUserDuplicateEmail {ctx.String(http.StatusOK, "邮箱冲突")return}if err != nil {ctx.String(http.StatusOK, "系统错误")return}ctx.String(http.StatusOK, "注册成功")
}
func (u *UserHandler) Login(ctx *gin.Context) {type LoginRequest struct {Email    string `json:"email"`Password string `json:"password"`}var request LoginRequestif err := ctx.Bind(&request); err != nil {return}// 调用以一下svc 的方法user, err := u.svc.Login(ctx, request.Email, request.Password)if err == service.ErrInvalidUserOrPassword {ctx.String(http.StatusOK, "账号或密码错误")return}if err != nil {ctx.String(http.StatusOK, "系统错误")return}// 登录成功设置sessionsess := sessions.Default(ctx)// 设置session的key和valuesess.Set("userId", user.Id)sess.Save()ctx.String(http.StatusOK, "登录成功")return
}func (u *UserHandler) Logout(ctx *gin.Context) {}
func (u *UserHandler) Edit(ctx *gin.Context) {}

main.go

package mainimport ("github.com/gin-contrib/cors""github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie""github.com/gin-gonic/gin""gorm.io/driver/mysql""gorm.io/gorm""strings""time""webook/internal/repository""webook/internal/repository/dao""webook/internal/service""webook/internal/web""webook/internal/web/middleware"
)func main() {db := initDB()ser := initWebServer()u := initUser(db)u.RegisterRoutes(ser)ser.Run(":8080")}func initWebServer() *gin.Engine {ser := gin.Default()ser.Use(func(ctx *gin.Context) {println("这是一个中间件")})ser.Use(func(ctx *gin.Context) {println("这是二个中间件")})ser.Use(cors.New(cors.Config{//AllowOrigins: []string{"*"},//AllowMethods: []string{"POST", "GET"},AllowHeaders: []string{"Content-Type", "Authorization"},//ExposeHeaders: []string{"x-jwt-token"},// 是否允许你带 cookie 之类的东西AllowCredentials: true,AllowOriginFunc: func(origin string) bool {if strings.HasPrefix(origin, "http://localhost") {// 你的开发环境return true}return strings.Contains(origin, "yourcompany.com")},MaxAge: 12 * time.Hour,}))// 用 cookie 存储 sessionstore := cookie.NewStore([]byte("secret"))// 使用 session 中间件ser.Use(sessions.Sessions("mysession", store))//ser.Use(middleware.NewLoginMiddlewareBuilder().Build())//ser.Use(middleware.NewLoginMiddlewareBuilder().//	IgnorePaths("/users/signup").//	IgnorePaths("/users/login").Build())// v1middleware.IgnorePaths = []string{"sss"}ser.Use(middleware.CheckLogin())// 不能忽略sss这条路径server1 := gin.Default()server1.Use(middleware.CheckLogin())return ser
}func initUser(db *gorm.DB) *web.UserHandler {ud := dao.NewUserDAO(db)repo := repository.NewUserRepository(ud)svc := service.NewUserService(repo)u := web.NewUserHandler(svc)return u
}
func initDB() *gorm.DB {db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook"))if err != nil {// panic 相当于 整个 goroutine 结束panic("failed to connect database")}err = dao.InitTable(db)if err != nil {panic("failed to init table")}return db
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面对根据角色和单子状态如何有效的进行按钮权限的控制

当阁下看到这个按钮权限控制时&#xff0c;该如何应对 按钮权限是根据工单流程状态进行的&#xff0c;当工单走到某个流程时就显示该表格中对应的状态&#xff0c;初看也就简简单单&#xff0c;仔细一看&#xff0c;逻辑还是很复杂&#xff0c; 首先这里得说一下背景&#xff…

2024--Django平台开发-订单项目管理(十四)

day14 订单管理系统 1.关于登录 1.1 UI美化 页面美化&#xff0c;用BootStrap 自定义BooStrapForm类实现。 class BootStrapForm:exclude_filed_list []def __init__(self, *args, **kwargs):super().__init__(*args, **kwargs)# {title:对象,"percent":对象}fo…

ACL【新华三与华为的区别】

【解释】acl简单点解释就是&#xff0c;一套根据需求而设置的规则 【背景】 192.168.1.0/24 网段不允许访问 192.168.2.0/24 网段&#xff0c;要求使用基本 ACL 实现20_1 可以访问 20_6 的 TELNET 服务&#xff0c;但不能访问 FTP 服务 【操作步骤】 {易混点 }&#xff1a;1. …

阿里二面:要保证消息不丢失,又不重复,消息队列怎么选型?

大家好&#xff0c;我是君哥。 在使用消息队列时&#xff0c;有两个经常让我们烦恼的问题&#xff0c;消息丢失和消息重复。那我们在做技术选型时&#xff0c;有没有一个消息队列能解决消息丢失和消息重复这两个问题呢&#xff1f; 消息丢失 如上图&#xff0c;从生产者发送消…

小程序学习-17

attached最常用 推荐使用 lilfetimes 这种方法

【Linux】Linux进程间通信(四)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

基于嵌入式的智能智能通风系统

基于嵌入式的智能智能通风系统 功能说明 通过微信小程序控制窗户的开关状体以及倒计时开关和定时开关&#xff0c;小程序上实时显示当前温度湿度和光照强度。 功能展示 02智能通风系统 Mqtt服务器 http://www.yoyolife.fun/iot&#xff1a;Mqtt服务器&#xff0c;我是在这里注…

前端react入门day03-react获取dom与组件通信

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 受控表单绑定 React中获取DOM 组件通信 父传子 父传子-基础实现 父传子-props说明 父传子 - 特殊的…

J4 - ResNet与DenseNet结合

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 目录 环境模型设计模型效果展示总结与心得体会 环境 系统: Linux语言: Python3.8.10深度学习框架: Pytorch2.0.0cu118显卡&#xff1a;GT…

构建坚固网络,SD-WAN网络配置指南

在新兴的网络技术中&#xff0c;软件定义广域网&#xff08;SD-WAN&#xff09;作为关键角色脱颖而出&#xff0c;被广泛应用于构建高效可靠的企业网络。利用SD-WAN技术&#xff0c;企业能够更好地管理和优化其网络基础设施&#xff0c;实现更高效的连接和数据传输。本文将深入…

STM32标准库开发——串口发送/单字节接收

USART基本结构 串口发送信息 启动串口一的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);初始化对应串口一的时钟&#xff0c;引脚&#xff0c;将TX引脚设置为复用推挽输出。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_In…

BigeMap在Unity3d中的应用,助力数字孪生

1. 首先需要用到3个软件&#xff0c;unity&#xff0c;gis office 和 bigemap离线服务器 Unity下载地址:点击前往下载页面(Unity需要 Unity 2021.3.2f1之后的版本) Gis office下载地址:点击前往下载页面 Bigemap离线服务器 下载地址: 点击前往下载页面 Unity用于数字孪生项…