net/http与gin框架的关系分析

要想学好 gin 框架,首先要学习 net/http 服务,而二者的关系又是重中之重。

本文所要做的任务就是将二者"连接" 起来,让读者掌握其中之精髓。

一、Golang HTTP 标准库示例

使用 golang 启动 http 服务非常简单,就是一个标准的 C/S 架构服务,代码:

go 复制代码
package main

import (
	"fmt"
	"net/http"
)

func pingHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, net/http! v2\n")
}
func main() {
	http.HandleFunc("/ping", pingHandler)
	http.ListenAndServe(":8091", nil)
}

这段代码主要完成了两件事:

  1. 通过 http.HandleFunc 方法注册里 处理函数
  2. 启动 指定端口的 http 服务。

那背后隐藏了什么呢,我们主要致力于挖掘出核心的东西:

  • 路径注册、匹配是如何实现的,依托的核心是什么? 关键词:前缀树、暴露接口
  • http 服务的请求路径是怎么样的? 关键词:one-loop 模型

二、Golang HTTP 标准库 原理

2.1 服务注册

首先我们围绕 http.HandleFunc 源码展开:

go 复制代码
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

type ServeMux struct { // 对 Handler 的具体实现,内部通过一个 map 维护了从 path 到 handler 的映射关系.
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {  // 一个 handler 单元,内部包含了请求路径 path + 处理函数 handler 两部分.
    h Handler
    pattern string 
}

可以看到,是通过默认数据 defaultServeMux 实现的,该结构重点包含的方法:ServeHTTP 和 HandleFunc

首先讲解下为什么 ServeHTTP 方法很重要,因为 ServeMux 是对 Handler 的具体实现:

go 复制代码
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

而 Handler 的定义如下:

go 复制代码
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler 是一个 interface,暴露了方法: ServeHTTP,该方法根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应.

这种实现接口方法有什么好处呢,这里我们先留一个悬念,之后我们可以在后面的请求流程中看到,暂且不表。

其次我们来看 HandleFunc 方法,内部会将处理函数 handler 转为实现了 ServeHTTP 方法的 HandlerFunc 类型,将其作为 Handler interface 的实现类注册到 ServeMux 的路由 map 当中.

go 复制代码
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

type HandlerFunc func(ResponseWriter, *Request)

// Handle registers the handler for the given pattern.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	// 将 path 和 handler 包装成一个 muxEntry,以 path 为 key 注册到路由 map ServeMux.m 中
}

2.2 服务启动

http.ListenAndServe 通过调用 net/http 包公开的方法,实现对服务端的一键启动. 内部定义了一个新的 Server 对象,嵌套执行 Server.ListenAndServe 方法:

go 复制代码
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

Server.ListenAndServe 方法中,根据用户传入的端口,申请到一个监听器 listener,继而调用 Server.Serve 方法.

go 复制代码
func (srv *Server) ListenAndServe() error {
		addr := srv.Addr
		ln, err := net.Listen("tcp", addr)
		return srv.Serve(ln)
}

Server.Serve 方法很核心,体现了 http 服务端的运行架构:for + listener.accept 模式:

go 复制代码
func (srv *Server) Serve(l net.Listener) error {
  	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
		for {
			rw, err := l.Accept()
			// ...
			connCtx := ctx
			// ...
			c := srv.newConn(rw)
			// ...
			go c.serve(connCtx)
		}
	}
}

主要实现功能:

  • 将 server 封装成一组 kv 对,添加到 context 当中
  • 开启 for 循环,每轮循环调用 Listener.Accept 方法阻塞等待新连接到达
  • 每有一个连接到达,创建一个 goroutine 异步执行 conn.serve 方法负责处理

其中 conn.serve 是响应客户端连接的核心方法:

go 复制代码
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
		// ...
		c.r = &connReader{conn: c}
		c.bufr = newBufioReader(c.r)
		c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
	
		for {
		    w, err := c.readRequest(ctx)
		    // 核心
				serverHandler{c.server}.ServeHTTP(w, w.req)
		}

