【Gee】项目总结:模仿 GIN 实现简单的 Golang Web 框架

news2025/3/15 6:26:13

文章目录

  • Gee 项目回顾
  • Gee 项目总结
    • Golang 已经具备基础的 web 功能,为什么还需要 web 框架?
    • 作为 web 框架,Gee 框架完成了哪些功能?
    • 如何用 Gee 来构建 web 项目?

Gee 项目回顾

上个月月末我按照 Geektutu 的教程,实现了 Gee 这个基于 Golang 的简单 Web 框架,但是一直没有进行复盘总结。学习 Gee 的八篇文章的链接如下:

  • 【Gee】7天用 Go 从零实现 Web 框架 Gee
  • 【Gee】Day1:HTTP 基础
  • 【Gee】Day2:上下文
  • 【Gee】Day3:前缀树路由
  • 【Gee】Day4:分组控制
  • 【Gee】Day5:中间件
  • 【Gee】Day6:模板 Template
  • 【Gee】Day7:错误恢复

现在让我们按照与回顾 Zinx 框架类似的套路,对 Gee 框架进行总结。

Gee 项目总结

现在我们来对 Gee 项目进行总结。

和 Zinx 类似,我们从使用了 Gee 的一个 main 函数出发,来理解:

  • 在 Golang 的 web 开发项目中,为什么需要 web 框架?
  • 作为 web 框架,Gee 框架完成了哪些功能?
  • 如何使用 Gee 框架构建 web 项目?

一个使用 Gee 框架的简单 web 应用如下,这个应用只包含 HTTP 的 GET 方法,即:将静态的数据展示在前端的页面当中。

package main

import (
	"gee/gee"
	"net/http"
)

func main() {
	r := gee.New()
	r.Use(gee.Logger(), gee.Recovery())
	r.GET("/", func(c *gee.Context) {
		c.String(http.StatusOK, "Hello Geektutu\n")
	})
	// index out of range for testing Recovery()
	r.GET("/panic", func(c *gee.Context) {
		names := []string{"geektutu"}
		c.String(http.StatusOK, names[100])
	})

	v1 := r.Group("/v1")
	{
		v1.GET("/hello", func(c *gee.Context) {
			c.String(http.StatusOK, "A simple hello string to test Group")
		})
		v1.GET("/hello/:name", func(c *gee.Context) {
			// expect /localhost:9999/v1/hello/yggp
			c.String(http.StatusOK, "hello from %s, you're at %s now. \n", c.Param("name"), c.Path)
		})
		v1.GET("/hello2", func(c *gee.Context) {
			// expect /localhost:9999/v1/hello2?name=yggp
			c.String(http.StatusOK, "Another hello from %s, you're at %s now. \n", c.Query("name"), c.Path)
		})
		v1.GET("/json", func(c *gee.Context) {
			c.JSON(http.StatusOK, gee.H{
				"username": c.PostForm("username"),
				"password": c.PostForm("password"),
			})
		})
	}

	v2 := r.Group("/v2")
	{
		v2.GET("/assets/*filepath", func(c *gee.Context) {
			c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})
		})
	}

	r.Run(":9999")
}

从这个 main 函数入手,我们可以看到,首先使用 r := gee.New() 这个语句应该是生成了一个 Gee 框架的 web 引擎,之后可以通过 Use 的方式将中间件集成到框架当中。之后,与 GIN 框架类似,通过使用 GET 方法,可以为 Gee 注册一个相应的 handler,用于在访问指定 URL 的时候,触发相应的事件。

当然,和 GIN 类似,Gee 也支持分组,上面的应用创建了两个 Gee 的分组,分别是 v1 和 v2。Gee 还支持模糊查询,我们用到了:*,并且 Gee 还支持解析带有参数的 URL,通过 Query 方法来完成。

此外,还要提到的一点是 Gee 支持以多种格式返回 HTTP 响应,上例当中包含了 String 和 JSON 等格式。Gee 还支持 HTML 格式。下面我们深入 Gee 的源码,来理解 Gee 的设计思想,并在这个过程中回答我们最初提出的三个问题。

