深入理解Golang之http server的实现

前言

对于Golang来说,实现一个简单的 http server 非常容易,只需要短短几行代码。同时有了协程的加持,Go实现的 http server 能够取得非常优秀的性能。这篇文章将会对go标准库 net/http 实现http服务的原理进行较为深入的探究,以此来学习了解网络编程的常见范式以及设计思路。

HTTP服务

基于HTTP构建的网络应用包括两个端,即客户端( Client )和服务端( Server )。两个端的交互行为包括从客户端发出 request 、服务端接受 request 进行处理并返回 response 以及客户端处理 response 。所以http服务器的工作就在于如何接受来自客户端的 request ,并向客户端返回 response

典型的http服务端的处理流程可以用下图表示:

服务器在接收到请求时,首先会进入路由( router ),这是一个 Multiplexer ,路由的工作在于为这个 request 找到对应的处理器( handler ),处理器对 request 进行处理,并构建 response 。Golang实现的 http server 同样遵循这样的处理流程。

我们先看看Golang如何实现一个简单的 http server

package main

import (
 "fmt"
 "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "hello world")
}

func main() {
 http.HandleFunc("/", indexHandler)
 http.ListenAndServe(":8000", nil)
}

运行代码之后,在浏览器中打开 localhost:8000 就可以看到 hello world 。这段代码先利用 http.HandleFunc 在根路由 / 上注册了一个 indexHandler , 然后利用 http.ListenAndServe 开启监听。当有请求过来时,则根据路由执行对应的 handler 函数。

我们再来看一下另外一种常见的 http server 实现方式:

package main

import (
 "fmt"
 "net/http"
)

type indexHandler struct {
 content string
}

func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, ih.content)
}

func main() {
 http.Handle("/", &indexHandler{content: "hello world!"})
 http.ListenAndServe(":8001", nil)
}

Go实现的 http 服务步骤非常简单,首先注册路由,然后创建服务并开启监听即可。下文我们将从注册路由、开启服务、处理请求这几个步骤了解Golang如何实现 http 服务。

注册路由

http.HandleFunchttp.Handle 都是用于注册路由,可以发现两者的区别在于第二个参数,前者是一个具有 func(w http.ResponseWriter, r *http.Requests) 签名的函数,而后者是一个结构体,该结构体实现了 func(w http.ResponseWriter, r *http.Requests) 签名的方法。

http.HandleFunchttp.Handle 的源码如下:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

// 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))
}
func Handle(pattern string, handler Handler) {
 DefaultServeMux.Handle(pattern, handler)
}

可以看到这两个函数最终都由 DefaultServeMux 调用 Handle 方法来完成路由的注册。

这里我们遇到两种类型的对象: ServeMuxHandler ,我们先说 Handler

Handler

Handler 是一个接口:

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

Handler 接口中声明了名为 ServeHTTP 的函数签名,也就是说任何结构只要实现了这个 ServeHTTP 方法,那么这个结构体就是一个 Handler 对象。其实go的 http 服务都是基于 Handler 进行处理,而 Handler 对象的 ServeHTTP 方法也正是用以处理 request 并构建 response 的核心逻辑所在。

回到上面的 HandleFunc 函数,注意一下这行代码:

mux.Handle(pattern, HandlerFunc(handler))

可能有人认为 HandlerFunc 是一个函数,包装了传入的 handler 函数,返回了一个 Handler 对象。然而这里 HandlerFunc 实际上是将 handler 函数做了一个 类型转换 ,看一下 HandlerFunc 的定义:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
 f(w, r)
}

HandlerFunc 是一个类型,只不过表示的是一个具有 func(ResponseWriter, *Request) 签名的函数类型,并且这种类型实现了 ServeHTTP 方法(在 ServeHTTP 方法中又调用了自身),也就是说这个类型的函数其实就是一个 Handler 类型的对象。利用这种类型转换,我们可以将一个 handler 函数转换为一个

Handler 对象,而不需要定义一个结构体,再让这个结构实现 ServeHTTP 方法。读者可以体会一下这种技巧。

ServeMux

Golang中的路由(即 Multiplexer )基于 ServeMux 结构,先看一下 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 中的字段 m ,这是一个 mapkey 是路由表达式, value 是一个 muxEntry 结构, muxEntry 结构体存储了对应的路由表达式和 handler

值得注意的是, ServeMux 也实现了 ServeHTTP 方法:

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)
}

也就是说 ServeMux 结构体也是 Handler 对象,只不过 ServeMuxServeHTTP 方法不是用来处理具体的 request 和构建 response ,而是用来确定路由注册的 handler

注册路由

搞明白 HandlerServeMux 之后,我们再回到之前的代码:

