http 库的服务端实现

前言

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就是patternhandler
  • **m**:存储路由规则,key就是patternvaluemuEntry实体,muEntry实体中包含:patternhandler
  • **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连接,包含了创建socketbind绑定socket与地址,listen端口的操作,最后调用Serve方法循环等待客户端的请求:
image.png
可以看到,每个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,这个是一个性能瓶颈点。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值