Golang 已经具备基础的 web 功能,为什么还需要 web 框架?

在出发之前,我们需要看清楚,为什么我们需要踏上这一趟旅程。

一个最直接的问题就是,Golang 已经具备了基础的 web 功能,为什么还需要设计 web 框架?

要回答这个问题,我们需要先回顾,原生的 Golang web 库是如何实现一个 web 功能开发的。

我们将 web 功能开发做最大程度的简化,即:当我们得到一个 request 的时候,不进行业务处理,直接回显一些数据字段作为 response。一个简单的实现如下,这里我直接复制 Geektutu 在序言当中给出的经典案例:

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

上面这个语句段实现的功能是:在 main.go 中注册了 //count 两个 URL,并通过 HandlerFunc 类型的 handler 和 counter 赋予两个 URL 以行为,也就是说,当服务开启后,我们在浏览器访问指定的 URL,即可看到 HandlerFunc 对应的行为执行的结果。最后通过 http.ListenAndServe 开启 web 服务的 IP 和 port。

它和我们刚才使用的 Gee 相比,有诸多的弊端。首先,它不支持中间件,无法在开启 web 服务的前后以及 web 服务的过程中做一些必要的工作,比如日志记录、错误恢复等。其次,原生的 web 功能显然不支持 URL 的模糊查询,也就是说我们必须给出精准的 URL 才能正常执行功能。再次,原生的 web 功能不支持分组,不便于管理,需要人工记录具体的 URL 前缀。最后,原生的 web 没有对 HandlerFunc 进行封装,也就是说我们实现每一个业务功能时,都需要重写一个以 http.ResponseWriter*http.Request 为形参的 HandlerFunc。

显然上述实现对 web 应用的开发者来说非常不友好,对 web 功能进行更好的封装,形成一个 web 框架,有助于提高效率。在深入 Gee 之前,我们先来对比两个 main 函数当中业务注册的部分,看看二者有什么不同。

在使用原生 web 库的时候,我们是通过 http.HandlerFunc 进行 URL 和业务的绑定。而 Gee 框架中是通过:

r.GET("/", func(c *gee.Context){
	// ... ... ...
})

来完成业务的绑定。后者将 ResponseWriter*http.Request 封装到了 Context 当中,在二者的基础上,Context 还进行了进一步的拓展,其承载的功能远多于原生的 web 库,比如 Context 可以保存 URL 路径、HTTP Method、StatusCode、URL 当中的参数等。

综上所述,通过使用和不使用框架的对比,我们已经基本理解了 web 框架的必要性。

作为 web 框架,Gee 框架完成了哪些功能?

我们刚才已经提到了 Gee 实现的几乎所有功能,因此针对这个问题,我们将深入 Gee 框架的源码,对 Gee 进行剖析。

仍然从 main.go 出发,首先我们使用 r := gee.New() 创建了一个 Gee 的句柄。gee.New() 的返回值是一个 *Engine。那么我们为什么需要 Engine 呢?

要回答这个问题,我们仍然需要回到 Golang 原生的 web 库当中去。当我们调用 http.ListenAndServe 时,需要指定两个参数,第一个是服务开启的地址,而第二个参数是一个 Handler,我们在最初使用原生 web 库的时候将其设置为 nil,实际上可以通过实现 Handler 来完成业务的注册:

package main

import (
	"fmt"
	"log"
	"net/http"
)

// Engine is the uni handler for all requests
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.URL.Path {
	case "/":
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	case "/hello":
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":9999", engine))
}

因此这个 Engine 就是我们实现 web 框架的入口。

所以我们自然要看一下 Engine 的实现:

// Engine implements the interface of ServeHTTP
type Engine struct {
	*RouterGroup  // embed a RouterGroup pointer
	router        *router
	groups        []*RouterGroup     // store all groups
	htmlTemplates *template.Template // for html render
	funcMap       template.FuncMap   // for html render
}

