Go HTTP 编程 | 02 - net/http 包剖析

简介: Go HTTP 编程 | 02 - net/http 包剖析

一、net/http 包详解

在上一篇文章中我们已经使用 net/http(以下简称 http) 创建了一个 Web 服务,并从源码层面分析了整个请求流转的过程,其中有两个比较核心的组件或者功能,一个是连接 Conn,另外一个是 ServeMux

Conn

http 包中使用 goroutine 来处理连接的读写,这样既可以使每个请求保持独立,请求相互之间不会影响,不会阻塞,可以高效的响应网络请求,有可以实现高并发和高性能。Go 在等待客户端请求的代码如下:

c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
复制代码

go c.serve(connCtx) 会为每一个请求创建一个 Conn,这个 Conn 中保存了该次请求的信息,然后在通过 serve 函数传递到对应的 handler,该 handler 中可以读取到相应请求的 header 信息,保证了每个请求的独立性。

ServeMux

在创建 Web 服务器的时候,我们通过 ListenAndServe 函数的第二个参数传递了一个 handler,这个 handlernil,在 ServeHTTP 函数中如果 handlernil,会给这个 handler 赋值一个 DefaultServeMux

err := http.ListenAndServe(":9000", nil)
复制代码
handler := sh.srv.Handler
if handler == nil {
   handler = DefaultServeMux
}
复制代码

DefaultServeMux 就是一个 ServeMux

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
复制代码

ServeMux 结构体的定义如下:

type ServeMux struct {
   mu    sync.RWMutex // 锁,请求涉及到并发处理,这里需要一个锁的机制
   m     map[string]muxEntry // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达方式
   es    []muxEntry // slice of entries sorted from longest to shortest.
   hosts bool       // whether any patterns contain hostnames
}
复制代码

muxEntry 结构体的定义如下:

type muxEntry struct {
   h       Handler // 路由对应的 handler
   pattern string 
}
复制代码

Handler 结构体的定义如下:

type Handler interface {
   ServeHTTP(ResponseWriter, *Request) // 路由实现器
}
复制代码

Handler 是一个接口,该接口中定义了一个 ServeHTTP 方法,查看 HandleFun

image.png

HandleFun 类型定义如下:

type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
   f(w, r)
}
复制代码

我们在创建 Web 服务器的时候通过 HandleFun 函数将路由和处理函数进行了绑定:

http.HandleFunc("/", sayHelloName)
复制代码

我们定义的方法 sayHelloName 虽然没有直接实现 ServeHTTP 方法,但是 sayHelloName 方法就是 HandleFunc 调用后的结果,这个类型默认实现了 ServeHTTP 方法,即调用了 HandleFunc(f) 强制类型转换 fHandleFunc 类型,这样就拥有了 ServeHTTP 方法。

路由器里面存储好了相应的路由映射规则后,通过调用 mux.handler(r).ServeHTTP(w,r) 来分发请求,mux.handler(r) 方法的源代码如下:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
   mux.mu.RLock()
   defer mux.mu.RUnlock()
   // Host-specific pattern takes precedence over generic ones
   if mux.hosts {
      h, pattern = mux.match(host + path)
   }
   if h == nil {
      h, pattern = mux.match(path)
   }
   if h == nil {
      h, pattern = NotFoundHandler(), ""
   }
   return
}
复制代码

handler 函数是根据用户请求的 URL 和路由器里面存储的 map 匹配获取存储的 handler,获取之后就可以调用 handlerServeHTTP 方法执行相应的函数了。

Go 支持外部实现路由器,也就是 ListenerAndServe 方法的第二个参数,它是一个 Handler 接口,也就是实现 Handler 接口就可以实现自定义路由器。

在自定义的 Web 服务器的 main.go 文件中增加自定义路由器的代码:

