- 原生httpServer服务启动监听原理
- 原生httpServer中的多路复用器及路由注册原理
- 原生httpServer接收请求后的路由发现原理
- 服务启动前的始化容器创建Engine引擎
- 中间件的注册(中间件又分为全局中间件,路由组中间件)
- 路由的注册
- 服务启动
- 接收请求的处理
//注意go服务的启动类package要编写为main
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
//1.创建router,实际返回的是一个Engine引擎,也可以说成初始化容器创建Engine引擎
router := gin.Default()
//router:=gin.New() 了解Default与New的区别
//2.注册全局中间件
router.Use(MiddlewareFunc())
//3.注册路由
router.GET("/test", func(context *gin.Context) {
context.Writer.Write([]byte("返回参数"))
})
//4.注册路由组,Group()方法会返回一个新生成的RouterGroup指针,用来区分不同的路由组与执行对应不同的中间件等
groupRouter := router.Group("/groupPath")
//5.路由组注册中间件
groupRouter.Use(MiddlewareFunc())
{
//6.路由组注册路由
groupRouter.GET("/hello", func(context *gin.Context) {
context.Writer.Write([]byte("返回参数"))
})
}
//7.添加资源路径
router.Static("/static", "dist/static")
//前端接口
//这样只要启动后端代码,访问根目录就直接访问到静态资源了
router.StaticFile("/", "dist/index.html")
//8.监听端口启动服务
router.Run(":8080")
}
//中间件函数
func MiddlewareFunc() gin.HandlerFunc {
return func(c *gin.Context) {
log.Printf("[INFO] %s", "中间件业务执行")
c.Next()
}
}
- 通过调用gin.New() 或Default方法来创建router初始化容器,实际返回的是一个Engine引擎,也可以说成初始化容器创建Engine引擎
- 调用Use()方法注册中间件
- 通过Group()方法返回一个新生成的RouterGroup指针,实现路由分组,并且在执行完Group()后,会复制全局中间件到新生成的RouterGroup.Handlers中,接下来路由注册的时候就可以一起写入树节点中
- 调用GET或POS方法注册路由
- 调用Run方法启动服务
type Engine struct {
//管理路由和中间件的组件,它定义了 URL 路径与处理函数的映射关系。
RouterGroup
//如果当前路径的处理函数不存在,但是路径+'/'的处理函数存在,则允许进行重定向,默认为 true
RedirectTrailingSlash bool
//允许修复当前请求路径,如/FOO和/..//Foo会被修复为/foo,并进行重定向,默认为 false。
RedirectFixedPath bool
HandleMethodNotAllowed bool
ForwardedByClientIP bool
AppEngine bool
//使用未转义的请求路径(url.RawPath),默认为 false
UseRawPath bool
//对请求路径值进行转义(url.Path),默认为 true
UnescapePathValues bool
MaxMultipartMemory int64
//去除额外的反斜杠,默认为 false
RemoveExtraSlash bool
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
//每一个 HTTP 方法会有一颗方法树,方法树记录了路径和路径上的处理函数
trees methodTrees
}
- 创建一个默认的RouterGroup,RouterGroup中Handlers存放的是当前全局中间件, basePath 就是存放这个分组的基础路由路径"/"
- 通过make()函数初始化trees属性,tress负责采用类似字典树的结构存储路由和handle方法的映射
- 通过sync/pool 实现context池,减少频繁context实例化带来的资源消耗
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
//实例化默认的RouterGroup,其中Handlers为中间件数组
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
//trees 是最重要的点!!!!负责存储路由和handle方法的映射,采用类似字典树的结构
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJsonPrefix: "while(1);",
}
engine.RouterGroup.engine = engine
//这里采用 sync/pool 实现context池,减少频繁context实例化带来的资源消耗
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
type RouterGroup struct {
//用来保存当前路由组的函数链
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
- 我们执行gin.New/gin.Default函数初始化容器获取到Engine实例,Engine中存在一个RouterGroup属性,在初始化时会创建一个默认的RouterGroup,这个默认的RouterGropu中的Handlers存放的是当前全局中间件, basePath 就是存放这个分组的基础路由路径"/"(参考gin.New源码)
- 在Engine 实例调用Group()方法会返回一个新的RouterGroup,并且在执行完Group()后,会复制全局中间件到新生成的RouterGroup.Handlers中,接下来路由注册的时候就可以一起写入树节点中
- Engine 中包含一个RouterGroup属性,该属性中保存的是初始化容器时提供的默认路由组
- RouterGroup 中也持有着Engine属性指针,能给更方便的获取上下文相关数据
- Engine 与 RouterGroup 都实现了IRouter 接口,也可以将Engine 看为RouterGroup
var _ IRouter = &RouterGroup{}
var _ IRouter = &Engine{}
type IRouter interface {
IRoutes
//创建一个新的RouterGroup
Group(string, ...HandlerFunc) *RouterGroup
}
type IRoutes interface {
//添加中间件/处理函数即用append方法向HandlerFunc切片添加一个HandlerFunc方法
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
//对所有已知类型调用一遍handle,接收该路由所有类型请求
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
//把本地filepath路径下的单个静态文件当作服务提供给relativePath路由
//包装了go的http.ServeFile函数作为处理器,注册了GET和HEAD路由
StaticFile(string, string) IRoutes
StaticFileFS(string, string, http.FileSystem) IRoutes
//root路径下的文件当作静态目录作为路由提供服务,不能使用通配符
//计算绝对路径
//调用go的http.StripPrefix函数,实现把静态目录映射为路由
//封装http.FileSystem.Open(),Close(),httpServeHTTP()等函数作为处理器
//加上/*filepath后缀(用于处理器中当作路径取文件),注册GET,HEAD类型的路由
Static(string, string) IRoutes
//和Static一样,Static方法实际上是把root字符串通过go的http.Dir()函数
//包装成http.FileSystem对象直接调用StaticFS实现的
StaticFS(string, http.FileSystem) IRoutes
}
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
}
return nil
}
// Context是gin最重要的部分。它允许我们在中间件之间传递变量,
// 管理流程,验证请求的JSON并渲染JSON响应等。
type Context struct {
//响应处理(实现了http.ResponseWriter 和 gin.ResponseWriter)
writermem responseWriter
// Request是一个指向http.Request类型的指针,表示当前的HTTP请求
Request *http.Request
// Writer是一个ResponseWriter类型的接口,表示当前的HTTP响应
Writer ResponseWriter
// Params是一个Params类型的切片,表示URL中的参数
Params Params
// handlers是一个HandlersChain类型的切片,表示注册的中间件和处理函数
handlers HandlersChain
// 表示当前执行到第几个中间件或处理函数
index int8
// 表示当前请求的完整路径
fullPath string
// 个指向Engine类型的指针,表示当前使用的Gin引擎
engine *Engine
// Params类型的指针,表示URL中的参数(与Params字段相同)
params *Params
// 指向skippedNode类型切片的指针,表示跳过的路由节点
skippedNodes *[]skippedNode
// sync.RWMutex类型的变量,用于保护Keys字段
mu sync.RWMutex
//键值对映射表,专门用于每个请求的上下文。
Keys map[string]interface{}
// 错误列表,附加到所有使用这个上下文的处理器/中间件
Errors errorMsgs
// 定义了一个手动接受的格式列表,用于内容协商
Accepted []string
// queryCache是一个url.Values类型的变量,缓存了c.Request.URL.Query()的结果
queryCache url.Values
//一个url.Values类型的变量,缓存了c.Request.PostForm,它包含了从POST, PATCH或PUT请求体中解析出来的表单数据
formCache url.Values
// 一个http.SameSite类型的变量,允许服务器定义一个cookie属性,使得浏览器不能在跨站请求中发送这个cookie
sameSite http.SameSite
}
- 在Context中维护了一个调用链HandlersChain,可以链式执行Handler
- 提供了一个Keys属性,可以理解为一个上下文级别的缓存有对应的get,set方法
- 提供了一个Params属性,可以获取路由(路径)变量
- 提供了一个Query属性,可以获取url传参
- 提供了大量的Bind方法,基于提供的Bind方法可以很方便的解析获取例如json, xml, protobuf, form, query, yaml各种个样的数据格式, 提高我们的开发速度
//Abort源码,意思是将index移动末尾,所以原来后面的就不再执行
func (c *Context) Abort() {
c.index = abortIndex
}
- Engine 实现了 net/http.Handler 下的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法,本身是一个Handler
- 在当前这个Run()方法中会调用 net/http下的ListenAndServe()函数,这里就来到了net/http标准库,可以参考前面做的笔记说一下net/http
HttpServer 多路复用器及路由注册原理
HttpServer 服务启动到Accept等待接收连接
HttpServer 服务启动接到连接后的处理逻辑
多路复用 netpoller 初始化
多路复用 Accept/Read/Write
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
- 根据配置参数决定是否使用编码后的 URL 路径,以及去除多余的反斜杠。
- 根据 HTTP 请求方法找到对应的方法树,若找到对应的方法树,从方法树中获取路由信息,并把处理函数,参数路径信息记录到 Context 上,调用 Context 的 Next 方法开始执行调用链上的函数。若方法树中不存在路由信息,则判断路径+'/'的路由定义是否存在,并尝试进行重定向。
- 如果没有找到对应路由信息,根据配置参数返回 HTTP 404 (NOT FOUND) 或 405 (METHOD NOT ALLOWED) 错误
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
if engine.RemoveExtraSlash {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
- 服务启动前的始化容器创建Engine引擎
- 中间件的注册(中间件又分为全局中间件,路由组中间件)
- 路由的注册
- 服务启动
- 接收请求的处理
- 实例化默认的RouterGroup,RouterGroup内部有一个Handlers属性,是一个用来存储中间件的数组,
- 调用make函数初始化trees属性,这个属性内部采用类似字典树的结构存储了路由和handle方法的映射
- 调用allocateContext()方法,采用 sync/pool 的方式初始化创建gin.Context
- 最终Engine创建完成并返回
- 调用 resolveAddress 解析传入的地址,若没有传入地址,则默认使用 PORT 环境变量作为端口号,在此端口上运行 HTTP 服务
- Engine 实现了 net/http.Handler 下的 ServeHTTP()本身是一个Handler,
- 在当前这个Run()方法中会调用 net/http下的ListenAndServe()函数,这里就来到了net/http启动服务
- 继续了解net/http下的ListenAndServe(),内部会调用
4.1: " net.Listen(“tcp”, addr)“: 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
4.2: “srv.Serve(ln)””: 等待接收客户端连接Accept(),与接收到连接后的处理流程
- 执行"DefaultResolver.resolveAddrList"根据协议名称和地址取得 Internet 协议族地址列表,
- 根据协议执行listenTCP或listenUnix 进行实际socket创建,fd创建,监听等操作, 具体根据传入的协议族来确定
- 会初始化多路复用epoll相关业务,以TCP为例,通过listenTCP()函数最终会调用到net/sock_posix.go下的一个socket()
- 最终会调用newFD(),FD.Init(),pollDesc.init(),封装一个epoll文件描述符实例epollevent
- 执行poll_runtime_pollOpen: 调用alloc()初始化总大小约为 4KB的pollDesc结构体,调用netpollopen(),将可读,可写,对端断开,边缘触发 的监听事件注册到epollevent中
- 具体参考前面做的笔记netpoller 初始化
- 方法内通过for开启了一个死循环
- 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
- 当接收到连接请求Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
- 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
- 当接收到一个请求后for循环继续执行等待下一个连接