Engine 结构本身并不复杂,它包含五个成员,分别是*RouterGrouproutergrouphtmlTemplatesfuncMap,后两个是用于渲染 HTML 模板的,鉴于前后端分离的开发模式是目前 web 开发的主流,我们一般不会在后端直接对 HTML 进行渲染,而是应该将专业的事情交给前端,后端只需要处理好业务逻辑并按照 RESTful 接口返回 json 给前端即可,因此在总结 Gee 的过程中我不会深入探讨后两个成员及与 HTML 渲染相关的方法,而重点关注如何实现 web 框架。

Engine 嵌入了一个 *RouterGroup,而之后我们又将看到 RouterGroup 中具有一个 *Engine 成员,实际上这就是 Engine 和 RouterGroup 相互引用的体现。Engine 本身就是一个 RouterGroup,它的URL是 /,即“根”,因此在其之上拓展出来的 Group 都是它的子集。鉴于我们有可能要在 root 上使用一些中间件,因此嵌入 RouterGroup 可以使 Engine 获得 RouterGroup 的一些行为,而不需要重复实现。

router 成员是路由指针 *router 类型,每一个 Engine 只具有一个 router,router 的作用也非常简单,就是对添加的路由进行管理。比如我在根 URL 目录下添加了 /hello/users/:name,那么 router 就应该帮助我将 HandlerFunc 与目录绑定,并记录 :name 这类模糊前缀。

groups 是一个 *RouterGroups 类型的 slice,这里要理清 groups 和内嵌的 *RouterGroups 之间的区别。内嵌一个 *RouterGroups 使得 Engine 获得了 RouterGroup 的行为,而 groups 保存的是 Engine 根目录下辖的其它 Group,比如 v1 等。

Engine 的工厂函数如下:

func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

它首先创建了一个 *Engine,并创建了一个 *RouterGroup 赋给 engine,同时需要将 engine 赋值给 RouterGroup,原因在于二者是相互引用的,互相绑定的是指针。groups 同样需要初始化,它最初包含的成员就是 engine 这个根 RouterGroup。完成初始化后,返回新创建的 *Engine,就可以开始使用了。

由于 Engine 需要实现 http.Handler 这个接口,因此我们需要实现接口的方法 ServeHTTP,其实现是:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range engine.groups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...)
		}
	}
	c := newContext(w, req)
	c.handlers = middlewares
	c.engine = engine
	engine.router.handle(c)
}

ServeHTTP 方法要做的事情其实就是处理一次 HTTP Request。在 ServeHTTP 方法当中,首先创建了一个 HandlerFunc 类型的 slice,之后对 engine 包含的 groups 进行遍历,如果当前请求的 URL 包含 group 的前缀,那么就需要使用这个 group 所注册的中间件对这个请求进行处理,将中间件对应的 HandlerFunc 追加到 middlewares slice 当中。处理好中间件 HandlerFunc 之后,我们要做的是新建一个有关本次 HTTP Request 的 Context,将与本次请求相关的信息保存到 Context 当中。

Context 指的就是上下文,它的结构定义如下:

type Context struct {
	// origin objects
	// 封装了 http.ResponseWriter 和 *http.Request
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode int
	// middleware
	handlers []HandlerFunc
	index    int
	// engine pointer
	engine *Engine
}

Context 对 http.ResponseWriter*http.Request 进行了封装,其中还保存了一些额外的请求信息,比如本次请求的 URL 路径、方法(GET/POST)以及模糊查询的参数。此外,Context 中还保存了相应信息,比如 HTTP 状态码。保存了中间件对应的 HandlerFunc slice 以及执行与 HandlerFunc slice 对应的 index,index 用于控制当前执行到哪个 HandlerFunc。最后,Context 还引用了 *Engine,每一个 Context 绑定的都是同一个 Engine,便于 Context 快速对 Engine 进行访问。

ServeHTTP 当中新建一个 Context 调用的工厂函数如下:

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Writer: w,
		Req:    req,
		Path:   req.URL.Path,
		Method: req.Method,
		index:  -1,
	}
}

可以看到此处的 index 成员被初始化为 -1,原因我们将在后面提到 Next() 的时候讲到。

