目录
[一. 为什么需要 Handler 与 ServerMux](#一. 为什么需要 Handler 与 ServerMux)
[二. 建立连接](#二. 建立连接)
[三. 路由分发](#三. 路由分发)
[1. ServerMux 是什么](#1. ServerMux 是什么)
[2. serverMux底层结构](#2. serverMux底层结构)
[3. Handler 是什么](#3. Handler 是什么)
[四. 业务处理](#四. 业务处理)
[1. r *http.Request ------ 请求(客户端 -> 服务器)](#1. r *http.Request —— 请求(客户端 -> 服务器))
[2. w http.ResponseWriter------ 响应(服务器 → 客户端)](#2. w http.ResponseWriter—— 响应(服务器 → 客户端))
[3. 两者配合的完整流程](#3. 两者配合的完整流程)
在 Golang的Web编程中, http包是最核心的基础标准库, 其中 Handler 和 ServerMux 是整个 Web 请求处理流程的关键抽象, 下面我针对这两个核心功能进行详细解析。
一. 为什么需要 Handler 与 ServerMux
Web 服务要解决的本质问题:
一个 Web 服务器, 本质上需要解决三个问题:
-
监听端口,接收 HTTP 请求
-
根据请求路径找到对应的处理逻辑
-
执行处理逻辑并返回 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()
这一步做了三件关键的事情:
-
创建 TCP Listener
-
进入 Accept 循环
-
为每个连接启动 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请求处理流程:
- 客户端发送请求
- Go HTTP 服务器接收到请求
- 调用根 Handler 的ServerHTTP方法(这里是DefaultServerMux)
- ServerMux根据 URL 路径 /index 查找注册的Handler
- 找到匹配的Handler(比如你注册的函数或结构体)
- 调用该Handler的ServerHTTP方法
- 你的代码执行,写入响应(w.Write(...))
- 响应返回给客户端
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的学习进行一次梳理和总结。