http 包怎么用
使用 golang 的 http 包可以很简易的实现一个 web 服务,如下
main.go
package main
import (
"log"
"net/http"
"runtime"
"fmt"
)
func foo(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi! babe~"))
}
func echo(w http.ResponseWriter, r *http.Request) {
s := fmt.Sprintf("gorotines count: %d", runtime.NumGoroutine())
w.Write([]byte(s))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/foo", foo)plainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplain
mux.HandleFunc("/echo/goroutines", echo)
log.Println("Listening...")
http.ListenAndServe(":3000", mux)
}
那如果我想看看整个服务是怎么实现的,该怎么办呢?
ListenAndServe()
接收一个地址和处理程序的参数,此函数的定义如下
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
然后调用了
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
然后上述函数又调用了 Serve 函数
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
srv.trackListener(l, true)
defer srv.trackListener(l, false)
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
for {
// Accept等待并返回listener的下一个连接
rw, e := l.Accept()
if e != nil { ... } // 省略一些代码
tempDelay = 0
// 使用rw创建一个新连接
c := srv.newConn(rw)
// 将链接置为激活状态,同时可指定在客户端连接更改状态时调用可选的回调函数
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
从上面的go c.serve(ctx)
可以看出,http 包在 ctx 上下文组装好之后交给了 gorotine 来处理这个请求。在继续下一步之前,我们先看看这个 ctx 上下文,Context 被定义为一个接口,它在 golang 中被运用的非常广泛。
type Context interface {
// Deadline 设置了两个参数deadline, ok
// deadline 表示上下文被取消的截止时间
// 如果没有设置deadline,Deadline的ok参数会返回false。
// 连续调用返回结果相同
Deadline() (deadline time.Time, ok bool)
// 如果上下文被取消,Done会返回一个被关闭的chan
// 如果上下文从没被取消过,Done将返回nil
// 连续调用返回结果相同
Done() <-chan struct{}
// Done 的 chan被关闭后,也就是上下文被取消时,Err会返回非零的错误值。
// 当 Done 的 chan被关闭后,连续调用返回结果相同
Err() error
// 也就是通过key去获取该key上下文中的值,如果没有则为nil,可见ctx是一个键值对。该值是线程安全的
Value(key interface{}) interface{}
}
好了介绍完 context 之后,我们再来看看 Serve 函数中的baseCtx := context.Background()
是干什么的。
// Background返回一个非零的空Context。它没有值也没有deadline,所以也不会被取消,
// 它通常在main函数被用来初始化,测试,以及作为请求传入的顶级Context
func Background() Context {
return background
}
嗯,他其实就是初始化的一个作用。
接下来又碰到了 WithValue 函数,我们继续看看 WithValue 的定义。
// 生成一个绑定了一个键值对数据的Context,可以通过parent访问到上一层的context,这个绑定的数据可以通过Context.Value方法访问到
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// 一个valueCtx结构带有一个键值对。然后用来嵌套其他的Context。
type valueCtx struct {
Context
key, val interface{}
}
结合源码,那么这个 context 定义结构就可以了解了
// 下面定义了两个context key,一个存储了type *Server,另一个存储了type net.Addr
ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"}
// 顶层context
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
// parent 为 顶层的context
ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
那么为什么这么定义呢? 思考思考,对 context 的作用和细节还没系统了解过,context 是一个很重要的功能 TODO
继续往下看 go serve(ctx)
,可以看到这里用 gorotine 来处理每个链接来支撑并发,这也是支持并发的关键。
// 处理一个新链接
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
...
// HTTP/1.x from here on.
// 这里又碰到WithCancel函数,WithCancel返回带有父context 的Done通道副本和一个cancelCtx函数。
// 返回的上下文的Done通道在调用了返回的cancelCtx函数或父context的Done通道关闭时关闭,以先发生者为准。
// 取消此上下文会释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用cancelCtx。所以可以看到使用了defer去调用cancelCtx释放资源
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 {
// 从链接中读取请求
w, err := c.readRequest(ctx)
if c.r.remain != c.server.initialReadLimitSize() {
// If we read any bytes off the wire, we're active.
c.setState(c.rwc, StateActive)
}
...
// 核心点,该处就是处理请求的hanler
serverHandler{c.server}.ServeHTTP(w, w.req)
w.cancelCtx()
...
c.rwc.SetReadDeadline(time.Time{})
}
}
好了到这我们知道是用serverHandler{c.server}.ServeHTTP(w, w.req)
来处理请求的。我们回过头去看看,路由和 handler 是怎么绑定到一起的
// ServeMux是一个HTTP请求多路复用器,说白了就承担了路由功能呗
// 在ServeMux 的注释中,我们可以了解到整个路由的一些机制。
// 模式名称固定,带根的路径,如"/favicon.ico",或带根的子树,如"/images/"(请注意尾部斜杠)。
// 较长的模式优先于较短的模式,因此如果存在"/images/"和"/images/thumbnails/"注册的handler,则"/images/thumbnails/"开头的路径将调用后者的handler,然后前者将接收"/images/"子树中任何其他路径的请求,比方说"/images/xxxx"等等。
mux := http.NewServeMux()
// 往mux上绑定了两个handler
mux.HandleFunc("/foo", foo)
mux.HandleFunc("/echo/goroutines", echo)
我们看到 mux 调用了 HandleFunc,来看看他们的定义
// HandleFunc为给定pattern注册handler
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
// 如果pattern已经存在handler了,将会panic
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
...
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
if pattern[0] != '/' {
mux.hosts = true
}
// 如果pattern是/tree/,则为/tree插入隐式永久重定向
// 通过显式注册可以覆盖
n := len(pattern)
if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
// 如果pattern包含host name,将其删除并使用剩余的路径进行重定向。
path := pattern
if pattern[0] != '/' {
// strings.Index 返回子串 sep "/" 在字符串 pattern 中第一次出现的位置
// 如果找不到,则返回 -1,如果 sep 为空,则返回 0。
path = pattern[strings.Index(pattern, "/"):]
}
url := &url.URL{Path: path}
// 在我们的例子中pattern为"/echo/gorotine/",则会为"/echo/gorotine" 添加一个重定向
mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
}
}
看完上面的定义,我们知道路由和 handler 是怎么存储的了。
再看看 ServeMux 结构,m 是一个字典形式的,当我们调用 HandlerFunc 会把 pattern 即"/echo/goroutines"作为 key,muxEntry 作为 value,muxEntry 为一个包含 pattern,handler 和 explicit 的结构。
type ServeMux struct {
mu sync.RWMutex // 读写锁
m map[string]muxEntry // 存储结构
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
explicit bool // 该pattern是否完全匹配handler
h Handler
pattern string
}
好了,上述把 handler 绑定到了 server 上。那么是如何通过 url 查找 handler 的呢?先看看 ServeHTTP
// ServeHTTP将请求分派给handler,该handler的pattern与请求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
}
// 通过Handler找到最匹配的handler来处理该请求
h, _ := mux.Handler(r)
// 调用处理请求
h.ServeHTTP(w, r)
}
从上面代码可以看到,使用 handler 的 ServeHTTP 方法去处理请求,这里又有一个疑问了,为什么 ServeHTTP 的 w 是值传递,而 r 是引用传递呢?
先看看 w,r 的定义,通过观察 ResponseWriter,和 Request 的定义就知道为什么这么做了。
// HTTP处理程序使用ResponseWriter接口来构造HTTP响应。
// Handler.ServeHTTP方法返回后,可能就无法使用ResponseWriter了。
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
// 请求表示由服务器接收或由客户端发送的HTTP请求。
type Request struct {
...
}
可以看到,ResponseWriter 是一个接口,Request 是一个结构。我们往回拨一下,看看这个接口是什么。
w, err := c.readRequest(ctx)
...
// 正是readRequest返回的
serverHandler{c.server}.ServeHTTP(w, w.req)
// 再看看 readRequest的函数签名,其实也是一个指针来的。
func (c *conn) readRequest(ctx context.Context) (w *response, err error)
{
...
w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),
// We populate these ahead of time so we're not
// reading from req.Header after their Handler starts
// and maybe mutates it (Issue 14940)
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
if isH2Upgrade {
w.closeAfterReply = true
}
// 这里有个令人窒息的操作,对于vegetable的我来说有点难以理解。w.cw.res的res其实也是一个response,w.w的第一个w是response结构,第二个w是一个*bufio.Writer结构。
w.cw.res = w
// newBufioWriterSize返回一个Writer结构的指针,而w的Writer是一个方法,注意区分
w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
return w, nil
}
// response的Write方法正是掉用了第二个w结构的Write方法,把数据写入了缓冲区
// 在main.go中向response里写数据的方法 w.Write([]byte("hi! babe~"))
func (w *response) Write(data []byte) (n int, err error) {
return w.write(len(data), data, "")
}
// Write的具体实现
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
...
if dataB != nil {
return w.w.Write(dataB)
} else {
return w.w.WriteString(dataS)
}
}
// w.w 也就是 *bufio.Writer结构 的方法。可以看到通过copy把p写入了write结构。
func (b *Writer) Write(p []byte) (nn int, err error) {
...
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
从上面的代码可以看到 ResponseWriter 接口,其实也是传入了一个 response 结构的指针,又解决一个疑问,nice
// Handler返回用于给定请求的handler,返回依据参考r.Method,r.Host和r.URL.Path等参数。它总是返回一个非空的handler(如果没有则返回NotFound的handler)。
// 如果路径不规范,则处理程序将会走内部生成的handler,重定向到它的规范路径。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
if r.Method != "CONNECT" {
if p := cleanPath(r.URL.Path); p != r.URL.Path {
_, pattern = mux.handler(r.Host, p)
url := *r.URL
url.Path = p
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
}
return mux.handler(r.Host, r.URL.Path)
}
// handler函数是Handler的主要实现,host参数传入请求的r.Host, path参数传入r.URL.Path
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
}
上面的调用链handler.ServeHTTP
-> func (mux *ServeMux) Handler(r *Request)
-> func (mux *ServeMux) handler(r *Request)
当到达func (mux *ServeMux) handler
的时候,一切逻辑就清晰了起来。先抛一个问题,"/echo/"和"/echo/goroutines/"这俩怎么区分 handler?从前面的注释,我们知道"/echo/"会处理它所有的子树,而"/echo/goroutines/"就是它的子树,匹配的时候会根据最长原则,也就是会先匹配"/echo/goroutines/"的 handler,那我们来看看这个具体实现。
// 上层调用match函数path传入r.URL.Path
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
var n = 0
// m里的存储规则是 m['/echo/gorotines/'] = EntryMux{}
for k, v := range mux.m {
if !pathMatch(k, path) {
continue
}
// 从pathMatch函数可以知道"/echo/"会匹配所有它的子树,也就是类似"/echo/xxx"这些,该函数都会返回true。
// 所以下面这段逻辑就是上面问题的答案。即最长原则,如果满足len(k) > n 的情况,h会被替换成更长path的那个handler。
// 这段代码的时间复杂度是O(n),其他的更高效的web框架会不会实现一个O(lgn)的算法呢?我们知道trie树可以做的,下次看看其他的框架怎么实现的
if h == nil || len(k) > n {
n = len(k)
h = v.h
pattern = v.pattern
}
}
return
}
// 可以看到这个pathMatch是拿pattern和path做比较,
func pathMatch(pattern, path string) bool {
if len(pattern) == 0 {
// should not happen
return false
}
n := len(pattern)
// 如果pattern不以'/'结尾 直接比较
if pattern[n-1] != '/' {
return pattern == path
}
// 关键部位,path比pattern要长
// 截取path[0:n] 和 pattern匹配,也就是如果我们的path为"/echo/goroutines/" 我们注册的handler只有"/echo/"的话,那么"/echo/goroutines/" 会匹配到"/echo/"
return len(path) >= n && path[0:n] == pattern
}
带着一些问题,阅读了整个 http 请求的一些源码,其中确实很复杂,通过了解代码能搞大概搞清楚怎么处理的,当然作者为什么这么写脑海里仍然有一个疑问,等姿势水平再高一点再来探究。整篇分析到此结束,怎么把这些条理化展示的水平还有待提高。
回顾一下提出的几个问题
- "/echo/"和"/echo/goroutines/"这怎么区分匹配 handler?
- 为什么 ServeHTTP 的 w 是值传递,而 r 是引用传递呢?
- http server 怎么处理并发请求?
- 了解 context 在golang中的应用?TODO
参考:
傅小黑的这篇文章框架很清晰
http://fuxiaohei.me/2016/9/20/go-and-http-server.html