回到 ServeHTTP,它新建一个 Context 之后,将 middlewares 和 engine 赋值给 Context 对应的字段,便可以开始处理具体的业务了。这里我需要再强调一下,ServeHTTP 这个方法做的事情就是当一个 Request 到来时,处理这个 Request,并返回具体的 Response。一个例子就是当服务开启后,用户在浏览器输入 URL 并进入,此时 ServeHTTP 开始工作,结束后将 Response 反馈到浏览器上(如果有数据要反馈的话)。

具体的业务函数以及中间件通过 engine.router.handle(c) 来进行工作。即通过调用 engine 的 router 成员的 handle 方法来开始工作。所以我们现在来看一下 router 的作用。

router 的结构定义如下:

type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}

顾名思义,router 所做的就是保存注册的 URL,并将具体的业务与 URL 相绑定。当 ServeHTTP 工作时,就需要来到 router 查找具体的 HandlerFunc,调用 HandlerFunc 完成业务处理。

router 的结构中包含两个字段,分别是 roots 和 handlers,二者都是 map 类型,roots 是 map[string]*node,而 handlers 是 map[string]HandlerFunc

我们先来谈 roots。显然 roots 的作用是保存注册的 URL 路径,通过 router 的 addRoute 方法来完成。addRoute 方法的定义如下:

func (r *router) addRoute(method, pattern string, handler HandlerFunc) {
	parts := parsePattern(pattern)

	key := method + "-" + pattern
	_, ok := r.roots[method]
	if !ok {
		r.roots[method] = &node{}
	}
	r.roots[method].insert(pattern, parts, 0)
	r.handlers[key] = handler
}

它的输入是 method 和 pattern 两个 string,method 对应的是 GET 或 POST,而 pattern 就是具体的 URL,最后一个输入是 HandlerFunc 类型的 handler,即要绑定的业务函数。

在 addRouter 当中,首先通过 parsePattern 方法将 URL 以 / 为单位进行分割,得到每一个部分。roots 这个 map 的 key 由 method 和 pattern 共同组成,它的 value 是 *node。通过调用 node 的 insert 方法,将 pattern 插入到 router 当中,最后将 handler 与 method + pattern 构成的 key 相绑定。

重点在于,我们如何将 pattern 插入到 router 当中,node 的 insert 方法背后做了什么。

这就引出了 Trie 树实现的路由字典。Trie 树是一种字典树,它以字符串当中共同的前缀作为树中的节点,向下不断地拓展,直到整个字符串结束。由于 URL 当中天然使用 / 对字符串进行分割,因此非常适合使用 Trie 树来对 URL 进行保存。基于 Trie 树的路由详见:【Gee】Day3:前缀树路由,此处不再重复。

成功将 URL 及其对应的业务函数注册到 router 当中之后,当我们真正处理业务时,我们需要根据用户输入的 URL 找到对应的业务函数。由于 roots 是一个以 string 为 key,以 *node 为 value 的字典,而我们可以利用 *node 当中保存的 pattern 加上 method 的方法恢复出保存着 HandlerFunc 的 key,因此我们需要实现一个查找路由的方法,它的返回值应该是 *node。与此同时,我们实现的 Gee 应该能够进行模糊查询,因此这个方法应该能够将输入 URL 当中的参数与注册 URL 当中的模糊参数进行绑定,因此便有了 getRoute 方法:

func (r *router) getRoute(method, path string) (*node, map[string]string) {
	searchParts := parsePattern(path)
	params := make(map[string]string)
	root, ok := r.roots[method]

	if !ok {
		return nil, nil
	}

	n := root.search(searchParts, 0)

	if n != nil {
		parts := parsePattern(n.pattern)
		for index, part := range parts {
			if part[0] == ':' {
				params[part[1:]] = searchParts[index]
			}
			if part[0] == '*' && len(part) > 1 {
				params[part[1:]] = strings.Join(searchParts[index:], "/")
				break
			}
		}
		return n, params
	}

	return nil, nil
}

简单描述一下 getRoute 的工作流程。如果 roots 中没有注册这个 URL 对应的 method(GET/POST),那么直接返回 nil 即可。如果有 method 的注册,才进一步寻找输入的 URL path 是否注册到了 router 当中。此处调用 node 的 search 方法进行查找,它将会递归地在 Trie 树中查找匹配的 URL。如果能够找到,那么处理模糊参数,保存到 params 这个字典当中,处理完之后将 *node 类型的 n 和 params 返回即可。

