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

Go实战训练之Web Server 与路由树

Server & 路由树

Server

Web 核心

对于一个 Web 框架,至少要提供三个抽象:

  • Server:代表服务器的抽象
  • Context:表示上下文的抽象
  • 路由树

Server

从特性上来说,至少要提供三部分功能:

  • 生命周期控制:即启动,关闭,生命周期的回调;
  • 路由注册接口:提供路由注册功能;
  • 作为 http 包到 Web 框架的桥梁

http.Handler接口

http 包暴露了一个接口 Handler。
它是我们引入自定义 Web 框架相关的连接点。

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}

接口的定义

版本一:只组合 http.Handler

//Server file
type Server interface {http.Handler
}
//Server_test file
func TestServer(t *testing.T) {var s Serverhttp.ListenAndServe("localhost:8080", s)
}

优点:

  • 用户在使用的时候只需要调用 http.ListenAndServe 就可以
  • 和 HTTPS 协议完全无缝衔接
  • 极简设计

缺点:

  • 难以控制生命周期,并且在控制生命周期的时候增加回调支持
  • 缺乏控制力:如果将来希望支持优雅退出的功能,将难以支持

版本二:组合 http.Handler 并且增加 Start 方法。

//Server file
type Server interface {http.HandlerStart(addr string) error
}
//Server_test file
func TestServer(t *testing.T) {var s Serverhttp.ListenAndServe("localhost:8080", s)s.Start(":8080")//Start 方法可以不需要 addr 参数,那么在创建实现类的时候传入地址就可以。
}

优点:

  • Server 既可以当成普通的 http.Handler 来使用,又可以作为一个独立的实体,拥有自己的管理生命周期的能力
  • 完全的控制,可以为所欲为

缺点:

  • 如果用户不希望使用 ListenAndServeTLS,那么 Server 需要提供 HTTPS 的支持

版本一和版本二都直接耦合了 Go 自带的 http 包,如果我们希望切换为 fasthttp 或者类似的 http 包,则会非常困难。

HTTPServer 实现 Server

// Server_test file
type HTTPServer struct {}var _Server = &HTTPServer{}func (s *HTTPServer) ServerHTTP(writer http.ResponseWriter, request *http.Request)  {}func (s *HTTPServer) Start(addr string) error {return http.ListenAndServe(addr, s)
}

该实现直接使用 http.ListenAndServe 来启动,后续可以根据需要替换为:

  • 内部创建 http.Server 来启动
  • 使用 http.Serve 来启动,换取更大的灵活性,如将端口监听和服务器启动分离等

ServeHTTP 则是我们整个 Web 框架的核心入口。我们将在整个方法内部完成:

  • Context 构建
  • 路由匹配
  • 执行业务逻辑

路由树

实现路由树得步骤:

  • 全静态匹配
  • 支持通配符匹配
  • 支持参数路由

不同框架路由树的实现:

  • Beego:
    • ControllerRegister:类似于容器,放着所有的路由树
      • 路由树是按照 HTTP method 来组织的,例如 GET 方法会对应有一棵路由树
    • Tree:它代表的就是路由树,在 Beego 里
      面,一棵路由树被看做是由子树组成的
    • leafInfo:代表叶子节点
    • 树的设计并没有采用 children 式来定义,而是采用递归式的定义,即一棵树是由根节点 + 子树构成

  • Gin:
    • methodTrees:也就是路由树也是按照 HTTP 方法组织的,例如 GET 会有一棵路由树
    • methodTree:定义了单棵树。树在 Gin 里面采用的是 children 的定义方式,即树由节点构成(注意对比 Beego)
    • node:代表树上的一个节点,里面维持住了 children,即子节点。同时有 nodeType 和 wildChild 来标记一些特殊节点
    • gin是利用路由的公共前缀来构造路由树

路由树的设计总结:

归根结底就是设计一颗多叉树。

  • 同时我们按照 HTTP 方法来组织路由树,每个 HTTP 方法一棵树
  • 节点维持住自己的子节点

