http 服务源码分析

lpxxn · · 823 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

多读go的源码,可以加深对go语言的理解和认知,今天分享一下http相关的源码部分 在不使用第三方库的情况下,我们可以很容易的的用go实现一个http服务, ``` package main import ( "fmt" "net/http" ) func IndexHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello world ! ") } func main() { http.HandleFunc("/", IndexHandler) if err := http.ListenAndServe(":9100", nil); err != nil { panic(err) } } ``` 直接在浏览器里访问9100端口就可以返回 `hello world ! ` go已经把所有的细节封装好了,我们只需要自己去写Handler实现就够了。源码简单来说做了以下几件事: * 把我们自定义的Handler方法添加到默认路由`DefaultServeMux`的Map里比如:`http.HandleFunc("/", IndexHandler)` (btw: go语言的map是非线程安全的,可以在http源码里看到官方的处理方式); * 启动一个tcp服务监听9100端口,等待http调用; * 当监听到有http调用时,启动一个协程来处理这个请求,这个是go的http服务快的一个重要原因,把请求内容转换成http.Request, 把当前连接封装http.RespnseWriter; * 默认路由`DefaultServeMux`根据request的path找到相应的Handler,把 request和 responseWriter传给Handler 进行业务逻辑处理,response响应信息write给客户端; ### ServeMux & Handler http 包的默认路由 `DefaultServeMux` 是 `ServeMux` 结构休的实例 `http.HandleFunc("/", IndexHandler)` 的调用,会把path信息和自定义的方法信息保存到 `DefaultServeMux` 的 `m map[string]muxEntry`变量里 我们看一下`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 } type muxEntry struct { h Handler pattern string } ``` `ServeMux` 中保存了`path`和`Handler` 的对应关系,也是路由关系。 #### Handler `muxEntry` 中的 `h Handler` 对就的就是我们自定义的Handler方法比如,我们自己例子中的方法 `func IndexHandler(w http.ResponseWriter, r *http.Request)` 细心的同学可能会问 Handler是一个接口,但是我们只是定义了一个方法,这是怎么转换的呢? 接口Halder设置了签名规则,也就是我们自定义的处理方法 ``` type Handler interface { ServeHTTP(ResponseWriter, *Request) } ``` go语言中所有的自定义类型都可以实现自己的方法,http包是用一个自定义的func来去实现了Handler接口, ``` type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ``` 然后在`ServerMux`的方法`HandleFunc`处理的时候会把 `handler func(ResponseWriter, *Request)` 转换成 `HandlerFunc`, 如下所示: ``` // HandleFunc registers the handler function for the given pattern. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) } ``` `ServerMux` 结构中还有一个读写锁 `mu sync.RWMutex` mu就是用来处理多线程下map的安全访问的。 #### 查找&调用 Handler 得到自定义的handler方法,就是去map中根据path匹配得到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) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern } // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" } ``` `ServeMux` 实现了 `Handler` 接口,也是默认的路由调用的具体规则实现的地方,他的 `ServeHTTP` 方法处理方式就是得到自定义的handler方法,并调用我们自定义的方法: ``` 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) h.ServeHTTP(w, r) } ``` 接口Halder设置了签名规则,也就是我们自定义的处理方法 比如下面的代码,函数IndexHandler就是我们自定义的方法,返回给客户端请求一个 `hello world ! ` 字符串。中间请求是如何调用到我们自定义的方法的具体逻辑都是http包提供的,但是一点也不神秘, ``` http.HandleFunc("/", IndexHandler) // IndexHandler 我们自己定义的Handler方法 func IndexHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello world ! ") } ``` ``` type Handler interface { ServeHTTP(ResponseWriter, *Request) } // type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ``` ### http ListenAndServe 说完 `ServeMux` 是如何结合 `Handler` 接口,来实现路由和调用后,就要说一下,http服务是如何得到客户端传入的信息,封装requet和rresponse的。 在启动程序的时候`http.ListenAndServe`, 有两个参数,第一个是指写端口号,第二个是处理逻辑,如果我们没有给定处理逻辑,会使用默认的处理`DefaultServeMux` ``` func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) } ``` `ListenAndServe` 方法打开tcp端口进行监听,然后把`Listener` 传给`srv.Serve`方法 ``` func (srv *Server) ListenAndServe() error { // 省略部分代码 ... ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) } ``` 具体要说一下 `Service` 方法,这个方法中,对监听tcp请求,然后把请求的客户端连接进行封装, ``` func (srv *Server) Serve(l net.Listener) error { // 省略部分代码 ... ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, e := l.Accept() // 省略部分代码 ... tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew) // before Serve can return go c.serve(ctx) } } ``` 把客户端的请求封装成一个Conn,`然后启动一个协程go c.serve(ctx)来处理这个连接请求,这就是http包快的一个重要原因`,每一个连接就是一个协程。客户端可以先和服务器进行连接,然后利用这个conn来多次发送http请求,这样,就可以减少每次的进行连接而提高一些速度。像一些rpc里就是利用这点去实现的双向的stream流,比如我之前的帖子[go微服务框架go-micro深度学习(五) stream 调用过程详解](https://www.cnblogs.com/li-peng/p/10510198.html),他就是建立一个tcp连接,然后基于这个conn,发送多个request,返回多次respose数据。 ``` // Serve a new connection. func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) // 省略部分代码 ... // 循环读取请求 ... for { // 读取请求数据,封装response 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) } // 省略部分代码 ... // 路由调用自定义的方法,把封装好的responseWrite和 request传到方法内 serverHandler{c.server}.ServeHTTP(w, w.req) w.cancelCtx() if c.hijacked() { return } w.finishRequest() // 省略部分代码 ... } } ```

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

823 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传