摘要

由于Golang优秀的并发处理,很多公司使用Golang编写微服务。对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器。加上Golang的协程,这个服务器可以拥有极高的性能。然而,正是因为代码过于简单,我们才应该去研究他的底层实现,做到会用,也知道为什么这么用。

在本文中,会以自顶向下的方式,从如何使用,到如何实现,一点点的分析Golang中net/http这个包中关于Http服务器的实现方式。内容可能会越来越难理解,作者会尽量把这些源码讲的更清楚一些,希望对各位有所帮助。

1 创建

首先,我们以怎么用为起点。

毕竟,知道了怎么用,才能一步一步的深入挖掘为什么这么用。

先来看第一种最简单的创建方式(省略了导包):

func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World !")
} func main() {
http.HandleFunc("/", helloWorldHandler)
http.ListenAndServe(":8000", nil)
}

其实在这一部分中,代码应该很容易理解。就是先做一个映射,把需要访问的地址,和访问后执行的函数,写在一起。然后再加上监听的端口,就可以了。

如果你是一个Java程序员,你应该能发觉这个和Java中的Servlet很相似。也是创建一个个的Servlet,然后注册。

再来看看第二种创建方式,也一样省略了导包:

type helloWorldHandler struct {
content string
} func (handler *helloWorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, handler.content)
} func main() {
http.Handle("/", &helloWorldHandler{content: "Hello World!"})
http.ListenAndServe(":8000", nil)
}

在这里,我们能发现相较于第一种方法,有些许的改动。

我们定义了一个结构体,然后又给这个结构体编写了一个方法。根据我们之前对于接口的概念:要实现一个接口必须要实现这个接口的所有方法

那么我们是不是可以推测:存在这么一个接口A,里面有一个名为ServeHTTP的方法,而我们所编写的这个结构体,他已经实现了这个接口A了,他现在是属于这个A类型的一个结构体了。

type A interface{
ServeHTTP()
}

并且,在main函数中关于映射URI和方法的参数部分,需要调用实现了这个接口A的一个对象。

带着这个问题,我们可以继续往下。

2 注册

在第一部分,我们提到了两种注册方式,一种是传入一个函数,一种是传入一个结构体指针。

http.HandleFunc("/", helloWorldHandler)

http.Handle("/", &helloWorldHandler{content: "Hello World!"})

我们来看看http包内的源码:

package http

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

先看一下这里的代码,他们被称为注册函数

首先研究一下HandleFunc这个函数。在main函数中,调用了这个具有func(pattern string, handler func(ResponseWriter, *Request))签名的函数,这里的patternstring类型的,指的是匹配的URI,这个很容易理解。第二个参数是一个具有func(ResponseWriter, *Request)签名的函数。

然后我们继续看,在这个函数中,调用了这个方法:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}

我们可以看到,最终是调用了DefaultServeMux对象的Handle方法

好,先到这里,我们再看一看刚刚提到的签名为func (pattern string, handler Handler)另外一个函数。在这个函数里面,同样是调用了DefaultServeMux对象的Handle方法

也就是说,无论我们使用哪种注册函数,最终调用的都是这个函数:

func (mux *ServeMux) Handle(pattern string, handler Handler)

这里涉及到了两种对象,第一是ServeMux对象,第二是Handler对象。

ServeMux对象我们一会再聊,先聊聊Handler对象。

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

在Golang中,Handler是一种接口类型,只要一个类型的对象实现了ServeHTTP这个方法,就可以称这个对象是Handler类型的。

注意到,在前面有一行代码是这样的:

mux.Handle(pattern, HandlerFunc(handler))

有人可能会想,HandlerFunc func(ResponseWriter, *Request)这个函数,是输入一个函数,返回一个Handler类型的对象,其实这是不对的。我们来看看这个函数的源码:

type HandlerFunc func(ResponseWriter, *Request)

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