//noinspection ALL
func main(){
   //http.HandleFunc("/", sayHelloName)
   alphaMux := &AlphaMux{}
   err := http.ListenAndServe(":9000", alphaMux)
   if err != nil {
      log.Fatal("ListenAndServer: ", err)
   }
}
func sayHelloName(w http.ResponseWriter, r *http.Request){
   fmt.Fprint(w, "Hello, AlphaMux")
}
// 自定义路由器结果体
type AlphaMux struct {
}
// 自定义路由规则
func (p *AlphaMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   if r.URL.Path == "/" {
      sayHelloName(w, r)
      return
   }
   http.NotFound(w, r)
   return
}
复制代码

执行 main.go 文件,在浏览器中输入 localhost:9000,浏览器显示内容如下:

image.png

自定义的路由器生效。

二、Go 代码的执行流程

在使用默认的 DefaultServeMux 作为路由器的代码中,来看一下代码的执行流程:

Go 首先会调用 http.HandleFunc,这一行代码内部执行了以下几步:

第一步:调用了 DefaultServeMuxHandleFunc 方法;

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

第二步:调用了 DefaultServeMuxHandle 方法:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
   if handler == nil {
      panic("http: nil handler")
   }
   mux.Handle(pattern, HandlerFunc(handler))
}
复制代码

第三步:通过 Handler 方法,往 DefaultServeMuxmap[string]muxEntry 中增加对应的 handler 和路由规划:

image.png

接着调用了 http.ListenAndServe("9000", nil),这行代码内部执行了以下几件事:

第一:实例化一个 Server,并调用 ServerListenAndServe() 方法

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

第二:ListenAndServe() 方法中通过 net.Listen 监听传递的端口,在 ListenAndServe() 方法最后调用了 Serve() 方法:

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

第三:Serve 方法中通过 for() 循环接收请求,然后在该方法末尾实例化了一个新的连接 Conn,并开启一个新的 goroutine 为这个请求进行服务 go serve(connCtx)

func (srv *Server) Serve(l net.Listener) error {
   // 其他代码省略
   for {
      rw, err := l.Accept()
      // 其他代码省略
      c := srv.newConn(rw)
      c.setState(c.rwc, StateNew, runHooks) // before Serve can return
      go c.serve(connCtx)
   }
}
复制代码

第四:serve() 函数中通过 for 循环读取请求,并给每个请求分发一个 handler,分发 handler 是通过 ServeHTTP() 方法完成的:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
   // 其余代码省略
   // 通过 for 循环读取请求
   for {
      w, err := c.readRequest(ctx)
      // 其余代码省略    
      // 获取 handler
      serverHandler{c.server}.ServeHTTP(w, w.req)
      inFlightResponse = nil
      // 其余代码省略
   }
}
复制代码

第五:ServeHTTP 方法会分配 handler,我们在 main.go 文件中的 ListenAndServe 方法传递的第二个参数为 nil,这里就分配了一个 DefaultServeMux,并在最后调用了 DefaultServeMuxServeHTTP 方法:

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
   handler := sh.srv.Handler
   if handler == nil {
      handler = DefaultServeMux
   }
   // 其余代码省略
   handler.ServeHTTP(rw, req)
}
复制代码

第六:DefaultServeMux 其实就是一个 ServeMuxServeMuxServeHTTP 方法会根据请求匹配相应的 handler

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
   if r.RequestURI == "*" {
      if r.ProtoAtLeast(1, 1) {
         w.Header().Set("Connection", "close")
      }
      w.WriteHeader(StatusBadRequest)
      return
   }
   h, _ := mux.Handler(r)
   h.ServeHTTP(w, r)
}
复制代码

第七:查看 Handler() 方法的源码,是通过 mux.handler() 方法来选择 handlerhandler 的选择会有三种情况:

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
   mux.mu.RLock()
   defer mux.mu.RUnlock()
   // Host-specific pattern takes precedence over generic ones
   if mux.hosts {
      h, pattern = mux.match(host + path)
   }
   if h == nil {
      h, pattern = mux.match(path)
   }
   if h == nil {
      h, pattern = NotFoundHandler(), ""
   }
   return
}
复制代码