静态匹配

所谓的静态匹配,就是路径的每一段都必须严格相等。

接口设计

关键类型:

  • router:维持住了所有的路由树,它是整个路由注册和查找的总入口。router 里面维护了一个 map,是按照 HTTP 方法来组织路由树的
  • node:代表的是节点。它里面有一个 children 的 map 结构,使用 map 结构是为了快速查找到子节点
type router struct {// trees 是按照 HTTP 方法来组织的// 如 GET => *nodetrees map[string]*node
}type node struct {path string//children 子节点//子节点的 path => *nodechildren map[string]*node//handler 命中路由之后的逻辑handler HandlerFunc
}func newRouter() router {return router{trees: map[string]*node{},}
}

用简化版的 TDD,即:

  1. 定义 API
  2. 定义测试
  3. 添加测试用例
  4. 实现,并且确保实现能够通过测试用例
  5. 重复 3-4 直到考虑了所有的场景
  6. 重复步骤 1-5

全局静态匹配

  • 方法路由设计
{method: http.MethodGet,path:   "/",
},
{method: http.MethodGet,path:   "/user",
},
{method: http.MethodGet,path:   "/user/home",
},
{method: http.MethodGet,path:   "/order/detail",
},
{method: http.MethodPost,path:   "/order/create",
},
{method: http.MethodPost,path:   "/login",
},
  • 非法用例进行判断
//非法样例
r = newRouter()//空字符串
assert.PanicsWithValue(t, "web: 路由是空字符串", func() {r.addRoute(http.MethodGet, "", mockHandler)
})//前导没有 /
assert.PanicsWithValue(t, "web: 路由必须以 / 开头", func() {r.addRoute(http.MethodGet, "a/b/c", mockHandler)
})
//后缀有 /
assert.PanicsWithValue(t, "web: 路由不能以 / 结尾", func() {r.addRoute(http.MethodGet, "/a/b/c/", mockHandler)
})r.addRoute(http.MethodGet, "/", mockHandler)
// 根节点重复注册
assert.PanicsWithValue(t, "web: 路由重复注册", func() {r.addRoute(http.MethodGet, "/", mockHandler)
})
// 普通节点重复注册
r.addRoute(http.MethodGet, "/a/b/c", mockHandler)
assert.PanicsWithValue(t, http.MethodGet, func() {r.addRoute(http.MethodGet, "/a/b/c", mockHandler)
})// 多个
assert.PanicsWithValue(t, "web: 非法路由。不允许使用 //a/b, /a//b 之类的路由, [/a//b]", func() {r.addRoute(http.MethodGet, "/a//b", mockHandler)
})
assert.PanicsWithValue(t, "web: 非法路由。不允许使用 //a/b, /a//b 之类的路由, [//a/b]", func() {r.addRoute(http.MethodGet, "//a/b", mockHandler)
})
  • 添加路由
// addRoute 注册路由
// method 是 HTTP 方法
//	path 必须以 / 开始并且结尾不能有 /,中间也不允许有连续的 /
func (r *router) addRoute(method string, path string, handler HandlerFunc) {// 空字符串判断if path == "" {panic("web: 路由是空字符串")}// 必须以 / 为开头判断if path[0] != '/' {panic("web: 路由必须以 / 开头")}// 结尾不能有 /if path != "/" && path[len(path)-1] == '/' {panic("web: 不能以 / 为结尾")}root, ok := r.trees[method]//这是一个全新的 HTTP 方法,我们必须创建根节点if !ok {root = &node{path: "/",}r.trees[method] = root}// 路径冲突if path == "/" {if root.handler != nil {panic("web: 路径冲突")}root.handler = handlerreturn}// 对路由进行切割segs := strings.Split(path[1:], "/")//开始进行处理for _, s := range segs {// 对空路径进行判断if s == "" {panic(fmt.Sprintf("web: 非法路由。不允许使用 //a/b, /a//b 之类的路由, [%s]", path))}root = root.childrenOfCreate(s)}if root.handler != nil {panic(fmt.Sprintf("web: 路由冲突[%s]", path))}
}
  • 查找或者创建一个子节点
