golang net/http的server源码浅析

本文主要分享golang net/http server.go中代码运行的过程。

一、背景

  分享这个源码浅析,肯定是在日常学习,工作中有遇到一些不明白的事,下面先上代码,一个很简单的http server:

package main

import (
	"fmt"
	"io"
	"net/http"
)

func myhandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println(w, "hello world")
	io.WriteString(w, "hello world")
}

func myhandler1(w http.ResponseWriter, r *http.Request) {
	fmt.Println(w, "hello golang")
	io.WriteString(w, "hello golang")
}

func main() {
	http.HandleFunc("/helloWorld", myhandler)
	http.HandleFunc("/helloGolang", myhandler1)
	fmt.Println("http server running in http://127.0.0.1 ... ... ")
	err := http.ListenAndServe(":8080", nil)
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
		}
	}()
	if err != nil {
		panic("listen failed")
	}
}

  把代码跑起来:

$ go run web.go

  在浏览器上输入:http://127.0.0.1:8080/helloWorld 就可以看到"hello world"出现在浏览器页面上。
  上面的代码是很简单,学过golang的同学都会,就是导一个net/http标准库,跟着流程走,但是大家有没有想过或者看过它的实现过程呢?
  我在学习的过程中,思索到以下几个问题,本文也就是从这几个问题出发,把上面代码实现的过程,整理出来,分享给大家。

  1. 我在浏览器输入相应的地址,怎么就给我映射到了相应的函数中去呢?
  2. http.HandleFunc()函数到底做了什么?
  3. http.ListenAndServe()函数到底做了什么就能实现监听客户端?

  下面来一一整理

二、http.HandleFunc()函数到底做了什么?

  http.HandleFunc()函数到底做了什么?我们来具体看一下该函数的实现过程,这个函数大约在net/http的server.go的2422行左右
图1
  这里出现DefaultServeMux是什么?我们再深入看看。
图2
  发现 DefaultServeMux 是一个全局变量,是一个 指向ServeMux的指针,ServeMux是一个呢?源码是这么解释的
图3serveMux解释
  基本上理解第一段落就好:ServeMux是一个路由器,它根据已注册的路径(pattren)列表匹配每个传入请求的URL,并调用与URL最匹配的路径(pattern)的处理程序(handler)。例如下文中的"/helloworld"就是pattern,"myhandler"就是handler

http.HandleFunc("/helloWorld", myhandler)

  知道了 DefaultServeMux 是一个 指向ServeMux的指针,那么DefaultServeMux.HandleFunc()就是调用ServeMux的成员方法HandleFunc()
图4
  这里有个比较重要的类型HandlerFunc,由下图可以看到它是一个func(ResponseWriter, *Request)类型,这个类型还有一个类型方法ServeHTTP()。
  ServeHTTP()至关重要,如果同学用过beego框架,源码也有会实现这个方法。关于类型方法的理解可以看《go语言编程》的第三章第一节-为类型添加方法,也可以看我写的type关键字用法一文。
  HandlerFunc(handler)就是将handler,也就是myhandler转化为func(ResponseWriter, *Request)类型
  注:需要《go语言编程》资源的同学可以留言,给你网盘链接,资源仅供学习交流用。
图5
  好了,类型HandlerFunc了解完后,我们回到主干上,ServeMux的成员方法HandleFunc()里还调用了Handle()函数。
图6
  注意了,此函数有两个参数,第一个是pattern,也就是路径。第二个是接口类型Handler,如下图
图7
  ServeMux的Handle()函数是一个核心函数,下文的这句代码实现了将你想要登记的’("/helloWorld", myhandler)'加到DefaultServeMux这个全局变量中去,形成一个路由器,或者一张路由表(muxEntry),然后等待请求的到来,来一个就到表里面查找(根据pattern查找),找到相应的pattrn就执行相应的处理逻辑,也就是执行handler,关于怎么查找后文会提到。

mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

  下面来看一张图,大概就理解了。
图8
  不妨我们看一下,pattern与handler是怎么加上去的,每执行一次“http.HandleFunc("/helloWorld", myhandler)”就会执行下图中的函数,我们打印出来看一下(这里我只想注册两个pattern,所以也就打印出来两次)
图9
  上图我们已经可以看到全局变量DefaultServeMux已经有了两条路由规则。我们再回顾一下DefaultServeMux的结构,
图2
  总结:
    http.HandleFunc()将每一条路由规则添加到DefaultServeMux这个全局变量中,形成一张可查询的路由表供请求使用。

三、http.ListenAndServe()函数到底做了什么

  http.ListenAndServe()函数到底做了什么?我们来具体看一下该函数的实现过程,这个函数大约在net/http的server.go的3019行左右
图10
  从上图可以看到,利用传进来的“9090”端口(handler为nil)创建了一个Server实例,接着调用实例方法ListenAndServe(),那么来看看Server是怎么定义的(一部分,完整的自行查看)。可以看到Server结构体里的Handler是接口类型,跟上文提到的一样
图11
   再来理解实例方法ListenAndServe()做了什么?下图,net.Listen()返回一个listener类型以监听客户端的连接,也就是继续调用实例方法Serve()(这里实例方法不停调用,看得贼难受)。怎么监听请求的连接?熟悉socket通信的都知道,无非就是开个循环不停的accept(),有请求过来就继续代码逻辑,没有就干等,阻塞等待。那看看Serve()是不是真的这么干?