可以看下核心的实现:

go 复制代码
type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  handler := sh.srv.Handler
	handler.ServeHTTP(rw, req)
}

在 serveHandler.ServeHTTP 方法中,会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节。

基于接口而非实现,此后开始调用实现的 ServeHTTP 方法,匹配到相应的处理函数后执行:

go 复制代码
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
  h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
	return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
  mux.mu.RLock()
	defer mux.mu.RUnlock()
	
	h, pattern = mux.match(path)
}

三、Gin 框架原理

Gin 是在 Golang HTTP 标准库 net/http 基础之上的再封装,两者的交互边界图。

可以看出,在 net/http 的既定框架下,gin 所做的是提供了一个 gin.Engine 对象作为 Handler 注入其中,从而实现路由注册/匹配、请求处理链路的优化。

我们通过一个 简化版 gin进行学习核心思想,示例代码:

go 复制代码
func testMiddle(c *gin.Context) {
	fmt.Println("middle test")
}

func main() {
	// 构造默认配置的 gin.Engine
	engine := gin.Default()
	// 注册中间件
	engine.Use(testMiddle)
	// 注册方法
	engine.Handle("GET", "/test", func(c *gin.Context) {
		fmt.Println("route test")
	})
	// 启动 http server
	if err := engine.Run(); err != nil {
		fmt.Println(err)
	}
}

主要做了几件事:

  1. 构造默认配置的 gin.Engine
  2. 注册中间件
  3. 注册方法
  4. 启动 http server

gin 是如何与 net/http 链接起来的呢?

  1. 路由注册与查找:gin 的核心结构体 Engine 即实现了该接口:
go 复制代码
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	engine.handleHTTPRequest(c)
}
  1. 服务启动:通过 Engine.Run() 启动 http server 的背后其实是通过 http.ListenAndServe() 启动
go 复制代码
func (engine *Engine) Run(addr ...string) (err error) {
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine.Handler())
	return
}

至此,整个文章已经实现了闭环,更能够学习到连接的核心思想。


参考:

1\]: https://zhuanlan.zhihu.com/p/609258171 Golang HTTP 标准库实现原理 \[2\]: https://astro.yufengbiji.com/posts/golang/ Golang net/http \[3\]: https://zhuanlan.zhihu.com/p/611116090 解析 Gin 框架底层原理 \[4\]: https://blog.csdn.net/weixin_45177370/article/details/135295839?spm=1001.2014.3001.5501 Gin 源码深度解析及实现

相关推荐
Bruce_Liuxiaowei4 小时前
网站敏感文件_目录大全(分类记忆+风险标注)
运维·网络·网络协议·http·网络安全·https
charlee446 小时前
使用cpp-httplib发布HTTP服务
c++·http·json·cpp-httplib
爬山算法8 小时前
Netty(22)如何实现基于Netty的HTTP客户端和服务器?
服务器·网络协议·http
爱吃香蕉的阿豪8 小时前
NET Core中ConcurrentDictionary详解:并发场景下的安全利器及服务端实践
安全·http·.netcore·高并发
吴佳浩 Alben1 天前
Gin 入门指南 Swagger aipfox集成
gin
小阿宁的猫猫1 天前
CSRF漏洞的原理、防御和比赛中的运用
安全·http·xss·csrf
教练、我想打篮球1 天前
120 同样的 url, header, 参数, 使用 OkHttp 能够成功获取数据, 使用 RestTemplate 报错
http·okhttp·resttemplate·accept
zfj3211 天前
websocket为什么需要在tcp连接成功后先发送一个标准的http请求,然后在当前tcp连接上升级协议成websocket
websocket·tcp/ip·http
杀手不太冷!1 天前
Jenkins的安装与使用;git clone url的时候,url为http和ssh时候的区别
git·http·jenkins
irisart1 天前
第二章【NGINX 开源功能】—— HTTP 服务器(下)
nginx·http·开源