//子节点不存在则创建一个子节点
func (n *node) childrenOfCreate(path string) *node {if n.children == nil {n.children = make(map[string]*node)}child, ok := n.children[path]if !ok {child = &node{path: path}n.children[path] = child}return child
}
  • 对 method 进行校验

    定义为私有的 addRoute 即可实现:

    • 用户只能通过 Get 或者 Post 方法来注册,那么可以确保 method 参数永远都是对的
    • addRoute 在接口里面是私有的,限制了用户将无法实现 Server。实际上如果用户想要实现 Server,就约等于自己实现一个 Web 框架了。

path 之所以会有那么强的约束,是因为我希望用户写出来的代码风格要一致,就是注册的路由有些人喜欢加 /,有些人不喜欢加 /。

  • 路由查找
// test_file
// 待测试路由
testRoutes := []struct {method stringpath   string
}{{method: http.MethodGet,path:   "/",},{method: http.MethodGet,path:   "/user",},{method: http.MethodGet,path:   "/order/create",},{method: http.MethodGet,path:   "/user/*/home",},{method: http.MethodGet,path:   "/order/*",},
}
// 测试样例
testCase := []struct {name     stringmethod   stringpath     stringfound    boolwantNode *node
}{{name:   "method not found",method: http.MethodHead,},{name:   "path not found",method: http.MethodGet,path:   "/abc",},{name:   "root",method: http.MethodGet,found:  true,path:   "/",wantNode: &node{path:    "/",handler: mockHandler,},},{name:   "user",method: http.MethodGet,found:  true,path:   "/user",wantNode: &node{path:    "user",handler: mockHandler,},},{name:   "no handler",method: http.MethodPost,found:  true,path:   "/order",wantNode: &node{path: "order",},},{name:   "two layer",method: http.MethodPost,found:  true,path:   "/order/create",wantNode: &node{path:    "create",handler: mockHandler,},},
}
  • 实现代码
// 路由查找实现
func (r *router) findRoute(method string, path string) (*node, bool) {root, ok := r.trees[method]if !ok {return nil, false}if path == "/" {return root, true}segs := strings.Split(strings.Trim(path, "/"), "/")for _, s := range segs {root, ok = root.childof(s)if !ok {return nil, false}}return root, true
}//判断是否存在子节点
func (n *node) childof(path string) (*node, bool) {if n.children == nil {return nil, false}res, ok := n.children[path]return res, ok
}
  • Server 集成 router

这种情况下,用户只能使用 NewHTTPServer 来创建服务器实例。

如果考虑到用户可能自己 s := &HTTPServer 引起 panic,那么可以将 HTTPServer
做成私有的,即改名为 httpServer。