根据输入的 URL,查找到 router 中保存的 node 之后,我们就可以开始调用 HandlerFunc 进行业务处理了。我们刚才已经提到,ServeHTTP 方法的最后正是调用了 router 的 handle 方法进行最终的业务处理,它的输入是 *Context,因此 router 需要实现 handle 这个方法来完成最终的业务处理:

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
		key := c.Method + "-" + n.pattern
		c.Params = params
		c.handlers = append(c.handlers, r.handlers[key])
	} else {
		c.handlers = append(c.handlers, func(c *Context) {
			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
	}
	c.Next()
}

首先,通过 getRoute 我们可以得到 node 和模糊参数字典 params,如果 node 不为 nil,我们就根据 method 和 n.pattern 恢复出 handlers 这个字典的 key,通过 key 可以访问到这个 URL 对应的注册业务函数。我们需要将这个函数追加到 Context 的 handlers slice 当中。Context 的 handlers 已经保存了中间件的业务函数,中间件和具体的业务都需要执行,其具体的顺序通过 c.Next() 来控制。

最后,调用 c.Next(),即可开始整个业务的处理。至此,我们梳理清楚了业务的注册以及当一个 HTTP Request 到来时,Gee 是如何 URL 注册的业务函数及中间件对 Request 进行处理的。

我们来看一下 c.Next() 的实现,它是 Context 类型的方法:

func (c *Context) Next() {
	c.index++
	s := len(c.handlers)
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

还记得嘛?之前我们已经提到,index 的初始值为 -1,因此当调用 c.Next() 的时候,会将 index 递增为 0,此时遍历 c.handlers 开始执行对应的业务函数即可。在 c.handlers 当中,保存的不仅仅是具体的业务处理函数,还保存着当前分组下的中间件。中间件的执行顺序与 c.handlers 及下标 index 的变动有关。一个简单的例子是,如果我们想要业务函数先执行,中间件的 index 为 0,而业务函数为 1,那么在中间件的 HandlerFunc 中先调用 c.Next() 即可。

根据 demo 当中给出的 main.go 实现,我们几乎已经讲解完了 Gee 的工作原理。最后我们来谈一下,如何用 Gee 来构建 web 项目。

如何用 Gee 来构建 web 项目?

我们已经提到,目前主流的 web 应用开发模式是前后端分离的,即:前端将输入数据传给后端,后端再将业务处理后的输出结果返回给前端,在前端负责数据渲染与用户交互,而后端专注于业务处理。因此一个 web 框架重要的是如何解析和返回不同格式的数据。

此处我们将这个问题大大地简化,虽然有失一般性,但是却是极具代表性的应用场景,即 Gee 如何处理 JSON 格式的数据?

在 demo 当中,我们使用了:

v2 := r.Group("/v2")
{
	v2.GET("/assets/*filepath", func(c *gee.Context) {
		c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})
	})
}

上述代码通过 GET 方法在客户端显示 JSON 格式的数据,那么我们来看看 c.JSON 究竟做了什么:

func (c *Context) JSON(code int, obj interface{}) {
	c.SetHeader("Content-Type", "application/json")
	c.Status(code)
	encoder := json.NewEncoder(c.Writer)
	if err := encoder.Encode(obj); err != nil {
		http.Error(c.Writer, err.Error(), 500)
	}
}

实际上 c.JSON 就是对 Gee 发送 JSON 格式数据的封装,它会通过 c.SetHeader 设置请求头(实际上是对 c.WriterHeader 进行设置),并设置状态码,最后通过 json 的 Encoder 对 JSON 数据进行编码。传递进来的数据是一个空接口类型,意味着只要符合 JSON 数据的格式,就可以通过 json 的 Encoder 将空接口转为 JSON 结构。

通过新建的 json Encoder 将空接口结构编码并发送,即可完成从后端向前端 JSON 数据的传递。

最后,通过调用:

r.Run(":9999")

