当前位置: 首页 > news >正文

【GO基础学习】gin框架路由详解

文章目录

  • gin框架路由详解
    • (1)go mod tidy
    • (2)r := gin.Default()
    • (3)r.GET()
      • 路由注册
    • (4)r.Run()
      • 路由匹配
    • 总结


gin框架路由详解

先创建一个项目,编写一个简单的demo,对这个demo进行讲解。

  1. 创建一个go的项目,采用GoLand:

在这里插入图片描述
2. 在该项目下创建一个main.go文件:

package mainimport ("net/http""github.com/gin-gonic/gin"
)func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "hello word")})r.Run(":8000")
}

上面就是一个非常简单的gin的使用,逐行代码解读,在解读前,需要先下载gin库,写入main.go后,在terminal执行go mod tidy命令就会添加main里面的gin库。

(1)go mod tidy

  • 添加缺失的依赖

如果你的代码中引用了某些依赖(通过 import),但它们没有被记录在 go.mod 文件中,go mod tidy 会自动将这些缺失的依赖添加到 go.mod 文件中。

  • 移除未使用的依赖

如果你的代码中不再使用某些依赖(即没有通过 import 引用),go mod tidy 会从 go.mod 文件中移除这些无用的依赖。

  • 更新 go.sum 文件

go mod tidy 会检查 go.sum 文件(存储模块的校验和)是否与 go.mod 文件一致:

​ -如果某些依赖的校验和缺失,它会添加。

​ -如果某些校验和多余(对应的依赖已被移除),它会删除。

(2)r := gin.Default()

创建默认的Gin引擎,点进这个方法查看源码:

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine.With(opts...)
}

返回了Engine的指针结构体,还包括一些日志和中断复原的操作。

关于Engine结构体:【是 Gin 框架的核心结构体,它既是路由表的管理器,也是 HTTP 服务的入口】

type Engine struct {RouterGrouptrees       methodTrees  // 每种 HTTP 方法对应的路由树maxParams   uint16       // 路由参数最大数量maxSections uint16       // 路由路径最大分段数量handlers404 HandlersChain // 404 处理函数链// 其他字段...
}
  • trees: 存储路由表的核心字段,每种 HTTP 方法有一棵对应的 Radix 树。

  • RouterGroup: 用于管理路由组和中间件。

  • handlers404: 默认的 404 错误处理。

关于trees是路由规则的核心,存储路由表,每种 HTTP 方法有一棵对应的 Radix 树。
(1)Radix 树
公共前缀的树结构,是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:
在这里插入图片描述
(2)methodTrees

type methodTree struct {method stringroot   *node
}type methodTrees []methodTree

method是http的类型,每个路由路径的片段都由一个node节点构成:

type node struct {path      string        // 当前节点的路径部分indices   string        // 子节点的索引,用于快速查找children  []*node       // 子节点handlers  HandlersChain // 当前节点的处理函数priority  uint32        // 优先级,用于优化匹配顺序wildChild bool          // 是否包含通配符子节点nType     nodeType      // 节点类型: static, param, catchAll
}

path: 存储路径片段。

indices: 子节点索引,表示每个子节点的第一个字符,用于快速查找。

handlers: 当前节点绑定的处理函数。

wildChild: 是否有动态或通配符子节点。

nType:

  • static: 静态路径节点。
  • param: 动态路径节点(如 :id)。
  • catchAll: 通配符节点(如 *filepath)。

(3)r.GET()

Gin 的路由实现主要分为路由注册路由匹配两部分。

路由注册

点进GET方法:

// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}

handle方法:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}

addRoute方法:【注册路由的核心函数】

func (e *Engine) addRoute(method, path string, handlers HandlersChain) {root := e.trees.get(method) // 获取当前方法对应的路由树if root == nil {root = new(node)       // 如果路由树不存在,创建新的树e.trees = append(e.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers) // 将路径插入到 Radix 树中
}