type HTTPServer struct{ router }func NewHTTPServer() *HTTPServer {return &HTTPServer{router: newRouter(),}
}
func (s *HTTPServer) server(ctx *Context) {n, ok := s.findRoute(ctx.Req.Method, ctx.Req.URL.Path)if !ok || n.handler == nil {ctx.Resp.WriteHeader(404)ctx.Resp.Write([]byte("Not Found"))return}n.handler(ctx)
}
  • 通配符匹配

    所谓通配符匹配,是指用 * 号来表达匹配任何路径。要考虑几个问题:

    • 如果路径是 /a/b/c 能不能命中 /a/* 路由?
    • 如果注册了两个路由 /user/123/home/user/*/*。那么输入路径 /user/123/detail 能不能命中 /user/*/*?

    这两个都是理论上可以,但是不应该命中。

    • 从实现的角度来说,其实并不难。
    • 从用户的角度来说,他们不应该设计这种路由。给用户自由,但是也要限制不良实践。
    • 后者要求的是一种可回溯的路由匹配,即发现 /user/123/home 匹配不上之后要回溯回去 /user/* 进一步查找,典型的投入大产出低的特性。

对 node 进行修改:

// node 代表路由树的节点
// 路由树的匹配顺序为:
// 1、静态完全匹配
// 2、通配符匹配
// 这是不回溯匹配
type node struct {path string//children 子节点//子节点的 path => *nodechildren map[string]*node//handler 命中路由之后的逻辑handler HandlerFunc//通配符 * 表示的节点,任意匹配starChild *node
}

对 childof 进行修改:

func (n *node) childof(path string) (*node, bool) {if n.children == nil {return n.starChild, n.starChild != nil}res, ok := n.children[path]if !ok {return n.starChild, n.starChild != nil}return res, ok
}

对 childOrCreate 进行修改:

func (n *node) childrenOfCreate(path string) *node {if path == "*" {if n.children == nil {n.starChild = &node{path: "*"}}}if n.children == nil {n.children = make(map[string]*node)}child, ok := n.children[path]if !ok {child = &node{path: path}n.children[path] = child}return child
}

增加 addRouter 测试用例

{method: http.MethodGet,path:   "/order/*",
},
{method: http.MethodGet,path:   "/*",
},
{method: http.MethodGet,path:   "/*/*",
},
{method: http.MethodGet,path:   "/*/abc",
},
{method: http.MethodGet,path:   "/*/abc/**",
},

增加 findRoute 测试用例