我们可以发现,这个函数,他是属于一种叫HandlerFunc类型的函数。

在Golang中,这是一种很特别的特性。我们可以将函数设置为一种类型,或者你可以理解成变量,你可以用一个变量名去表示这个函数,可以把这个函数赋值给某一个变量。

fn := func(){
fmt.Println("x is",x)
}
fn()

所以,在这里这个函数类型也是实现了ServeHTTP方法的,也就是说,这个名为HandlerFunc的函数类型,也是属于Handler类型的。所以,这个方法其实并不是输入一组参数,返回一个Handler类型,而是他本身就是一个Handler类型,可以直接调用ServeHTTP方法。

这里比较绕,但是相信当你理解了之后,会感觉妙啊

说完了Handler,我们再来聊聊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
}

我们先关注一下这个结构里面的m字段。这个字段是一个map类型,key是URI,value是muxEntry类型。而这个muxEntry类型,里面包含了一个HandlerURI。也就是说,通过这个m字段,我们可以用URI找到对应的Handler对象。

继续说回上面提到的func (mux *ServeMux) Handle(pattern string, handler Handler)方法。我们已经知道了调用这个方法的对象是ServeMux,也知道了这个方法的参数中的Handler是什么,下面让我们来看看这个方法的详细实现:

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}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
} if pattern[0] != '/' {
mux.hosts = true
}
}

在这个方法中,我们可以看到,Handle方法会先判断传入的URIhandler是否合法,然后判断这个URI对应的处理器是否已经注册,然后将这个URIhandler对应的map写入ServeMux对象中。

注意,这里还有一个步骤。如果这个URI是以/结尾的,将会被送入es数组中,按长度排序。至于为什么会这么做,我们在后面的内容将会提到。

说完了这些,我们应该可以猜到这个ServeMux对象的作用了。他可以存储我们注册的URIHandler,以实现当有请求进来的时候,可以委派给相对应的Handler的功能。

考虑到这个功能,那么我们也可以推断出,这个ServeMux也是一个Handler,只不过他和其他的Handler不同。其他的Handler处理的是具体的请求,而这个ServeMux处理的是请求的分配。

所以,ServeMux也实现了ServeHTTP方法,他也是一个Handler。而对于他是怎么实现ServeHTTP方法的,我们也在后面的内容提到。

3 监听

现在,让我们来聊聊main函数中的第二行:

http.ListenAndServe(":8000", nil)

按照惯例,我们来看一看这个方法的实现:

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

这里的Server,是一个复杂的结构体,里面包含了设置服务器的很多参数,但是这里我们只聊AddrHandler这两个属性。

Addr很容易理解,就是这个服务器所监听的地址。

Handler是处理器,负责把请求分配给各个对应的handler。在这里留空,则使用Golang默认的处理器,也就是上文中我们提到的实现了ServeHTTP方法的ServeMux

知道了这些,我们继续往下看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(ln)
}

这里比较重要的有两行,第一是ln, err := net.Listen("tcp", addr),也就是说,开始监听addr这个地址的tcp连接

然后,调用srv.Serve(ln),我们来看看代码(省略部分,只保留与本文有关的逻辑):

func (srv *Server) Serve(l net.Listener) error {
...
for{
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx)
}
}

简单来讲,在这个方法中,有一个死循环,他不断接收新的连接,然后启动一个协程,处理这个连接。我们来看看c.serve(connCtx)的具体实现:

func (c *conn) serve(ctx context.Context) {
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}

省略其他所有的细节,最关键的就是这一行代码了,然后我们再看看这个ServeHTTP方法。注意,这里的c.server,还是指的是最开始的那个Server结构体。坚持一下下,马上就到最关键的地方啦:

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

这里的ServeHTTP方法逻辑很容易看出,如果最开始没有定义一个全局处理的Handler,则会使用Golang的默认handlerDefaultServeMux

