前言
net/http 库的客户端实现(上)
net/http 库的客户端实现(下)
net/http 库的服务端实现
上两篇文章介绍了 http 客户端的实现,这篇文章看一下服务端的实现
服务端
使用 net/http
库可以快速搭建HTTP
服务,HTTP
服务端主要包含两部分:
- 注册处理器:
net/http.HandleFunc
函数用于注册处理器 - 监听端口:
net/http.ListenAndServe
用于处理请求
package mainimport ("fmt""net/http"
)func hello(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}func main() {http.HandleFunc("/hello", hello)http.ListenAndServe(":8080", nil)
}
注册处理器
直接调用net/http.HandleFunc
可以注册路由和处理函数:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}
我们调用http.HandleFunc("/hello", hello)
注册路径处理函数,这里将路径/hello
的处理函数设置为hello
。处理函数的类型必须是:
func (http.ResponseWriter, *http.Request)
它调用HTTP
服务起的DefaultServeMux
处理请求,DefaultServeMux
本质是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
}
**mu**
:需要加读写锁保证并发安全,注册处理器时会加写锁保证写map
的数据正确性,这个map
就是pattern
和handler
;**m**
:存储路由规则,key
就是pattern
,value
是muEntry
实体,muEntry
实体中包含:pattern
和handler
**es**
:存储的也是muxEntry
实体,因为我们使用map
存储路由和handler
的对应关系,所以只能索引静态路由,并不支持[path_param]
,所以这块的作用是当在map
中没有找到匹配的路由时,会遍历这个切片进行前缀匹配,这个切片按照路由长度进行排序;**hosts**
:这个也是用来应对特殊case
,如果我们注册的路由没有以/
开始,那么就认为我们注册的路由包含host
,所以路由匹配时需要加上host
;
func (mux *ServeMux) Handle(pattern string, handler Handler) {// 加锁,保证并发安全mux.mu.Lock()defer mux.mu.Unlock()if pattern == "" {panic("http: invalid pattern")}if handler == nil {panic("http: nil handler")}if _, exist := mux.m[pattern]; exist {panic("http: multiple registrations for " + pattern)}if mux.m == nil {mux.m = make(map[string]muxEntry)}e := muxEntry{h: handler, pattern: pattern}// map存储路由和处理函数的映射mux.m[pattern] = e// 如果路由最后加了`/`放入到切片后在路由匹配时做前缀匹配if pattern[len(pattern)-1] == '/' {mux.es = appendSorted(mux.es, e)}// 如果路由第一位不是/,则认为注册的路由加上了host,所以在路由匹配时使用host+path进行匹配;if pattern[0] != '/' {mux.hosts = true}
}
监听端口
net/http库提供了ListenAndServe()用来监听TCP连接并处理请求:
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}
在这里初始化Server结构,然后调用ListenAndServe:
func (srv *Server) ListenAndServe() error {if srv.shuttingDown() {return ErrServerClosed}addr := srv.Addrif addr == "" {addr = ":http"}// 调用 net 进行 tcp 连接ln, err := net.Listen("tcp", addr)if err != nil {return err}return srv.Serve(ln)
}
这里调用net
网络库进行tcp
连接,包含了创建socket
、bind
绑定socket
与地址,listen
端口的操作,最后调用Serve
方法循环等待客户端的请求:
可以看到,每个HTTP
请求服务端都会单独创建一个goroutine
来处理请求,我们看一下处理过程:
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())var inFlightResponse *responsedefer func() {// 添加recover函数防止panic引发主程序挂掉;if err := recover(); err != nil && err != ErrAbortHandler {const size = 64 << 10buf := make([]byte, size)buf = buf[:runtime.Stack(buf, false)]c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)}}()// HTTP/1.x from here on.ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer cancelCtx()c.r = &connReader{conn: c}c.bufr = newBufioReader(c.r)c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)for {// 读取请求,从连接中获取HTTP请求并构建一个实现了`net/http.Conn.ResponseWriter`接口的变量`net/http.response`w, err := c.readRequest(ctx)if c.r.remain != c.server.initialReadLimitSize() {c.setState(c.rwc, StateActive, runHooks)}if err != nil {}// 处理请求serverHandler{c.server}.ServeHTTP(w, w.req)}
}
继续跟踪ServeHTTP
方法,ServeMux
是一个HTTP
请求的多路复用器,在这里可以根据请求的URL
匹配合适的处理器
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)// 这块就是执行我们注册的handler,也就是例子中的getProfile()h.ServeHTTP(w, r)
}
路由匹配
mux.Handler()
中是路由匹配的代码
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 onesif mux.hosts {h, pattern = mux.match(host + path)}if h == nil {h, pattern = mux.match(path)}if h == nil {h, pattern = NotFoundHandler(), ""}return
}func (mux *ServeMux) match(path string) (h Handler, pattern string) {// 先从map中查找v, ok := mux.m[path]if ok {// 找打了返回注册的函数return v.h, v.pattern}// 从切片中进行前缀匹配for _, e := range mux.es {if strings.HasPrefix(path, e.pattern) {return e.h, e.pattern}}return nil, ""
}
总结
服务端的代码看主逻辑主要是看两部分,
- 一个是注册处理器,标准库使用
map
进行存储,本质是一个静态索引,同时维护了一个切片,用来做前缀匹配,只要以/
结尾的,都会在切片中存储; - 服务端监听端口本质也是使用net网络库进行
TCP
连接,然后监听对应的TCP
连接,每一个HTTP
请求都会开一个goroutine
去处理请求,所以如果有海量请求,会在一瞬间创建大量的goroutine
,这个是一个性能瓶颈点。