DefaultServeMux.Handle(pattern, handler)

这里的 DefaultServeMux 表示一个默认的 Multiplexer ,当我们没有创建自定义的 Multiplexer ,则会自动使用一个默认的 Multiplexer

然后再看一下 ServeMuxHandle 方法具体做了什么:

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)
 }
 // 利用当前的路由和handler创建muxEntry对象
 e := muxEntry{h: handler, pattern: pattern}
 // 向ServeMux的map[string]muxEntry增加新的路由匹配规则
 mux.m[pattern] = e
 // 如果路由表达式以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry中,按照路由表达式长度排序
 if pattern[len(pattern)-1] == '/' {
  mux.es = appendSorted(mux.es, e)
 }

 if pattern[0] != '/' {
  mux.hosts = true
 }
}

Handle 方法主要做了两件事情:一个就是向 ServeMuxmap[string]muxEntry 增加给定的路由匹配规则;然后如果路由表达式以 '/' 结尾,则将对应的 muxEntry 对象加入到 []muxEntry 中,按照路由表达式长度排序。前者很好理解,但后者可能不太容易看出来有什么作用,这个问题后面再作分析。

自定义ServeMux

我们也可以创建自定义的 ServeMux 取代默认的 DefaultServeMux

package main

import (
 "fmt"
 "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "hello world")
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
 w.Header().Set("Content-Type", "text/html")
 html := `<!doctype html>
 <META http-equiv="Content-Type" content="text/html" charset="utf-8">
 <html lang="zh-CN">
   <head>
     <title>Golang</title>
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" />
   </head>
   <body>
    <div id="app">Welcome!</div>
   </body>
 </html>`
 fmt.Fprintf(w, html)
}

func main() {
 mux := http.NewServeMux()
 mux.Handle("/", http.HandlerFunc(indexHandler))
 mux.HandleFunc("/welcome", htmlHandler)
 http.ListenAndServe(":8001", mux)
}

NewServeMux() 可以创建一个 ServeMux 实例,之前提到 ServeMux 也实现了 ServeHTTP 方法,因此 mux 也是一个 Handler 对象。对于 ListenAndServe() 方法,如果传入的 handler 参数是自定义 ServeMux 实例 mux ,那么 Server 实例接收到的路由对象将不再是 DefaultServeMux 而是 mux

开启服务

首先从 http.ListenAndServe 这个方法开始:

func ListenAndServe(addr string, handler Handler) error {
 server := &Server{Addr: addr, Handler: handler}
 return server.ListenAndServe()
}

func (srv *Server) ListenAndServe() error {
 if srv.shuttingDown() {
  return ErrServerClosed
 }
 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)})
}

这里先创建了一个 Server 对象,传入了地址和 handler 参数,然后调用 Server 对象 ListenAndServe() 方法。

看一下 Server 这个结构体, Server 结构体中字段比较多,可以先大致了解一下:

type Server struct {
 Addr string // TCP address to listen on, ":http" if empty
 Handler Handler // handler to invoke, http.DefaultServeMux if nil
 TLSConfig *tls.Config
 ReadTimeout time.Duration
 ReadHeaderTimeout time.Duration
 WriteTimeout time.Duration
 IdleTimeout time.Duration
 MaxHeaderBytes int
 TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
 ConnState func(net.Conn, ConnState)
 ErrorLog *log.Logger

 disableKeepAlives int32  // accessed atomically.
 inShutdown  int32  // accessed atomically (non-zero means we're in Shutdown)
 nextProtoOnce  sync.Once // guards setupHTTP2_* init
 nextProtoErr  error  // result of http2.ConfigureServer if used

 mu   sync.Mutex
 listeners map[*net.Listener]struct{}
 activeConn map[*conn]struct{}
 doneChan chan struct{}
 onShutdown []func()
}

ServerListenAndServe 方法中,会初始化监听地址 Addr ,同时调用 Listen 方法设置监听。最后将监听的TCP对象传入 Serve 方法:

func (srv *Server) Serve(l net.Listener) error {
 ...

 baseCtx := context.Background() // base is always background, per Issue 16220
 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
  rw, e := l.Accept() // 等待新的连接建立

  ...

  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew) // before Serve can return
  go c.serve(ctx) // 创建新的协程处理请求
 }
}

这里隐去了一些细节,以便了解 Serve 方法的主要逻辑。首先创建一个上下文对象,然后调用 ListenerAccept() 等待新的连接建立;一旦有新的连接建立,则调用 ServernewConn() 创建新的连接对象,并将连接的状态标志为 StateNew ,然后开启一个新的 goroutine 处理连接请求。

处理连接