假设,我们这里使用的是DefaultServeMux,执行ServeHTTP方法。说到这里你是否有印象,我们在上一个章节里提到的:

所以,ServeMux也实现了ServeHTTP方法,他也是一个Handler。而对于他是怎么实现ServeHTTP方法的,我们也在后面的内容提到。

就是这里,对于ServeMux来说,他就是一个处理请求分发的Handler

如果你学过Java,我跟你说他和ServletDispatcher很相似,你应该能理解吧。

4 处理

到了这里,就是最后一步了,我们来看看这里处理请求分发的ServeHTTP方法具体实现:

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

在省去其他细节之后我们应该可以推断,这个mux.Handler(r)方法返回的h,应该是所请求的URI所对应的Handler。然后,执行这个Handler所对应的ServeHTTP方法。我们来看看mux.Handler(r)这个方法:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
...
return mux.handler(host, 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
}

到了这里,代码就变得简洁明了了。重点就是这个mux.match方法,会根据地址,来返回对应的Handler。我们来看看这个方法:

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中存储了key为这个URI的路由规则的映射,则直接返回这个URI对应的Handler

否则,就去匹配es数组。还记得吗,这个数组是之前注册路由的时候提到的,如果URI是以/结尾的,就会把这个路由映射添加到es数组中,并由长到短进行排序。

这样的作用是,可以优先匹配到最长的URI,以达到近似匹配的时候能够匹配到最合适的路由的目的。

至此,返回对应的Handler,然后执行,就成功的实现了处理相对应的请求了。

写在最后

首先,谢谢你能看到这里!

不知道你有没有理解我所说的内容,希望这篇文章可以给你一些帮助。

其实写这篇文章的目的是这样的,学完了Golang的基础之后作者准备开始研究Golang Web。但是查找各种资料后发现,并没有找到一条很合适的学习路线。然后本来作者打算去直接研究一个框架,如MeeGo,Gin等。但是又考虑到,框架只是用来解决问题的,学会了框架却不知道基础内容,有种知其然不知其所以然的感觉。

所以,作者打算从Golang的net/http包的源码开始,慢慢去了解怎么用原生的Go语言去建立一个HTTP服务器,然后去了解一下怎么进行缓存,做持久化等,这也是作者思考之后决定的一条学习路线。当能够把这些内容都研究明白之后,再去研究框架,去看这些框架是怎么解决问题的,可能才是比较合适的。

当然了,作者也是刚入门。所以,可能会有很多的疏漏。如果在阅读的过程中,有哪些解释不到位,或者理解出现了偏差,也请你留言指正。

再次感谢~

PS:如果有其他的问题,也可以在公众号找到作者。并且,所有文章第一时间会在公众号更新,欢迎来找作者玩~