// 通配符匹配
{// 命中/order/*name:   "star match",method: http.MethodPost,path:   "/order/delete",found:  true,wantNode: &node{path:    "*",handler: mockHandler,},
},
{//命中中间的通配符// user/*/homename:   "start in middle",method: http.MethodGet,path:   "/user/Tom/home",wantNode: &node{path:    "home",handler: mockHandler,},
},
{// 比/order/* 多一段name:   "overflow",method: http.MethodPost,path:   "/order/delete/123",
},
  • 参数路径

    所谓参数路径,就是指在路径中带上参数,同时这些参数对应的值可以被业务取出来使用。

    例如:/user/:id ,如果输入路径 /user/123,那么会命中这个路由,并且 id = 123

    那么要考虑:

    • 允不允许同样的参数路径和通配符匹配一起注册?例如同时注册 /user/*/user/:id

    可以,但是没必要,用户也不应该设计这种路由。

对 node 进行修改

type HandlerFunc func(ctx *Context)// node 代表路由树的节点
// 路由树的匹配顺序为:
// 1、静态完全匹配
// 2、通配符匹配
// 这是不回溯匹配
type node struct {path string//children 子节点//子节点的 path => *nodechildren map[string]*node//handler 命中路由之后的逻辑handler HandlerFunc//通配符 * 表示的节点,任意匹配starChild *node//路径参数paramchild *node
}

对 childrenOfCreate 进行变更

func (n *node) childrenOfCreate(path string) *node {if path == "*" {if n.children == nil {n.starChild = &node{path: "*"}}}if path == "*" {if n.paramchild != nil {panic(fmt.Sprintf("web: 非法路由,已有路径参数路由。不允许同时注册通配符路由和参数路由 [%s]", path))}if n.starChild == nil {n.starChild = &node{path: path}}return n.starChild}// 以 : 开头,我们一般认为是参数路由if path[0] == ':' {if n.starChild != nil {panic(fmt.Sprintf("web: 非法路由,已有路径参数路由,不允许同时注册通配符路由和参数路由 [%s]", path))}if n.paramchild != nil {if n.paramchild.path != path {panic(fmt.Sprintf("web: 路由冲突, 参数路由冲突,已有 %s,新注册 %s", &n.paramchild.path, path))}} else {n.paramchild = &node{path: path}}return n.paramchild}if n.children == nil {n.children = make(map[string]*node)}child, ok := n.children[path]if !ok {child = &node{path: path}n.children[path] = child}return child
}

对 childof 进行修改

// childof 返回子节点
// 第一个返回值为 *node 是命中的节点
// 第二个返回值为 bool 代表是否命中参数值
// 第三个返回值为 bool 代表是否命中
func (n *node) childof(path string) (*node, bool, bool) {if n.children == nil {if n.paramchild != nil {return n.paramchild, true, true}return n.starChild, true, n.starChild != nil}res, ok := n.children[path]if !ok {if n.paramchild != nil {return n.paramchild, true, true}return n.starChild, false, n.starChild != nil}return res, true, true
}

获取路径参数

// 修改Context文件
type Context struct {Req        *http.RequestResp       http.ResponseWriterPathParams map[string]string
}
//新增 matchInfo 表示参数信息
type matchInfo struct {n          *nodepathParams map[string]string
}
// 对 findRoute 进行修改
func (r *router) findRoute1(method string, path string) (*matchInfo, bool) {root, ok := r.trees[method]if !ok {return nil, false}if path == "/" {return &matchInfo{n: root}, true}segs := strings.Split(strings.Trim(path, "/"), "/")mi := &matchInfo{}for _, s := range segs {var matchParam boolroot, matchParam, ok = root.childOf1(s)if !ok {return nil, false}if matchParam {mi.addValue(root.path[1:], s)}}mi.n = rootreturn mi, true
}

/user/:id,输入路径 /user/123,那么就相当于把 id = 123 这样一个键值对放进去了 mi (matchInfo) 里面。

路由树总结

  • 已经注册了的路由,无法被覆盖,例如 /user/home 注册两次,会冲突
  • path 必须以 / 开始并且结尾不能有 /,中间也不允许有连续的 /
  • 不能在同一个位置注册不同的参数路由,例如 /user/:id/user/:name 冲突
  • 不能在同一个位置同时注册通配符路由和参数路由,例如 /user/:id/user/* 冲突
  • 同名路径参数,在路由匹配的时候,值会被覆盖,例如 /user/:id/abc/:id,那么 /user/123/abc/456 最终 id = 456

路由树疑问

Q: 为什么在注册路由用 panic?

A: 俗话说,遇事不决用 error。为什么注册路由的过程我们有一大堆 panic ?

这个地方确实可以考虑返回 error。例如 Get 方法,但是这要求用户必须处理返回的 error。

从另外一个角度来说,用户必须要注册完路由,才能启动 HTTPServer。那么我们就可以采用 panic,因为启动之前就代表应用还没运行。

Q:路由树是线程安全的吗?

A:显然不是线程安全的。

我们要求用户必须要注册完路由才能启动 HTTPServer。而正常的用法都是在启动之前依次注册路由,不存在并发场景。

至于运行期间动态注册路由,没必要支持。这是典型的为了解决 1% 的问题,引入 99% 的代码。

总结

为了尽最大可能方便各位同学能够电脑上进行调试和提交代码,我将我自己的写文章时的代码提交至 Github仓库当中。

如果大家对我所写代码有修改或者优化的话,欢迎大家提交 优化后的代码

最后欢迎大家 Follow Github 作者。

相关文章:

Go实战训练之Web Server 与路由树

Server & 路由树 Server Web 核心 对于一个 Web 框架,至少要提供三个抽象: Server:代表服务器的抽象Context:表示上下文的抽象路由树 Server 从特性上来说,至少要提供三部分功能: 生命周期控制&…...

C#中接口设计相关原则

在C#中,接口(Interface)是一种引用类型,它定义了一个契约,指定了一个类必须实现的成员(属性、方法、事件、索引器)。接口不提供这些成员的实现,只指定成员必须按照特定的方式被实现。…...

Pytorch学习笔记——卷积操作

一、认识卷积操作 卷积操作是一种数学运算,它涉及两个函数:输入函数(通常是图像)和卷积核(也称为滤波器或特征检测器)。卷积核在输入函数上滑动,将核中的每个元素与其覆盖的输入函数区域中的对应…...

探索鸿蒙开发:鸿蒙系统如何引领嵌入式技术革新

嵌入式技术已经成为现代社会不可或缺的一部分。而在这个领域,华为凭借其自主研发的鸿蒙操作系统,正悄然引领着一场技术革新的浪潮。本文将探讨鸿蒙开发的特点、优势以及其对嵌入式技术发展的深远影响。 鸿蒙操作系统的特点 鸿蒙,作为华为推…...

chrome extension插件替换网络请求中的useragent

感觉Chrome商店中的插件不能很好的实现自己想要的效果,那么就来自己动手吧。 本文以百度为例: 一般来说网页请求如下: 当前使用的useragent是User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safar…...

PHP基础【介绍,注释,更改编码,赋值,数据类型】

源码 <?php //单行注释 /* 多行注释 *///通过header()函数发送http头的请求信息用来指定页面的字符集编码 header("Content-type:text/html;Charsetutf-8"); //告诉浏览器&#xff0c;当前页面的内容类型是HTML&#xff0c;并且页面内容使用的是UTF-8编码。//ph…...