第八:最后调用 handlerServeHTTP 方法就实现了响应客户端:

image.png


相关文章
|
18天前
|
开发框架 .NET 测试技术
了解 .NET 9 中的新 Microsoft.AspNetCore.OpenApi 包,并将其与 NSwag 和 Swashbuckle.AspNetCore 进行比较。
本文介绍了 `.NET 9` 中新推出的 `Microsoft.AspNetCore.OpenApi` 包,该包旨在为 `ASP.NET Core` 应用程序生成 `OpenAPI` 文档。文章对比了 `NSwag` 和 `Swashbuckle.AspNetCore` 两大现有库,探讨了新包的优势和不足,特别是在性能和功能方面。尽管新包在某些方面尚不及成熟库完善,但其对原生 `AoT` 编译的支持和未来的扩展潜力使其成为一个值得考虑的选择。文章还提供了详细的性能测试数据和优化建议,适合对 `OpenAPI` 文档生成感兴趣的开发者阅读。
42 3
了解 .NET 9 中的新 Microsoft.AspNetCore.OpenApi 包,并将其与 NSwag 和 Swashbuckle.AspNetCore 进行比较。
|
7天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
32 13
|
1月前
|
网络协议 JavaScript 前端开发
【HTTP】HTTP报文格式和抓包
【HTTP】HTTP报文格式和抓包
36 0
http数据包抓包解析
http数据包抓包解析
http数据包抓包解析课程笔记
http数据包抓包解析课程笔记
|
3月前
|
Web App开发 存储
常见抓包工具配置抓取HTTPS
常见抓包工具配置抓取HTTPS
|
3月前
|
机器学习/深度学习 Ubuntu Linux
在Linux中,如何按照该要求抓包:只过滤出访问http服务的,目标ip为192.168.0.111,一共抓1000个包,并且保存到1.cap文件中?
在Linux中,如何按照该要求抓包:只过滤出访问http服务的,目标ip为192.168.0.111,一共抓1000个包,并且保存到1.cap文件中?
|
4月前
|
JSON 网络协议 安全
《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(1)-HTTP和HTTPS基础知识
【7月更文挑战第16天】本文介绍了HTTP和HTTPS协议的基本概念与作用,强调了理解HTTP协议对使用抓包工具Fiddler的重要性。HTTP是用于Web浏览器与服务器间信息传输的协议,不加密,易被截取,不适合传输敏感信息。HTTPS是HTTP的安全版,通过SSL/TLS提供加密和服务器身份验证,确保数据安全。HTTP请求包括请求行、请求头、空行和可选的请求主体,响应则有响应行、响应头、空行和响应主体。HTTP协议无状态,而HTTPS解决了安全性问题,但也带来了额外的计算开销。Fiddler作为一个强大的抓包工具,可以帮助开发者和测试人员分析HTTP/HTTPS通信,理解请求和响应的结构。
74 4
《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(1)-HTTP和HTTPS基础知识
|
4月前
|
Web App开发 XML 缓存
《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(4)-会话面板和HTTP会话数据操作详解
【7月更文挑战第19天】Fiddler会话面板概览:Fiddler的会话列表显示HTTP请求的详细信息,包括ID、状态码、协议、主机名、URL、内容类型、大小、进程及自定义备注。颜色和图标标识状态,如红色表示错误,黄色为认证,蓝色是HTML响应。用户可右键列进行搜索、标记重复、隐藏或自定义列,如添加请求方法。通过界面或脚本可添加自定义列,如显示ServerIP。会话还可复制和保存,解决乱码问题需解码响应体。
69 0
《吐血整理》保姆级系列教程-玩转Fiddler抓包教程(4)-会话面板和HTTP会话数据操作详解
|
3月前
|
网络协议 Go
go的net/http有哪些值得关注的细节?
go的net/http有哪些值得关注的细节?