Radix 树节点插入逻辑:addRoute in node
在 node 中的 addRoute 方法负责将路径拆分并插入到树中:

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := pathn.priority++numParams := countParams(path)  // 数一下参数个数// 空树就直接插入当前节点if len(n.path) == 0 && len(n.children) == 0 {n.insertChild(numParams, path, fullPath, handlers)n.nType = rootreturn}parentFullPathIndex := 0walk:for {// 更新当前节点的最大参数个数if numParams > n.maxParams {n.maxParams = numParams}// 找到最长的通用前缀// 这也意味着公共前缀不包含“:”"或“*” /// 因为现有键不能包含这些字符。i := longestCommonPrefix(path, n.path)// 分裂边缘(此处分裂的是当前树节点)// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点if i < len(n.path) {child := node{path:      n.path[i:],  // 公共前缀后的部分作为子节点wildChild: n.wildChild,indices:   n.indices,children:  n.children,handlers:  n.handlers,priority:  n.priority - 1, //子节点优先级-1fullPath:  n.fullPath,}// Update maxParams (max of all children)for _, v := range child.children {if v.maxParams > child.maxParams {child.maxParams = v.maxParams}}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = falsen.fullPath = fullPath[:parentFullPathIndex+i]}// 将新来的节点插入新的parent节点作为子节点if i < len(path) {path = path[i:]if n.wildChild {  // 如果是参数节点parentFullPathIndex += len(n.path)n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// 检查通配符是否匹配if len(path) >= len(n.path) && n.path == path[:len(n.path)] {// 检查更长的通配符, 例如 :name and :namesif len(n.path) >= len(path) || path[len(n.path)] == '/' {continue walk}}pathSeg := pathif n.nType != catchAll {pathSeg = strings.SplitN(path, "/", 2)[0]}prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.pathpanic("'" + pathSeg +"' in new path '" + fullPath +"' conflicts with existing wildcard '" + n.path +"' in existing prefix '" + prefix +"'")}// 取path首字母,用来与indices做比较c := path[0]// 处理参数后加斜线情况if n.nType == param && c == '/' && len(n.children) == 1 {parentFullPathIndex += len(n.path)n = n.children[0]n.priority++continue walk}// 检查路path下一个字节的子节点是否存在// 比如s的子节点现在是earch和upport,indices为eu// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点for i, max := 0, len(n.indices); i < max; i++ {if c == n.indices[i] {parentFullPathIndex += len(n.path)i = n.incrementChildPrio(i)n = n.children[i]continue walk}}// 否则就插入if c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65// 注意这里是直接拼接第一个字符到n.indicesn.indices += string([]byte{c})child := &node{maxParams: numParams,fullPath:  fullPath,}// 追加子节点n.children = append(n.children, child)n.incrementChildPrio(len(n.indices) - 1)n = child}n.insertChild(numParams, path, fullPath, handlers)return}// 已经注册过的节点if n.handlers != nil {panic("handlers are already registered for path '" + fullPath + "'")}n.handlers = handlersreturn}
}

整个路由树构造的详细过程:

(1)第一次注册路由,例如注册search
(2)继续注册一条没有公共前缀的路由,例如blog
(3)注册一条与先前注册的路由有公共前缀的路由,例如support

路由注册示例:

package mainimport ("github.com/gin-gonic/gin"
)func main() {r := gin.Default()// 注册静态路由r.GET("/hello", func(c *gin.Context) {c.String(200, "Hello, World!")})// 注册动态路由r.GET("/user/:id", func(c *gin.Context) {id := c.Param("id")c.String(200, "User ID: %s", id)})// 注册通配符路由r.GET("/static/*filepath", func(c *gin.Context) {filepath := c.Param("filepath")c.String(200, "Filepath: %s", filepath)})r.Run(":8080")
}

(4)r.Run()

路由匹配

路由匹配是根据请求路径在 Radix 树中查找对应节点并执行处理函数的过程。

核心代码:getValue

getValue 方法负责在 Radix 树中查找路径:
(1)Run()方法:

/ Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()if engine.isUnsafeTrustedProxies() {debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return
}

(2)Handler()方法处理类:

func (engine *Engine) Handler() http.Handler {if !engine.UseH2C {return engine}h2s := &http2.Server{}return h2c.NewHandler(engine, h2s)
}

(3)http.Handler

type Handler interface {ServeHTTP(ResponseWriter, *Request)
}

(4)ServeHTTP实现:

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 这里使用了对象池c := engine.pool.Get().(*Context)// 这里有一个细节就是Get对象后做初始化c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数engine.pool.Put(c)  // 处理完请求后将对象放回池子
}

(5)handleHTTPRequest方法

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {// 根据请求方法找到对应的路由树t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// 在路由树中根据path查找value := root.getValue(rPath, c.Params, unescape)if value.handlers != nil {c.handlers = value.handlersc.Params = value.paramsc.fullPath = value.fullPathc.Next()  // 执行函数链条c.writermem.WriteHeaderNow()return}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)
}

(6)getValue方法
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
如果找不到任何处理函数,则会尝试TSR(尾随斜杠重定向)。