ASP.NET小型证券术语解释及翻译系统的设计与开发

摘 要 在系统设计上&#xff0c;综合各种翻译类型网站优缺点&#xff0c;设计出具有任何使用者都可添加术语信息的且只有管理员能够实现术语修改及删除等独特方式的术语查看管理系统。此方式能够使术语量快速增大&#xff0c;并且便于使用者及管理员操作&#xff0c;满足相互…...

硬件知识积累 音频插座的了解,看音频插座的原理图来了解音频插座的引脚。

1. 音频接口 音频插座是一种用于连接音频信号线路的电子元件&#xff0c;常见于音频设备&#xff08;如音响、耳机、话筒等&#xff09;中。它的主要作用是将电子信号转化为声音信号&#xff0c;以满足人们对于音乐、电影、游戏等方面的需求。 根据插头形状的不同&#xff0c;音…...

error LNK2001: 无法解析的外部符号 “__declspec(dllimport) public: __cdecl ......

运行程序时&#xff0c;报如上图所示错误&#xff0c;其中一条是&#xff1a; ReflectionProbe.obj : error LNK2001: 无法解析的外部符号 "__declspec(dllimport) public: __cdecl osg::Object::Object(bool)" (__imp_??0ObjectosgQEAA_NZ) 报这个错误一般是因为…...

邮箱Webhook API发送邮件的性能怎么优化?

邮箱Webhook API发送邮件的步骤&#xff1f;如何用邮箱API发信&#xff1f; 随着业务规模的扩大&#xff0c;如何高效地通过邮箱Webhook API发送邮件&#xff0c;成为了许多企业面临的关键问题。下面&#xff0c;AokSend将探讨一些优化邮箱Webhook API发送邮件性能的方法。 邮…...

并发编程实现

一、并行编程 1、Parallel 类 Parallel类是System.Threading.Tasks命名空间中的一个重要类&#xff0c;它提供数据并行和任务并行的高级抽象。 For和ForEach Parallel类下的For和ForEach对应着普通的循环和遍历(普通的for和foreach)&#xff0c;但执行时会尝试在多个线程上…...

基于EBAZ4205矿板的图像处理:12图像二值化(阈值可调)

基于EBAZ4205矿板的图像处理&#xff1a;12图像二值化(阈值可调) 我的项目是基于EBAZ4205矿板的阈值可调的图像阈值二值化处理&#xff0c;可以通过按键调整二值化的阈值&#xff0c;key1为阈值加1&#xff0c;key4为阈值减1&#xff0c;key2为阈值加10&#xff0c;key5为阈值…...

人大金仓数据库报com.kingbase8.util.KSQLException: 致命错误: 用户 “SYSTEM“ Password 认证失败