图12
   下文是Serve()函数的源码。Serve()函数也是一个核心函数,看到for循环没?这是个死循环,通过“rw, e := l.Accept()”这句代码进行阻塞,这样又回到socket通信的原理。
  值得注意的是在这个函数的最后有一句“go c.serve(ctx)”,这样就专门为这一次的连接开一个goroutine去处理请求,映射到相应的处理逻辑中去。这句代码也充分体现了golang在语言层面上就具有高并发的特性

func (srv *Server) Serve(l net.Listener) error {
	if fn := testHookServerServe; fn != nil {
		fn(srv, l) // call hook with unwrapped listener
	}

	l = &onceCloseListener{Listener: l}
	defer l.Close()

	if err := srv.setupHTTP2_Serve(); err != nil {
		return err
	}

	if !srv.trackListener(&l, true) {
		return ErrServerClosed
	}
	defer srv.trackListener(&l, false)

	var tempDelay time.Duration     // how long to sleep on accept failure
	baseCtx := context.Background() // base is always background, per Issue 16220
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, e := l.Accept() // 等待连接---------------------
		if e != nil {
			select {
			case <-srv.getDoneChan():
				return ErrServerClosed
			default:
			}
			if ne, ok := e.(net.Error); ok && ne.Temporary() {
				if tempDelay == 0 {
					tempDelay = 5 * time.Millisecond
				} else {
					tempDelay *= 2
				}
				if max := 1 * time.Second; tempDelay > max {
					tempDelay = max
				}
				srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
				time.Sleep(tempDelay)
				continue
			}
			return e
		}
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew) // before Serve can return
		go c.serve(ctx) // 处理请求--------------------------
	}
}

  总结:
    http.ListenAndServe()函数总的来说做了两件大事,一是不停监听请求;二是对每一个请求开一个goroutine去处理,去比对路由表,映射到相应的处理逻辑。

四、在浏览器输入相应的地址,怎么就给我映射到了相应的函数中去呢?

  从第三节的最后我们了解到对每一次的请求就开一个goroutine去处理

go c.serve(ctx) 

   由于c.serve(ctx)函数太长,我就不一一post上来了,看主要的逻辑思路就好。创建一个serverHandler实例,由下面的图片可以看出这个serverHandler实例的srv成员根本就是我们一开始就创建的Server实例啊,见第三节中的第一张图片。紧接着就是调用了ServeHTTP(w, w.req)方法。

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
	... ...
	for {
		... ...
		// c.server就是 Server{Addr: addr, Handler: handler} handler为nil
		// serverHandler{c.server}创建了一个实例
		serverHandler{c.server}.ServeHTTP(w, w.req)
		... ...
		}

图13
   真正的重头戏来了,下图的“ handler = DefaultServeMux ”是关键的代码,记得吗? DefaultServeMux是全局变量,它指向ServeMux路由器,ServeMux里有muxEntry,muxEntry是注册了很多路由实例,只要把请求进来的pattern去比对muxEntry里的pattern就可以把请求映射到相应的处理函数中去,忘了这几者的定义可以回到第二节的第二张图或者倒数第三张图。接下来“ handler.ServeHTTP(rw, req) ”就是描述怎么通过请求的pattern查找已经在DefaultServeMux注册好的pattern,从而映射到相应的处理函数(handler)中去。
在这里插入图片描述
   上图的“ hander.ServeHTTP(rw, req) ”实际上就是调用了ServeMux的成员方法ServeHTTP()如下图。在成员函数中又调用了另外一个成员函数Handler()得到" h “,” h “是什么?” h “就是handler啊!是全局变量DefaultServeMux里已经注册好的路由表里将要映射到处理逻辑的函数,是muxEntry的成员” h “, 它是一个接口类型,就是我们的” myhandler “。在这里插入图片描述
   Handler()这个函数很简单,只是把请求的host、pattern拿出来,再放到另外一个成员函数handler()中。本文的例子,直接了解最后一句代码就可以,其他的代码无非就是加了一些其他配置后走的逻辑。
在这里插入图片描述
  handler()这个函数是一个中转函数,它拿着host、pattern按照条件去路由表里对比,找到映射的处理逻辑,本文是走第二个if语句的逻辑。
在这里插入图片描述
  match()函数,毫无疑问就是核心函数了,它描述了如何去根据请求的pattern去匹配到相应的处理逻辑handler,本文例子,for语句块的代码逻辑可以忽略。
在这里插入图片描述
  通过match()函数,我们知道根据请求的pattern去路由表里查找相应的handler,也就是该请求相应的处理函数。找到的处理函数怎么让它生效?我们回头看看
在这里插入图片描述
  上图中拿到的"h"也就是处理请求的函数名,然后怎么实现调用这个函数?文章就在” h.ServeHTTP(w, r) "里,这句代码充分体现了接口的灵活性,它是怎么调用的呢?答案在下两张图
图7
图5
  其实就相当于下图的代码,这里可能会有点绕,建议补一下 接口与type关键字定义的类型 这一部分内容
在这里插入图片描述

五、最后

  这边文章其实是我看beego框架源码时的附属品,这对我理解beego源码时帮助很大。
  看源码是一个极为费精神,并且考验耐心的过程,往往代码量上来之后常常看了下文忘了上文,有时觉得代码的编排极为混乱(毕竟不是自己写的,不了解那些大神的思路)。但是,看源码也是一个极为享受的过程,可以帮助自己查漏补缺,学到更多的不同写法,学到更好的编程思想。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈家大耳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值