// tree.gotype nodeValue struct {handlers HandlersChainparams   Params  // []Paramtsr      boolfullPath string
}func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {value.params = po
walk: // Outer loop for walking the treefor {prefix := n.pathif path == prefix {// 我们应该已经到达包含处理函数的节点。// 检查该节点是否注册有处理函数if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if path == "/" && n.wildChild && n.nType != root {value.tsr = truereturn}// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if indices[i] == '/' {n = n.children[i]value.tsr = (len(n.path) == 1 && n.handlers != nil) ||(n.nType == catchAll && n.children[0].handlers != nil)return}}return}if len(path) > len(prefix) && path[:len(prefix)] == prefix {path = path[len(prefix):]// 如果该节点没有通配符(param或catchAll)子节点// 我们可以继续查找下一个子节点if !n.wildChild {c := path[0]indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if c == indices[i] {n = n.children[i] // 遍历树continue walk}}// 没找到// 如果存在一个相同的URL但没有末尾/的叶子节点// 我们可以建议重定向到那里value.tsr = path == "/" && n.handlers != nilreturn}// 根据节点类型处理通配符子节点n = n.children[0]switch n.nType {case param:// find param end (either '/' or path end)end := 0for end < len(path) && path[end] != '/' {end++}// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[1:]val := path[:end]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(val); err != nil {value.params[i].Value = val // fallback, in case of error}} else {value.params[i].Value = val}// 继续向下查询if end < len(path) {if len(n.children) > 0 {path = path[end:]n = n.children[0]continue walk}// ... but we can'tvalue.tsr = len(path) == end+1return}if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if len(n.children) == 1 {// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数// 用于 TSR 推荐n = n.children[0]value.tsr = n.path == "/" && n.handlers != nil}returncase catchAll:// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[2:]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(path); err != nil {value.params[i].Value = path // fallback, in case of error}} else {value.params[i].Value = path}value.handlers = n.handlersvalue.fullPath = n.fullPathreturndefault:panic("invalid node type")}}// 找不到,如果存在一个在当前路径最后添加/的路由// 我们会建议重定向到那里value.tsr = (path == "/") ||(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&path == prefix[:len(prefix)-1] && n.handlers != nil)return}
}

Radix 树的路径匹配过程:

package mainimport ("fmt"
)type node struct {path     stringchildren []*nodehandlers func()
}func (n *node) addRoute(path string, handler func()) {child := &node{path: path, handlers: handler}n.children = append(n.children, child)
}func (n *node) getRoute(path string) func() {for _, child := range n.children {if child.path == path {return child.handlers}}return nil
}func main() {root := &node{}root.addRoute("/hello", func() {fmt.Println("Hello, World!")})handler := root.getRoute("/hello")if handler != nil {handler() // 输出:Hello, World!} else {fmt.Println("Route not found!")}
}

总结

  1. 创建路由表
    • 每种 HTTP 方法有独立的 Radix 树。
    • 路由通过 addRoute 插入到对应的树中。
  2. 处理 HTTP 请求
    • Gin 的入口是 EngineServeHTTP 方法。
    • 根据请求方法和路径查找路由节点:
      • 如果找到,执行绑定的处理函数。
      • 如果未找到,执行 404 处理函数。
  3. 分发请求
    • 匹配成功的路由节点的处理函数会被依次执行,支持中间件链。

相关文章:

【GO基础学习】gin框架路由详解

文章目录 gin框架路由详解&#xff08;1&#xff09;go mod tidy&#xff08;2&#xff09;r : gin.Default()&#xff08;3&#xff09;r.GET()路由注册 &#xff08;4&#xff09;r.Run()路由匹配 总结 gin框架路由详解 先创建一个项目&#xff0c;编写一个简单的demo&#…...

GPIO+TIM(无PWM)实现呼吸灯功能

程序特点&#xff1a; 1、模块化&#xff0c;可快速移植&#xff0c;5分钟便可完成移植。 2、通过GPIO普通定时器&#xff0c;实现呼吸灯功能。 3、PWM周期为5ms&#xff0c;占空比调节时间为20ms&#xff0c;占空比为100等份&#xff0c;即呼吸灯从暗到亮需要20ms*1002s。 …...

贪心算法.

贪心算法是指只从当前角度出发,做出当前情景下最好的选择,在某种意义上来说是局部最优解,并不从全局的角度做决策.如果贪心策略选择不恰当,可能无法得到全局最优解. 贪心算法的基本流程如下: 1.分析问题,确定优化目标,对变量进行初始化 2.制定贪心策略:在制定贪心策略时需要…...

Linux系统和makefile详解

### Linux系统详解 Linux是一个开源且功能强大的操作系统内核&#xff0c;自1991年由林纳斯托瓦兹首次发布以来&#xff0c;它已经成为全球最流行的操作系统之一。Linux的核心特性包括开源、多用户多任务、高稳定性与安全性&#xff0c;以及良好的跨平台能力。 1. **开源**&a…...

GitLab 将停止为中国区用户提供服务,60天迁移期如何应对? | LeetTalk Daily

“LeetTalk Daily”&#xff0c;每日科技前沿&#xff0c;由LeetTools AI精心筛选&#xff0c;为您带来最新鲜、最具洞察力的科技新闻。 GitLab作为一个广受欢迎的开源代码托管平台&#xff0c;近期宣布将停止服务中国大陆、澳门和香港地区的用户提供服务。根据官方通知&#x…...

【杂谈】-AI搜索引擎如何改变传统SEO及其在内容营销中的作用

AI搜索引擎如何改变传统SEO及其在内容营销中的作用 文章目录 AI搜索引擎如何改变传统SEO及其在内容营销中的作用1、什么是AI搜索引擎2、AI搜索引擎对SEO策略的影响3、AI搜索引擎在内容营销转型中的作用4、AI搜索引擎在营销领域的挑战、道德问题和未来5、总结 在当今的数字营销世…...

PTA数据结构编程题7-1最大子列和问题

我参考的B站up的思路 题目 题目链接 给定K个整数组成的序列{ N 1 ​ , N 2 ​ , …, N K ​ }&#xff0c;“连续子列”被定义为{ N i ​ , N i1 ​ , …, N j ​ }&#xff0c;其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 1…...

深入浅出:AWT的基本组件及其应用

目录 前言 1. AWT简介 2. AWT基本组件 2.1 Button&#xff1a;按钮 2.2 Label&#xff1a;标签 ​编辑 2.3 TextField&#xff1a;文本框 2.4 Checkbox&#xff1a;复选框 2.5 Choice&#xff1a;下拉菜单 2.6 List&#xff1a;列表 综合案例 注意 3. AWT事件处理 …...

MySQL45讲 第三十六讲 为什么临时表可以重名?——阅读总结

文章目录 MySQL45讲 第三十六讲 为什么临时表可以重名&#xff1f;——阅读总结一、引言二、临时表与内存表的区别&#xff08;一&#xff09;内存表&#xff08;二&#xff09;临时表 三、临时表的特性&#xff08;一&#xff09;可见性与生命周期&#xff08;二&#xff09;与…...

WebRTC服务质量(11)- Pacer机制(03) IntervalBudget

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…...

.NET常用的ORM框架及性能优劣分析总结

市面上有很多流行的 ORM&#xff08;对象关系映射&#xff09;框架可以用于 .NET 开发。本文主要针对以下几种常见的 ORM 框架&#xff0c;对其优劣进行分析及总结&#xff0c;希望能够帮助大家进行ORM框架的使用有所帮助。 1. Entity Framework (EF) 特点 • 官方支持&…...

Ubuntu网络配置(桥接模式, nat模式, host主机模式)

windows上安装了vmware虚拟机&#xff0c; vmware虚拟机上运行着ubuntu系统。windows与虚拟机可以通过三种方式进行通信。分别是桥接模式&#xff1b;nat模式&#xff1b;host模式 一、桥接模式 所谓桥接模式&#xff0c;也就是虚拟机与宿主机处于同一个网段&#xff0c; 宿主机…...

光通信复习

第一章 1.5 光纤通信系统的基本组成是怎么样的&#xff1f;试画出简图予以说明 光纤&#xff1a;主要负责光信号的传输光发送器&#xff1a;将用户端的电信号转化为光信号&#xff0c;入射到光纤内部光中继器&#xff1a;将光纤中发生衰减和畸变的光信号变成没有衰减和畸变的原…...

数字化转型中的投资决策:IT平台投资与业务应用投资的思考

在数字化转型的大潮中&#xff0c;企业常常面临一个核心问题&#xff1a;如何在繁杂的投资决策中精准地分配资源&#xff0c;特别是在IT平台投资和业务应用投资之间&#xff0c;如何合理划分责任与投入&#xff1f;在一些大型企业中&#xff0c;尤其是华为&#xff0c;针对不同…...

Linux快速入门-Linux的常用命令

Linux的常用命令 1. Linux的终端与工作区1.1 终端概述1.2 切换终端 2. Shell语言解释器2.1 Shell概述 3. 用户登录与身份切换3.1 su 命令3.2 sudo 命令 4. 文件、目录操作命令4.1 pwd 命令4.2 cd 命令4.3 ls 命令4.3.1 ls 指令叠加使用 4.4 mkdir 命令4.5 rmdir 命令4.6 cp 命令…...

【ORB-SLAM3:相机针孔模型和相机K8模型】

在ORB-SLAM3中&#xff0c;相机的建模是 SLAM 系统的核心之一&#xff0c;因为它直接影响到如何处理和利用图像数据进行定位和地图构建。ORB-SLAM3 支持不同的相机模型&#xff0c;其中包括针孔模型和鱼眼模型&#xff08;K8 模型&#xff09;。下面分别介绍这两种模型。 相机…...

Python函数(十二):函数的创建和调用、参数传递、返回值

前言&#xff1a;在编程的世界里&#xff0c;函数是一种基本的构建块&#xff0c;它允许我们将代码封装成可重复使用的单元。在Python中&#xff0c;函数的使用尤为重要&#xff0c;因为它不仅有助于代码的模块化&#xff0c;还提高了代码的可读性和可维护性。本章节&#xff0…...

掌握Docker命令与Dockerfile实战技巧:快速构建高效容器化应用

1. 介绍 Docker 是现代开发和运维的必备工具&#xff0c;集成了容器技术的优势。本文将记录 Docker 的常用指令&#xff0c;并会随着使用经验的积累进行不定期更新。 2. 常用命令 2.1 启动容器&#xff08;前台交互模式&#xff09; docker run --privileged --volume /hom…...

Virtualbox硬盘扩容

前言 有没有使用虚拟机安装操作系统的时候&#xff0c;虚拟硬盘一开始分配的虚拟硬盘空间不够用&#xff1f;在后期去扩容的伙伴们&#xff0c;下面我看看如何扩容virtualbox的虚拟硬盘&#xff1f; 重新分配虚拟硬盘大小 在virtualbox菜单选择【管理】-【工具】-【虚拟介质…...

10G光纤反射内存卡

在科技日新月异的今天&#xff0c;数据存储技术正以前所未有的速度发展&#xff0c;其中&#xff0c;“10G光纤反射内存卡”作为新一代存储技术的佼佼者&#xff0c;正逐步引领着数据存储领域的新风尚。本文将深入探讨这一创新产品的技术原理、性能优势、应用场景以及未来展望&…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)

说明&#xff1a; 想象一下&#xff0c;你正在用eNSP搭建一个虚拟的网络世界&#xff0c;里面有虚拟的路由器、交换机、电脑&#xff08;PC&#xff09;等等。这些设备都在你的电脑里面“运行”&#xff0c;它们之间可以互相通信&#xff0c;就像一个封闭的小王国。 但是&#…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

华为OD机试-食堂供餐-二分法

import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...

Qt Http Server模块功能及架构

Qt Http Server 是 Qt 6.0 中引入的一个新模块&#xff0c;它提供了一个轻量级的 HTTP 服务器实现&#xff0c;主要用于构建基于 HTTP 的应用程序和服务。 功能介绍&#xff1a; 主要功能 HTTP服务器功能&#xff1a; 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)

目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 &#xff08;1&#xff09;输入单引号 &#xff08;2&#xff09;万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...

DeepSeek越强,Kimi越慌?

被DeepSeek吊打的Kimi&#xff0c;还有多少人在用&#xff1f; 去年&#xff0c;月之暗面创始人杨植麟别提有多风光了。90后清华学霸&#xff0c;国产大模型六小虎之一&#xff0c;手握十几亿美金的融资。旗下的AI助手Kimi烧钱如流水&#xff0c;单月光是投流就花费2个亿。 疯…...

网页端 js 读取发票里的二维码信息(图片和PDF格式)

起因 为了实现在报销流程中&#xff0c;发票不能重用的限制&#xff0c;发票上传后&#xff0c;希望能读出发票号&#xff0c;并记录发票号已用&#xff0c;下次不再可用于报销。 基于上面的需求&#xff0c;研究了OCR 的方式和读PDF的方式&#xff0c;实际是可行的&#xff…...

C++ 类基础:封装、继承、多态与多线程模板实现

前言 C 是一门强大的面向对象编程语言&#xff0c;而类&#xff08;Class&#xff09;作为其核心特性之一&#xff0c;是理解和使用 C 的关键。本文将深入探讨 C 类的基本特性&#xff0c;包括封装、继承和多态&#xff0c;同时讨论类中的权限控制&#xff0c;并展示如何使用类…...