com.kingbase8.util.KSQLException: 致命错误: 用户 “SYSTEM” Password 认证失败 解决办法&#xff1a; 问题在于用户权限只不足&#xff0c;相关配置文件在一般在 /data/sys hba.conf,修改IPV4 local connections选项中的改为trust。...

文件加密软件哪个好?文件加密软件排行榜前十名(好用软件推荐)

文件加密软件哪个好&#xff1f;这是许多个人和企业用户在面临数据保护需求时所关心的问题。随着数字化时代的推进&#xff0c;数据安全问题日益凸显&#xff0c;文件加密软件成为了保护数据安全的重要手段。本文将为您介绍当前市场上排名前十的文件加密软件&#xff0c;帮助您…...

Netty的第一个简单Demo实现

目录 说明需求ClientServer写法总结 实现运行 说明 Netty 的一个练习&#xff0c;使用 Netty 连通 服务端 和 客户端&#xff0c;进行基本的通信。 需求 Client 连接服务端成功后&#xff0c;打印连接成功给服务端发送消息HelloServer Server 客户端连接成功后&#xff0…...

K8S 哲学 - 服务发现 services

apiVersion: v1 kind: Service metadata:name: deploy-servicelabels:app: deploy-service spec: ports: - port: 80targetPort: 80name: deploy-service-podselector: app: deploy-podtype: NodePort service 的 endPoint &#xff08;ep&#xff09; 主机端口分配方式 两…...

Springboot工程创建

目录 一、步骤 二、遇到的问题及解决方案 一、步骤 打开idea,点击文件 ->新建 ->新模块 选择Spring Initializr&#xff0c;并设置相关信息。其中组为域名&#xff0c;如果没有公司&#xff0c;可以默认com.example。点击下一步 蓝色方框部分需要去掉&#xff0c;软件包…...

日本站群服务器的优点以及适合该服务器的业务类型?

日本站群服务器的优点以及适合该服务器的业务类型? 日本站群服务器是指位于日本地区的多个网站共享同一台服务器的架构。这种服务器架构有着诸多优点&#xff0c;使其成为许多企业和网站管理员的首选。以下是日本站群服务器的优点以及适合该服务器的业务类型的分析&#xff1…...

堆的应用2——TOPK问题

TOPK问题 TOP-K问题&#xff1a;即求数据结合中前K个最大的元素或者最小的元素&#xff0c;一般情况下数据量都比较大。 比如&#xff1a;专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 情况1——数据量小 对于Top-K问题&#xff0c;能想到的最简单直接的方式就…...

leetcode-5. 最长回文子串

题目描述 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s "babad" 输出&#xff1a;"bab" 解释&#xff1a;"aba"…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

React 第五十五节 Router 中 useAsyncError的使用详解

前言 useAsyncError 是 React Router v6.4 引入的一个钩子&#xff0c;用于处理异步操作&#xff08;如数据加载&#xff09;中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误&#xff1a;捕获在 loader 或 action 中发生的异步错误替…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

条件运算符

C中的三目运算符&#xff08;也称条件运算符&#xff0c;英文&#xff1a;ternary operator&#xff09;是一种简洁的条件选择语句&#xff0c;语法如下&#xff1a; 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true&#xff0c;则整个表达式的结果为“表达式1”…...

跨链模式:多链互操作架构与性能扩展方案

跨链模式&#xff1a;多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈&#xff1a;模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展&#xff08;H2Cross架构&#xff09;&#xff1a; 适配层&#xf…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作

一、上下文切换 即使单核CPU也可以进行多线程执行代码&#xff0c;CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短&#xff0c;所以CPU会不断地切换线程执行&#xff0c;从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

Rapidio门铃消息FIFO溢出机制

关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系&#xff0c;以下是深入解析&#xff1a; 门铃FIFO溢出的本质 在RapidIO系统中&#xff0c;门铃消息FIFO是硬件控制器内部的缓冲区&#xff0c;用于临时存储接收到的门铃消息&#xff08;Doorbell Message&#xff09;。…...