当前位置: 首页 > 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"…...

【Flask 系统教程 1】入门及配置

当你开始学习 Flask 时&#xff0c;了解如何进行基本的配置是非常重要的。Flask 是一个简单而灵活的 Python Web 框架&#xff0c;它允许你快速构建 Web 应用程序&#xff0c;并且易于学习。在这篇博客中&#xff0c;我将介绍如何从零开始进行 Flask 的基础配置&#xff0c;适合…...

石家庄河北银行的

有些时候河北石家庄这边的甲方客户人员就是太苛刻了&#xff0c;尤其是银行业 比如河北银行的信息部的卢斌&#xff0c;兰州人&#xff0c;这个人的人品极度恶劣&#xff0c;对乙方的外包人员特别苛刻&#xff0c;像个大爷一样。自己什么都不会&#xff0c;连sql 都不会写&…...

【CCNP ENCOR OCG】CHAPTER 2》Spanning Tree Protocol

目录 “Do I Know This Already?” Quiz Foundation Topics Spanning Tree Protocol Fundamentals 802.1D Port States Spanning Tree Path Cost Root Bridge Election Locating Root Ports Locating Blocked Designated Switch Ports Verification of VLANs on Trun…...

docker无法映射/挂载根目录

docker无法映射&#xff08;挂载&#xff09;根目录下的文件夹只能映射家目录 最近想要使用nas-tools做做刮削&#xff0c;电影存在一个机械磁盘里&#xff0c;机械磁盘被挂载到/data1下&#xff0c;发现一个很奇怪的问题&#xff0c;docker只能挂载成功home目录下的文件夹&am…...

C++中不要重新定义继承而来的non-virtual函数

在 C 中&#xff0c;重定义继承而来的 non-virtual&#xff08;非虚&#xff09;函数通常是不推荐的&#xff0c;原因如下&#xff1a; 隐藏父类的实现&#xff1a;如果在派生类中重定义了一个非虚函数&#xff0c;这将隐藏父类中具有相同名称和参数的函数。这意味着即使通过基…...

C++ 对象型参数和返回值

对象型参数和返回值 1.对象型类型作为函数的参数2.对象型参数作为函数的返回值 1.对象型类型作为函数的参数 使用对象类型作为函数的参数或者返回值&#xff0c;可能会产生一些不必要的中间对象 例子&#xff1a; // 使用对象类型作为函数的参数 void test1(Car car) {}完整代…...

LeetCode 字符串专题——KMP算法_28. 找出字符串中第一个匹配项的下标

字符串专题——KMP算法 KMP算法例题 KMP算法 待更新 例题 https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/description/ class Solution {vector<int> next;void getNext(string s){int j-1;next[0]-1;int lens.size();for(int i…...

上班不想用脑子写代码了怎么办?那就试试Baidu Comate啊宝贝

本文目录 前言1、视频编程实战1.1、熟悉代码库中的代码1.2、参考现有代码编写新代码 2、下载使用教程3、使用体验3.1、AutoWork 产品测评3.2、解决有关ajax请求后重定向问题3.3、询问编程相关知识3.3.1、cookie和session的区别与联系3.3.2、数据库中主键外键的相关知识 4、问题…...

【管理咨询宝藏94】某国际咨询公司供应链财务数字化转型方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏94】某国际咨询公司供应链&财务数字化转型方案 【格式】PDF版本 【关键词】国际咨询公司、制造型企业转型、数字化转型 【核心观点】 - 172…...

C++_使用邻接表(链表-指针)实现有向图[完整示例及解释]

这个程序是一个图的实现&#xff0c;使用邻接表来表示图的结构&#xff1a; 1. 结构定义部分&#xff1a; - AdjListNode 结构定义了邻接表中的节点&#xff0c;每个节点包含一个名称和一个指向下一个邻接节点的指针。 - Node 结构定义了图中的节点&#xff0c;每个节点…...

网站制作书籍推荐/百度关键词优化软件排名

bzoj3944:Sum 裸的杜教筛 bzoj4916:神犇和蒟蒻 按照筛phi的方式稍微推一下就好 bzoj3512:DZY Loves Math IV 我打了一个神奇的(杜教筛记忆化搜索(搜索里面有两层根号的找约数,总体上是在利用mu来容斥)),这种方法虽然不优秀但是也能过,然而正解是(杜教筛记忆化搜索(充分利用phi函…...

做服装最好的网站建设/新东方英语培训机构官网

2019独角兽企业重金招聘Python工程师标准>>> ​ 简介 红帽在2019年5月7号红帽正式发布了rhel8&#xff0c;在发布rhel8之前红帽还换了logo&#xff0c;从要脸变成了不要脸&#xff0c;这么重大的改变&#xff0c;作为一个运维自然要关注一下 下载 下载地址如下 http…...

国外专卖模板的网站/公司网络优化方案

本文实例讲述了PHP使用redis实现统计缓存mysql压力的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;<?php header("Content-Type:text/html;charsetutf-8");include lib/mysql.class.php;$mysql_obj mysql :: getConn();// redis$redis new Redi…...

wordpress打开前台页面空白/seo顾问服务深圳

Vivado时序约束中Tcl命令的对象及属性在前面的章节中&#xff0c;我们用了很多Tcl的指令&#xff0c;但有些指令并没有把所有的参数多列出来解释&#xff0c;这一节&#xff0c;我们就把约束中的Tcl指令详细讲一下。我们前面讲到过get_pins和get_ports的区别&#xff0c;而且我…...

电子商务网站建设收获/湖南网站建设工作室

原文网址&#xff1a;http://blog.sina.com.cn/s/blog_5bdee3020101pvgy.html 市面上&#xff0c;经常充斥着&#xff0c;大量双卡双待手机&#xff0c;而许多消费者只是知道这种手机可以插入两个卡&#xff0c;直接打电话&#xff0c;而你真正的理解其中的含义吗&#xff1f; …...

wordpress 缺少父主题/搜索自媒体平台

NEW关注Tech逆向思维视频号最新视频→【最骇人的5起自动驾驶“杀人”事故】来源&#xff5c;机器人大讲堂文&#xff5c;Robospeak十万块多米诺骨牌倒下是个啥场面&#xff1f;等等&#xff0c;十万块&#xff1f;那得搭多久啊&#xff1f;这工程量可是相当巨大了&#xff0c;普…...