我们继续探索 connserve() 方法,这个方法同样很长,我们同样只看关键逻辑。坚持一下,马上就要看见大海了。

func (c *conn) serve(ctx context.Context) {

 ...

 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)
  }

  ...

  // HTTP cannot have multiple simultaneous active requests.[*]
  // Until the server replies to this request, it can't read another,
  // so we might as well run the handler in this goroutine.
  // [*] Not strictly true: HTTP pipelining. We could let them all process
  // in parallel even if their responses need to be serialized.
  // But we're not going to implement HTTP pipelining because it
  // was never deployed in the wild and the answer is HTTP/2.
  serverHandler{c.server}.ServeHTTP(w, w.req)
  w.cancelCtx()
  if c.hijacked() {
   return
  }
  w.finishRequest()
  if !w.shouldReuseConnection() {
   if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
    c.closeWriteAndWait()
   }
   return
  }
  c.setState(c.rwc, StateIdle) // 请求处理结束后,将连接状态置为空闲
  c.curReq.Store((*response)(nil))// 将当前请求置为空

  ...
 }
}

当一个连接建立之后,该连接中所有的请求都将在这个协程中进行处理,直到连接被关闭。在 serve() 方法中会循环调用 readRequest() 方法读取下一个请求进行处理,其中最关键的逻辑就是一行代码:

serverHandler{c.server}.ServeHTTP(w, w.req)

进一步解释 serverHandler

type serverHandler struct {
 srv *Server
}

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)
}

serverHandlerServeHTTP() 方法里的 sh.srv.Handler 其实就是我们最初在 http.ListenAndServe() 中传入的 Handler 对象,也就是我们自定义的 ServeMux 对象。如果该 Handler 对象为 nil ,则会使用默认的 DefaultServeMux 。最后调用 ServeMuxServeHTTP() 方法匹配当前路由对应的 handler 方法。

后面的逻辑就相对简单清晰了,主要在于调用 ServeMuxmatch 方法匹配到对应的已注册的路由表达式和 handler

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request 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)
 h.ServeHTTP(w, r)
}

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
}

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
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, ""
}

match 方法里我们看到之前提到的 map[string]muxEntry[]muxEntry 。这个方法里首先会利用进行精确匹配,在 map[string]muxEntry 中查找是否有对应的路由规则存在;如果没有匹配的路由规则,则会进行近似匹配。

对于类似 /path1/path2/path3 这样的路由,如果不能找到精确匹配的路由规则,那么则会去匹配和当前路由最接近的已注册的父路由,所以如果路由 /path1/path2/ 已注册,那么该路由会被匹配,否则继续匹配父路由,知道根路由 /

由于 []muxEntry 中的 muxEntry 按照路由表达是从长到短排序,所以进行近似匹配时匹配到的路由一定是已注册父路由中最接近的。

至此,Go实现的 http server 的大致原理介绍完毕!

总结

Golang通过 ServeMux 定义了一个多路器来管理路由,并通过 Handler 接口定义了路由处理函数的统一规范,即 Handler 都须实现 ServeHTTP 方法;同时 Handler 接口提供了强大的扩展性,方便开发者通过 Handler 接口实现各种中间件。相信大家阅读下来也能感受到 Handler 对象在 server 服务的实现中真的无处不在。理解了 server 实现的基本原理,大家就可以在此基础上阅读一些第三方的 http server 框架,以及编写特定功能的中间件。

以上。

参考资料