即可开启基于 Gee 的 web 服务,服务的地址是 localhost:9999。Run 方法其实就是对 http 的 ListenAndServe 的封装。

至此,我们基本完成了对 Gee 项目的总结,从底层全面回顾了 Gee 的实现,并分析了 Gee 设计模式的理由与合理性。通过 Gee,我加深了自己对 GIN 的理解。原来开发一个 web 框架,甚至说开发轮子,其实并没有那么难。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DeepLabv3+改进10:在主干网络中添加LSKBlock|动态调整其大型空间感受野,助力小目标识别

🔥【DeepLabv3+改进专栏!探索语义分割新高度】 🌟 你是否在为图像分割的精度与效率发愁? 📢 本专栏重磅推出: ✅ 独家改进策略:融合注意力机制、轻量化设计与多尺度优化 ✅ 即插即用模块:ASPP+升级、解码器 PS:订阅专栏提供完整代码 目录 论文简介 步骤一 步骤二…

词向量:优维大模型语义理解的深度引擎

★ 放闸溯源 优维大模型「骨架级」技术干货 第二篇 ⇓ 词向量是Transformer突破传统NLP技术瓶颈的核心&#xff0c;它通过稠密向量空间映射&#xff0c;将离散符号转化为连续语义表示。优维大模型基于词向量技术&#xff0c;构建了运维领域的“语义地图”&#xff0c;实现从…

编译原理:语法分析程序【附源码和超详细注释】

目录 一 、实验目的 二 、实验内容及步骤 三、程序分析 1. 需求分析 2. 功能分析 1. LL(1)文法功能分析 2. 算符优先文法功能分析 3. 信创分析-主要针对能力提升中国产操作系统上开发内容。 四、源代码 1. LL(1)文法代码 2. 算符优先文法 五、测试结果 1. LL(1)文…

使用Flask和OpenCV 实现树莓派与客户端的视频流传输与显示

使用 Python 和 OpenCV 实现树莓派与客户端的视频流传输与显示 在计算机视觉和物联网领域&#xff0c;经常需要将树莓派作为视频流服务器&#xff0c;通过网络将摄像头画面传输到客户端进行处理和显示。本文将详细介绍如何利用picamera2库、Flask 框架以及 OpenCV 库&#xff…

fs的proxy_media模式失效

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 在fs的使用过程中&#xff0c;某些场景只需要对rtp媒体做透传&#xff0c;又不需要任何处理。 在fs1.6的版本中&#xff0c;我们可以使用proxy_media来代理媒体的转发&#xff0c;媒体的协商由AB路端对端处理&#xff…

Linux 命名管道

文章目录 &#x1f680; 深入理解命名管道&#xff08;FIFO&#xff09;及其C实现一、命名管道核心特性1.1 &#x1f9e9; 基本概念 二、&#x1f4bb; 代码实现解析2.1 &#x1f4c1; 公共头文件&#xff08;common.hpp&#xff09;2.2 &#x1f5a5;️ 服务器端&#xff08;s…

HDU 学数数导致的

题目解析 首先&#xff0c;数对是有序的&#xff0c;<1,2>和<2,1>被视为不同的两组数字。 其次&#xff0c;数对<p,q>的p和q可以相等。 子序列为 p 0 p q&#xff0c;观察到&#xff0c;中间要出现一个0。那么&#xff0c;我们只需要找到第一个 p 满足与前…

软件/硬件I2C读写MPU6050

MPU6050简介 6轴&#xff1a;3轴加速度&#xff0c;3轴角速度 9轴&#xff1a;3轴加速度&#xff0c;3轴角速度和3轴磁场强度 10轴&#xff1a;3轴加速度&#xff0c;3轴角速度和3轴磁场强度和一个气压强度 加速度计具有静态稳定性&#xff0c;不具有动态稳定性 欧拉角&…

Android中的Wifi框架系列

Android wifi框架图 Android WIFI系统引入了wpa_supplicant&#xff0c;它的整个WIFI系统以wpa_supplicant为核心来定义上层接口和下层驱动接口。 Android WIFI主要分为六大层&#xff0c;分别是WiFi Settings层&#xff0c;Wifi Framework层&#xff0c;Wifi JNI 层&#xff…