Golang Web入门(1):自顶向下理解Http服务器的更多相关文章

  1. Golang Web入门(4):如何设计API

    摘要 在之前的几篇文章中,我们从如何实现最简单的HTTP服务器,到如何对路由进行改进,到如何增加中间件.总的来讲,我们已经把Web服务器相关的内容大概梳理了一遍了.在这一篇文章中,我们将从最简单的一个 ...

  2. Golang Web入门(2):如何实现一个高性能的路由

    摘要 在上一篇文章中,我们聊了聊在Golang中怎么实现一个Http服务器.但是在最后我们可以发现,固然DefaultServeMux可以做路由分发的功能,但是他的功能同样是不完善的. 由Defaul ...

  3. Golang Web入门(3):如何优雅的设计中间件

    摘要 在上一篇文章中,我们已经可以实现一个性能较高,且支持RESTful风格的路由了.但是,在Web应用的开发中,我们还需要一些可以被扩展的功能. 因此,在设计框架的过程中,应该留出可以扩展的空间,比 ...

  4. golang快速入门(五)初尝web服务

    提示:本系列文章适合对Go有持续冲动的读者 初探golang web服务 golang web开发是其一项重要且有竞争力的应用,本小结来看看再golang中怎么创建一个简单的web服务. 在不适用we ...

  5. COM编程入门第二部分——深入COM服务器

    本文为刚刚接触COM的程序员提供编程指南,解释COM服务器内幕以及如何用C++编写自己的接口.继上一篇COM编程入门之后,本文将讨论有关 COM服务器的内容,解释编写自己的COM接口和COM服务器所需 ...

  6. golang web实战之二(iris)

    之前写了一篇为:golang web实战之一(beego,mvc postgresql) 听说iris更好: 1.  iris hello world package main import &quo ...

  7. 建站有很多技术,如 HTML、HTML5、XHTML、CSS、SQL、JavaScript、PHP、http://ASP.NET、Web Services、浏览器脚本、服务器脚本等。它们的区别是什么?新手一点不懂,想理清所有这些技术之间的关系和应用范围。

    先普及用户通过 浏览器 访问网页 的过程: 网页内容是通过服务器运算得出的结果,将结果(网页代码)传输给浏览器,网页代码再通过浏览器运算(计算.渲染),最终展示在用户的眼前的. 至此,我们知道了有2个 ...

  8. Golang快速入门

    Go语言简介: Golang 简称 Go,是一个开源的编程语言,Go是从2007年末由 Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian L ...

  9. Web入门

    目录 Web入门 学习web路线 前端基础 三剑客的作用 BS架构 数据格式 HTTP协议 四大特性 数据格式 HTTP 状态码分类 状态码列表 案例:简易的BS架构 Web入门 什么是前端? 任何与 ...

随机推荐

  1. 前端构建工具gulpjs的使用介绍及技巧【转载】

    转载至:http://www.cnblogs.com/2050/p/4198792.html gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常 ...

  2. .NetCore 3.1 MVC 发布Linux实操

    记录自己.net Core的学习,现在来发布到Linux系统 第一步:准备一台CentOS 7 X64服务器 .net core 3.1项目 第二步:准备服务器.net core环境 1.安装微软官方 ...

  3. Spring Cloud 系列之 Netflix Hystrix 服务容错

    什么是 Hystrix Hystrix 源自 Netflix 团队于 2011 年开始研发.2012年 Hystrix 不断发展和成熟,Netflix 内部的许多团队都采用了它.如今,每天在 Netf ...

  4. Mybatis(三)Mybatis映射开发

    4.1 一对一 4.1.1 表对应关系, 一个订单对应一个用户 4.1.2 实体对应关系 public class Order { private int id; private Date order ...

  5. Shell中的参数传递

    原文链接 我们先来定义一个方法 function methodName() { paramName1=$1 paramName2=$2 paramName3=$3 # 依此类推,参数是从1开始标号,而 ...

  6. Python-生成器实现简单的"生产者消费者"模型

    一.使用生成器实现简单的生产者消费者模型, 1.效果截屏 代码如下: import time def consumer(name): print('%s 开始买手机' %name) while Tru ...

  7. git push错误,如何回滚

    --> git push Counting objects: 81, done.Delta compression using up to 4 threads.Compressing objec ...

  8. [LeetCode] 936. Stamping The Sequence 戳印序列

    You want to form a `target` string of lowercase letters. At the beginning, your sequence is target.l ...

  9. JSOI 2016 病毒感染 辅助Dp问题

    原题链接:https://www.luogu.com.cn/problem/P5774 分析 直接看这道题,第一个困惑点,那个绝对值的比较是什么东西,根据数学知识,我们可以知道这个意思是k到i的距离小 ...

  10. 第二次实验报告:使用 Packet Tracer 分析应用层协议

    个人信息: 姓名:倪晓东 班级:计算1811 学号:201821121020 1 实验目的 熟练使用Packet Tracer工具.分析抓到的应用层协议数据包,深入理解应用层协议,包括语法.语义.时序 ...