【Golang标准库文档--net/http】

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • golang的httpserver优雅重启方法详解

    前言 去年在做golangserver的时候,内部比较头疼的就是在线服务发布的时候,大量用户的请求在发布时候会被重连,在那时候也想了n多的方法,最后还是落在一个github上的项目,facebook的一个golang项目grace,那时候简单研究测试了一下可以就直接在内部使用了起来,这段时间突然想起来,又想仔细研究一下这个项目了. 从原理上来说是这样一个过程: 1)发布新的bin文件去覆盖老的bin文件 2)发送一个信号量,告诉正在运行的进程,进行重启 3)正在运行的进程收到信号后,会以子进程的

  • 深入理解Golang之http server的实现

    前言 对于Golang来说,实现一个简单的 http server 非常容易,只需要短短几行代码.同时有了协程的加持,Go实现的 http server 能够取得非常优秀的性能.这篇文章将会对go标准库 net/http 实现http服务的原理进行较为深入的探究,以此来学习了解网络编程的常见范式以及设计思路. HTTP服务 基于HTTP构建的网络应用包括两个端,即客户端( Client )和服务端( Server ).两个端的交互行为包括从客户端发出 request .服务端接受 request

  • golang实现http server提供文件下载功能

    简介 Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言. 罗伯特·格瑞史莫(Robert Griesemer),罗勃·派克(Rob Pike)及肯·汤普逊(Ken Thompson)于2007年9月开始设计Go,稍后Ian Lance Taylor.Russ Cox加入项目.Go是基于Inferno操作系统所开发的.Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及Mac OS X平台上进行了实现,后来追加了Windo

  • Golang实现http server提供压缩文件下载功能

    最近遇到了一个下载静态html报表的需求,需要以提供压缩包的形式完成下载功能,实现的过程中发现相关文档非常杂,故总结一下自己的实现. 开发环境: 系统环境:MacOS + Chrome 框架:beego 压缩功能:tar + gzip 目标压缩文件:自带数据和全部包的静态html文件 首先先提一下http server文件下载的实现,其实就是在后端返回前端的数据包中,将数据头设置为下载文件的格式,这样前端收到返回的响应时,会直接触发下载功能(就像时平时我们在chrome中点击下载那样) 数据头设

  • 深入理解Golang Channel 的底层结构

    目录 make chan 发送和接收 Goroutine Pause/Resume wait empty channel Golang 使用 Groutine 和 channels 实现了 CSP(Communicating Sequential Processes) 模型,channles在 goroutine 的通信和同步中承担着重要的角色. 在GopherCon 2017 中,Golang 专家 Kavya 深入介绍了 Go Channels 的内部机制,以及运行时调度器和内存管理系统是如

  • 深入理解Golang的反射reflect示例

    目录 编程语言中反射的概念 interface 和 反射 Golang的反射reflect reflect的基本功能TypeOf和ValueOf 说明 从relfect.Value中获取接口interface的信息 已知原有类型[进行“强制转换”] 说明 未知原有类型[遍历探测其Filed] 说明 通过reflect.Value设置实际变量的值 说明 通过reflect.ValueOf来进行方法的调用 说明 Golang的反射reflect性能 小结 总结 参考链接 编程语言中反射的概念 在计算

  • 深入理解Golang Channel 的底层结构

    目录 makechan 发送和接收 GoroutinePause/Resume waitemptychannel Golang 使用 Groutine 和 channels 实现了 CSP(Communicating Sequential Processes) 模型,channles在 goroutine 的通信和同步中承担着重要的角色. 在GopherCon 2017 中,Golang 专家 Kavya 深入介绍了 Go Channels 的内部机制,以及运行时调度器和内存管理系统是如何支持

  • 深入理解golang chan的使用

    目录 前言 见真身 结构体 发送数据 接收数据 上手 定义 发送与接收 前言 之前在看golang多线程通信的时候, 看到了go 的管道. 当时就觉得这玩意很神奇, 因为之前接触过的不管是php, java, Python, js, c等等, 都没有这玩意, 第一次见面, 难免勾起我的好奇心. 所以就想着看一看它具体是什么东西. 很明显, 管道是go实现在语言层面的功能, 所以我以为需要去翻他的源码了. 虽然最终没有翻到C的层次, 不过还是受益匪浅. 见真身 结构体 要想知道他是什么东西, 没什

  • 深入理解Golang channel的应用

    目录 前言 整体结构 创建 发送 接收 关闭 前言 channel是用于 goroutine 之间的同步.通信的数据结构 channel 的底层是通过 mutex 来控制并发的,但它为程序员提供了更高一层次的抽象,封装了更多的功能,这样并发编程变得更加容易和安全,得以让程序员把注意力留到业务上去,提升开发效率 channel的用途包括但不限于以下几点: 协程间通信,同步 定时任务:和timer结合 解耦生产方和消费方,实现阻塞队列 控制并发数 本文将介绍channel的底层原理,包括数据结构,c

  • 一文彻底理解Golang闭包实现原理

    目录 前言 函数一等公民 作用域 实现闭包 闭包扫描 闭包赋值 闭包函数调用 函数式编程 总结 前言 闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 Python 和 Go 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理. 函数一等公民 一门语言在实现闭包之前首先要具有的特性就是:First class function 函数是第一公民. 简单来说就是函数可以像一个普通的值一样在函数中传递,也能

  • 深入理解Golang make和new的区别及实现原理

    目录 前言 new的使用 底层实现 make的使用 底层实现 总结 前言 在Go语言中,有两个比较雷同的内置函数,分别是new和make方法,二者都可以用来分配内存,那他们有什么区别呢?对于初学者可能会觉得有点迷惑,尤其是在掌握不牢固的时候经常遇到panic,下面我们就从底层来分析一下二者的不同.感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助. new的使用 new可以对类型进行内存创建和初始化,其返回值是所创建类型的指针引用,这是与make函数的区别之一.我们通过一个示例代码看下: fun

随机推荐