【含文档+PPT+源码】基于Python的图书管理系统的设计与实现

项目介绍 本课程演示的是一款基于Python的图书管理系统的设计与实现&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 带你从零开始部署运行本套系统 该项目附…

开源工具利器:Mermaid助力知识图谱可视化与分享

在现代 web 开发中&#xff0c;可视化工具对于展示流程、结构和数据关系至关重要。Mermaid 是一款强大的 JavaScript 工具&#xff0c;它使用基于 Markdown 的语法来呈现可定制的图表、图表和可视化。对于展示流程、结构和数据关系至关重要。通过简单的文本描述&#xff0c;你可…

茂捷M1001电感式编码器芯片TSSOP28管脚,国产电感式编码器IC

简述&#xff1a; M1001 电感式编码器芯片是一款专为高精度位置检测而设计的芯片产品&#xff0c;采用先进的电感技术&#xff0c;能够精确测量旋转物体的位置和角度。芯片具有 SIN/COS、模拟、PWM、SENT、SPI、I2C等多种角度输出功能&#xff0c;具有高分辨率、宽工作温度范围…

LeetCode-跳跃游戏 II

方法一&#xff1a;反向查找出发位置 我们的目标是到达数组的最后一个位置&#xff0c;因此我们可以考虑最后一步跳跃前所在的位置&#xff0c;该位置通过跳跃能够到达最后一个位置。 如果有多个位置通过跳跃都能够到达最后一个位置&#xff0c;那么我们应该如何进行选择呢&a…

数据结构——双向链表dlist

前言&#xff1a;大家好&#x1f60d;&#xff0c;本文主要介绍了数据结构——双向链表dlist 一 双向链表定义 1. 双向链表的节点结构 二 双向链表操作 2.1 定义 2.2 初始化 2.3 插入 2.3.1 头插 2.3.2 尾插 2.3.3 按位置插 2.4 删除 2.4.1 头删 2.4.2 尾删 2.4.3 按…

IDEA 一键完成:打包 + 推送 + 部署docker镜像

1、本方案要解决场景&#xff1f; 想直接通过本地 IDEA 将最新的代码部署到远程服务器上。 2、本方案适用于什么样的项目&#xff1f; 项目是一个 Spring Boot 的 Java 项目。项目用 maven 进行管理。项目的运行基于 docker 容器&#xff08;即项目将被打成 docker image&am…

图像分类数据集

《动手学深度学习》-3.5-学习笔记 # 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式&#xff0c; # 并除以255使得所有像素的数值均在0&#xff5e;1之间 trans transforms.ToTensor()#用于将图像数据从 PIL 图像格式&#xff08;Python Imaging Library&#xff…

设计模式之美

UML建模 统一建模语言&#xff08;UML&#xff09;是用来设计软件的可视化建模语言。它的语言特点是简单 统一 图形化 能表达软件设计中的动态与静态信息。 UML的分类 动态结构图&#xff1a; 类图 对象图 组件图 部署图 动态行为图&#xff1a; 状态图 活动图 时序图 协作…

2025-03-15 学习记录--C/C++-PTA 练习3-4 统计字符

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 练习3-4 统计字符 本题要求编写程序&#xff0c;输入10个字符&#xff0c;统计其中英文字母、空格或回车、…

802.11标准

系列文章目录 文章目录 系列文章目录一、相关知识二、使用步骤1.802.11修正比较2.802.11ac 三、杂记 一、相关知识 跳频扩频&#xff1a;射频信号可分为窄带信号和扩频信号。如果射频信号的带宽大于承载数据所需的带宽&#xff0c;该信号就属于扩频信号。跳频扩频(FHSS)是一种…

母婴商城系统Springboot设计与实现

项目概述 《母婴商城系统Springboot》是一款基于Springboot框架开发的母婴类电商平台&#xff0c;旨在为母婴产品提供高效、便捷的在线购物体验。该系统功能全面&#xff0c;涵盖用户管理、商品分类、商品信息、商品资讯等核心模块&#xff0c;适合母婴电商企业或个人开发者快…