Golang web工作原理详解

目录

[一. 为什么需要 Handler 与 ServerMux](#一. 为什么需要 Handler 与 ServerMux)

[二. 建立连接](#二. 建立连接)

[三. 路由分发](#三. 路由分发)

[1. ServerMux 是什么](#1. ServerMux 是什么)

[2. serverMux底层结构](#2. serverMux底层结构)

[3. Handler 是什么](#3. Handler 是什么)

4.自定义根处理器

[四. 业务处理](#四. 业务处理)

[1. r *http.Request ------ 请求(客户端 -> 服务器)](#1. r *http.Request —— 请求(客户端 -> 服务器))

[2. w http.ResponseWriter------ 响应(服务器 → 客户端)](#2. w http.ResponseWriter—— 响应(服务器 → 客户端))

[3. 两者配合的完整流程](#3. 两者配合的完整流程)


在 Golang的Web编程中, http包是最核心的基础标准库, 其中 HandlerServerMux 是整个 Web 请求处理流程的关键抽象, 下面我针对这两个核心功能进行详细解析。

一. 为什么需要 Handler 与 ServerMux

Web 服务要解决的本质问题:

一个 Web 服务器, 本质上需要解决三个问题:

  1. 监听端口,接收 HTTP 请求

  2. 根据请求路径找到对应的处理逻辑

  3. 执行处理逻辑并返回 HTTP 响应

在 golang 中,这三个问题被拆分为:

问题 golang中的抽象
接收请求 http.Server
路由分发 ServerMux
业务处理 Handler

其中Handler 负责做什么,ServerMux 负责找谁做

二. 建立连接

接收请求主要是使用http.Server来创建, 与我们一般编写的 http 服务器不同,go 为了实现高并发和高性能,使用了 goroutines 来处理 Conn 的读写事件,这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是 go 高效的保证。

go 在等待客户端请求里面是这样写的:

Go 复制代码
c, err := srv.newConn(rw)
if err != nil {
    continue
}
go c.serve()

这里我们可以看到客户端的每次请求都会创建一个 Conn,这个 Conn 里面保存了该次请求的信息,然后再传递到对应的 handler,该 handler 中便可以读取到相应的 header 信息,这样保证了每个请求的独立性。

而go是如何让 Web 运行起来的呢也就是说

  • 如何监听端口?
  • 如何接收客户端请求?
  • 如何分配 handler?

go 是通过一个函数 ListenAndServe 来处理这些事情的,这个底层其实这样处理的:初始化一个 server 对象,然后调用了 net.Listen("tcp", addr),也就是底层用 TCP 协议搭建了一个服务,然后监控我们设置的端口。

对于ListenAndServe()是一个阻塞函数, 启动 HTTP 服务器并持续监听请求, 直到程序被中断

Go 复制代码
http.ListenAndServe(":8080", nil)

实际上等价于:

Go 复制代码
server := &http.Server{
    Addr:    ":8080",
    Handler: http.DefaultServeMux,
}
server.ListenAndServe()

这一步做了三件关键的事情:

  1. 创建 TCP Listener

  2. 进入 Accept 循环

  3. 为每个连接启动 goroutine

这样等待客户端连接就算建立TCP连接了。

三. 路由分发

1. ServerMux 是什么

ServerMux是go标准库提供的 HTTP 请求多路复用器, 它的作用就是根据请求路径, 选择一个合适的 Handler 来处理请求。

2. serverMux底层结构

Go 复制代码
type ServeMux struct {
    mu sync.RWMutex     // 锁,由于请求涉及到并发处理,因此这里需要一个锁机制
    m  map[string]muxEntry  // 路由规则,一个 string 对应一个 mux 实体,这里的 string 就是注册的路由表达式
    hosts bool         // 是否在任意的规则中带有 host 信息
}


type muxEntry struct {
    explicit bool    // 是否精确匹配
    h        Handler // 这个路由表达式对应哪个 handler
    pattern  string  // 匹配字符串
}


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

muxEntry就是 ServeMux 的路由项。你注册的每一个路由规则,如:

Go 复制代码
http.HandleFunc("/login", Login)
http.HandleFunc("/", Index)

都会被放入:

Go 复制代码
map[string]muxEntry {
    "/": {explicit: true, h: Index},
    "/login": {explicit: true, h: Login},
}

这个 map 就是存放路径来要告诉 ServeMux:访问某个路径时,应该调用哪一个 handler。

3. Handler 是什么

在 go 中,Handler 是一个接口:

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

Handler是一个接口:也就是一个能处理 HTTP 请求的对象

只要一个类型实现了ServerHTTP方法,它就是一个 Handler,

所有 HTTP 请求最终都通过调用某个实现了Handler接口的类型的ServerHTTP方法来处理。

像我们平时写的

Go 复制代码
func login(w http.ResponseWriter, r *http.Request) {}

这是普通函数, 它是如何变成Handler呢?

其实底层关系如下

Go 复制代码
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

也就是说:

  • HandlerFunc是一个函数类型, 但它实现了Handler接口
  • 所以HandlerFunc(pattern, fn)内部实际上是调用了Handle(pattern,HandlerFunc(fn)

换句话说就是: HandlerFunc是Handle的便捷封装,专为函数风格设计。

http.DefaultServeMux是一个全局默认的ServerMux实例

当你调用http.handler()或http.handleFunc()时, 实际上是向DefaultServeMux注册路由

而ServerMux本身也实现了Handler接口(因为它有ServerHTTP方法), 所以它可以作为根处理器传给服务器

Handler 的职责非常清晰:

  • 读取请求数据

  • 执行业务逻辑

  • 写入响应数据

它不关心:

  • 端口监听

  • 路由匹配

  • 并发调度

  • TCP 连接管理

示例:

Go 复制代码
func Index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello golang Web"))
}
func main(){
    http.HandleFunc("/index", Index)
    http.ListenAndServe(":8080", nil)
}

实际上, Index被适配成了一个 Handler。

当请求到来时:

bash 复制代码
请求路径 /index
    ↓
ServerMux 查找最匹配的路径
    ↓
返回对应的 Handler
    ↓
调用 Handler.ServeHTTP

Handler 的一次调用,只服务于一次 HTTP 请求

完整的HTTP请求处理流程:

  1. 客户端发送请求
  2. Go HTTP 服务器接收到请求
  3. 调用根 Handler 的ServerHTTP方法(这里是DefaultServerMux)
  4. ServerMux根据 URL 路径 /index 查找注册的Handler
  5. 找到匹配的Handler(比如你注册的函数或结构体)
  6. 调用该Handler的ServerHTTP方法
  7. 你的代码执行,写入响应(w.Write(...))
  8. 响应返回给客户端

4.自定义根处理器

其实我们可以完全不用ServerMux, 因为 http.ListenAndServe()的第二个参数就是Handler, 可以传入任意实现ServerHTTP的对象, 包括自定义路由器。

Go 复制代码
type MyRouter struct{}

func (mr *MyRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/users" {
        w.Write([]byte("用户列表..."))
    } else {
        http.NotFound(w, r)
    }
}

// 启动
http.ListenAndServe(":8080", &MyRouter{})

四. 业务处理

流程: 读取请求 → 执行业务逻辑 → 写入响应

当读取请求之后就可以在Handler函数里面写入业务逻辑了

针对这样一个函数:

Go 复制代码
func Index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello golang Web"))
}

这两个参数对应的功能分别是:

  • w http.ResponseWriter(接口):用于写入 HTTP 向客户端发送数据响应,你写回给浏览器的所有内容

  • r *http.Request(结构体):表示当前收到的客户端 HTTP 完整请求,浏览器发过来的所有信息

1. r *http.Request ------ 请求(客户端 -> 服务器)

Go 复制代码
type Request struct {
    Method string        // GET / POST / PUT ...
    URL    *url.URL      // 请求的 URL
    Header Header        // 请求头
    Body   io.ReadCloser // 请求体
    ...
}

这是浏览器这次请求的全家桶, 我们能从 r 中获得:

(1)请求方式

Go 复制代码
r.Method
  • GET
  • POST
  • PUT
  • DELETE

常用于判断逻辑

(2)URL 和路径

Go 复制代码
r.URL.Path      // /index
r.URL.RawQuery  // a=1&b=2

例如:

bash 复制代码
http://localhost:8080/index?a=1&b=2

(3)查询参数(GET 参数)

bash 复制代码
name := r.URL.Query().Get("name")

(4)请求头 Header

bash 复制代码
r.Header.Get("User-Agent")
r.Header.Get("Content-Type")

请求头包含了浏览器信息、JSON、表单类型等信息

(5)请求体 Body(POST / PUT)

bash 复制代码
r.Body
  • 表单数据

  • JSON

  • 文件上传

注意:

  • Body只能读一次

  • 本质上是一个流

2. w http.ResponseWriter------ 响应(服务器 → 客户端)

Go 复制代码
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

这是服务器响应的工具, 其中w可以:

(1)写响应体(最常用)

Go 复制代码
w.Write([]byte("Hello go web"))

也就是浏览器看到的内容

(2)写状态码

Go 复制代码
w.WriteHeader(http.StatusOK)        // 200
w.WriteHeader(http.StatusNotFound)  // 404

注意:

  • 只能写一次

  • 如果不写, 默认 200

(3)设置响应头

Go 复制代码
w.Header().Set("Content-Type", "text/html")

其必须在Write( )之前设置

完整示例

Go 复制代码
func Index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello Go Web"))
}

3. 两者配合的完整流程

Go 复制代码
浏览器
   ↓
Request (r)
   ↓
Index(w, r)
   ↓
ResponseWriter (w)
   ↓
浏览器显示结果

这时有人要问为什么 r 是指针, 而 w 不是?

r *http. Request是指针

  • Request 结构体很大

  • 避免拷贝

  • handler 可能修改它的一些字段

w http ResponseWriter是接口

  • 屏蔽底层实现

  • 可以是:

    • HTTP/1.1

    • HTTP/2

    • 测试用 mock

这也是 golang 的经典设计:接口 + 组合

以上但不限于这些内容在相关流行框架中都会进行进一步封装, 便于我们更好开发使用, 本文只是针对golang web的学习进行一次梳理和总结。

相关推荐
程序员:钧念2 小时前
【sh脚本与Python脚本的区别】
开发语言·人工智能·python·机器学习·语言模型·自然语言处理·transformer
青衫码上行2 小时前
SpringBoot多环境配置
java·spring boot·后端·学习
爬山算法2 小时前
Hibernate(54)Hibernate中的批量更新如何实现?
java·后端·hibernate
敲敲了个代码2 小时前
前端指纹技术是如何实现的?(Canvas、Audio、硬件API 核心原理解密)
前端·javascript·学习·算法·面试·web
Pth_you2 小时前
Python权限问题终极解决方案
开发语言·python
Ulyanov2 小时前
PyVista战场可视化实战(三):雷达与目标轨迹可视化
开发语言·人工智能·python·机器学习·系统架构·tkinter·gui开发
rfidunion2 小时前
springboot+VUE+部署(9。安装MySql)
spring boot·后端·mysql
张张努力变强2 小时前
C++ 类和对象(二):实例化、this指针、构造函数、析构函数详解
开发语言·c++
gaize12132 小时前
云计算服务和云解决方案-阿里云
开发语言·php