1 整体框架
http的交互框架是C-S架构
import (
"net/http"
)
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8091", nil)
}
可以通过http.HandleFunc注册请求路径/ping的handle函数;http.ListenAndServe将监听8091端口,这个端口启动了http服务
发送请求:
func main() {
reqBody, _ := json.Marshal(map[string]string{"key1": "val1", "key2": "val2"})
resp, _ := http.Post(":8091", "application/json", bytes.NewReader(reqBody))
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Printf("resp: %s", respBody)
}
服务端
核心数据结构
(1)Server
type Server struct {
// server 的地址
Addr string
// 路由处理器.
Handler Handler // handler to invoke, http.DefaultServeMux if nil
// ...
}
整个 http 服务端模块被封装在 Server 类当中.
Handler 是 Server 中最核心的成员字段,实现了从请求路径 path 到具体处理函数 handler 的注册和映射能力.
在用户构造 Server 对象时,倘若其中的 Handler 字段未显式声明,则会取 net/http 包下的单例对象 DefaultServeMux(ServerMux 类型) 进行兜底.
(2)Handler
Handler 是一个 interface,定义了方法: ServeHTTP.
该方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
(3)ServeMux
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
(4)muxEntry
muxEntry 为一个 handler 单元,内部包含了请求路径 path + 处理函数 handler 两部分.
type muxEntry struct {
h Handler
pattern string
}
注册handler
实现路由注册的核心逻辑位于 ServeMux.Handle 方法中,两个核心逻辑值得一提:
- 将 path 和 handler 包装成一个 muxEntry,以 path 为 key 注册到路由 map ServeMux.m 中
- 响应模糊匹配机制. 对于以 ‘/’ 结尾的 path,根据 path 长度将 muxEntry 有序插入到数组 ServeMux.es 中.(模糊匹配机制的伏笔在 2.3 小节回收)
启动server
调用 net/http 包下的公开方法 ListenAndServe,可以实现对服务端的一键启动. 内部会声明一个新的 Server 对象,嵌套执行 Server.ListenAndServe 方法.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
Server.ListenAndServe 方法中,根据用户传入的端口,申请到一个监听器 listener,继而调用 Server.Serve 方法.
func (srv *Server) ListenAndServe() error {
// ...
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
// ...
return srv.Serve(ln)
}
Server.Serve 方法很核心,体现了 http 服务端的运行架构:for + listener.accept 模式.
- 将 server 封装成一组 kv 对,添加到 context 当中
- 开启 for 循环,每轮循环调用 Listener.Accept 方法阻塞等待新连接到达
- 每有一个连接到达,创建一个 goroutine 异步执行 conn.serve 方法负责处理
var ServerContextKey = &contextKey{"http-server"}
type contextKey struct {
name string
}
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)
}
}
conn.serve 是响应客户端连接的核心方法:
- 从 conn 中读取到封装到 response 结构体,以及请求参数 http.Request
- 调用 serveHandler.ServeHTTP 方法,根据请求的 path 为其分配 handler
- 通过特定 handler 处理并响应请求
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)
w.cancelCtx()
// ...
}
}
依次调用 ServeMux.ServeHTTP、ServeMux.Handler、ServeMux.handler 等方法,最终在 ServeMux.match 方法中,以 Request 中的 path 为 pattern,在路由字典 Server.m 中匹配 handler,最后调用 handler.ServeHTTP 方法进行请求的处理和响应.
当通过路由字典 Server.m 未命中 handler 时,此时会启动模糊匹配模式,两个核心规则如下:
- 以 ‘/’ 结尾的 pattern 才能被添加到 Server.es 数组中,才有资格参与模糊匹配
- 模糊匹配时,会找到一个与请求路径 path 前缀完全匹配且长度最长的 pattern,其对应的handler 会作为本次请求的处理函数.
客户端
核心数据结构
(1)Client
与 Server 对仗,客户端模块也有一个 Client 类,实现对整个模块的封装:
- Transport:负责 http 通信的核心部分,也是接下来的讨论重点
- Jar:cookie 管理
- Timeout:超时设置
(2)RoundTripper
RoundTripper 是通信模块的 interface,需要实现方法 Roundtrip,即通过传入请求 Request,与服务端交互后获得响应 Response.
(3)Transport
Tranport 是 RoundTripper 的实现类,核心字段包括:
- idleConn:空闲连接 map,实现复用
- DialContext:新连接生成器
(4)Request
http 请求参数结构体.
type Request struct {
// 方法
Method string
// 请求路径
URL *url.URL
// 请求头
Header Header
// 请求参数内容
Body io.ReadCloser
// 服务器主机
Host string
// query 请求参数
Form url.Values
// 响应参数 struct
Response *Response
// 请求链路的上下文
ctx context.Context
// ...
}
(5)Response
http 响应参数结构体.
type Response struct {
// 请求状态,200 为 请求成功
StatusCode int // e.g. 200
// http 协议,如:HTTP/1.0
Proto string // e.g. "HTTP/1.0"
// 请求头
Header Header
// 响应参数内容
Body io.ReadCloser
// 指向请求参数
Request *Request
// ...
}
客户端发起一次 http 请求大致分为几个步骤:
- 构造 http 请求参数,这个过程就会加上cookie之类的,之后会更新cookie(服务器如果有更新)
- 获取用于与服务端交互的 tcp 连接,会先尝试复用相同协议 访问相同服务端的空闲连接,没有则新建连接
- 通过 tcp 连接发送请求参数
- 通过 tcp 连接接收响应结果