前言
net/http 库的客户端实现(上)
net/http 库的客户端实现(下)
net/http 库的服务端实现
上两篇文章介绍了 http 客户端的实现,这篇文章看一下服务端的实现
服务端
使用 net/http
库可以快速搭建HTTP
服务,HTTP
服务端主要包含两部分:
- 注册处理器:
net/http.HandleFunc
函数用于注册处理器 - 监听端口:
net/http.ListenAndServe
用于处理请求
package main
import (
"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.Addr
if 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 *response
defer func() {
// 添加recover函数防止panic引发主程序挂掉;
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := 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 = cancelCtx
defer 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 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
}
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
,这个是一个性能瓶颈点。