Eslody

生活在树上-始终热爱大地-升入天空

  • 主页
  • 随笔
所有文章 关于我

Eslody

生活在树上-始终热爱大地-升入天空

  • 主页
  • 随笔

net/http源码阅读

2021-06-04

golang原生库net/http包下有关服务器基本实现的源码阅读

在以前的文章里笔者介绍过Socket通信的一些基础知识:IO多路复用详解

基于这些知识,我们今天从应用开始,一步步深入golang原生http库

路由注册

golang通过以下两种方式实现一个http服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//第一种注册HandleFunc函数

package main

import (
"fmt"
"net/http"
)

func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}

func main () {
http.HandleFunc("/", HelloHandler)
http.ListenAndServe(":8000", nil)
}
//第二种直接注册Handler

package main

import (
"fmt"
"net/http"
)

type HelloHandlerStruct struct {
content string
}

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

func main() {
http.Handle("/", &HelloHandlerStruct{content: "Hello World"})
http.ListenAndServe(":8000", nil)
}

http.HandleFunc和http.Handle都是用于给路由规则指定处理器,http.HandleFunc的第一个参数为路由的匹配规则(pattern)第二个参数是一个签名为func(w http.ResponseWriter, r *http.Requests)的函数。而http.Handle的第二个参数为实现了http.Handler接口的类型的实例。而无论是http.HandleFunc方法还是http.Handle方法最终都会由DefaultServeMux调用Handle方法来完成路由处理器的注册

这里,我们遇到两种类型的对象:ServeMux和Handler

Handler

Handler为实现了ServeHTTP(ResponseWriter, *Request)的接口,这个方法用于实现通信的逻辑处理

而对于http.HandleFunc函数,它通过调用*ServeMux.HandleFunc方法,最终同样调用ServeMux.Handle方法(这个方法我们稍后再说)

1
2
3
4
5
6
7
8
9
10
11
12
13
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
//注意,HandlerFunc是一个实现了ServeHTTP的函数类型
type HandlerFunc func(ResponseWriter, *Request)

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

ServeMux

ServeMux是存储、分配路由服务的服务复用器,它也实现了ServeHTTP()方法,用以启动路由服务,选择匹配度最高的路由处理函数来处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type ServeMux struct {
mu sync.RWMutex //读写锁
m map[string]muxEntry //存储路由对应处理函数
es []muxEntry //用于路由的部分匹配
hosts bool //是否存在带主机名的路由
}

type muxEntry struct {
h Handler //处理函数
pattern string //路由
}

//我们稍后还会在处理连接时遇到这个方法
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)
}

Handle方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
}
//用路由的pattern和处理函数创建 muxEntry 对象
e := muxEntry{h: handler, pattern: pattern}
//向ServeMux的m 字段增加新的路由匹配规则
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
//如果路由pattern以'/'结尾,则将对应的muxEntry对象加入到[]muxEntry中,路由长的位于切片的前面。用于部分匹配
mux.es = appendSorted(mux.es, e)
}
//存在带主机名的路由
if pattern[0] != '/' {
mux.hosts = true
}
}

服务启动

观察以上实现方式我们可以发现,注册路由后,就能通过使用http.ListenAndServe方法启动服务器开始监听指定端口过来的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这里的handler对应ServeMux
//其实gin调用方法Run时最终传向的就是这个方法,但第二个参数却不是nil,而是自定义的engine,用以代替DefaultServeMux
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" //80端口
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}

这里对应socket基础中的listen->accept,Server对象的Serve方法会接收Listener中过来的连接,为每个连接创建一个goroutine,在goroutine中会用路由处理Handler对请求进行处理并构建响应

其中,Serve函数的主要逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
func (srv *Server) Serve(l net.Listener) error {
......
   baseCtx := context.Background() 
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()//接收listener过来的网络连接请求
......
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) //将连接放在Server.activeConn这个map中
go c.serve(ctx)//创建协程处理请求
}
}

自此,连接建立,服务启动

处理连接

接下来我们进入conn.serve函数,这里的conn是一个内部包含Server成员的结构体。核心逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)
}
......
serverHandler{c.server}.ServeHTTP(w, w.req)
//结束response
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))
......
}
}

关键代码为serverHandler{c.server}.ServeHTTP(w, w.req)这一行。serverHandler是代理了Server对象的结构体类型,而这个函数最终是对ServeMux.ServeHTTP()的包装。如果我们最初在http.ListenAndServe()中传入的Handler参数为nil,这里就会使用默认的DefaultServeMux,最后调用其ServeHTTP()方法匹配当前路由对应的handler方法

但请注意:如果要自定义服务复用器,就不能使用http包自带的http.handle方法,因为这个方法本身就是调用DefaultServeMux的路由分配逻辑。所以,一般当框架自定义路由时才会调用这个方法,在第二个参数传自己想要用的服务复用器(如gin中的engine),其已经实现了路由分配的功能。

如下是ServeMux调用ServeHTTP()的逻辑,注释已经写得很详细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
//如果请求路径为"*",告诉浏览器连接已关闭并返回状态码400
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(r *Request) (h Handler, pattern string) {
//重定向
if r.Method == "CONNECT" {
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}

return mux.handler(r.Host, r.URL.Path)
}
//去掉主机名的端口号
host := stripHostPort(r.Host)
//整理URL
path := cleanPath(r.URL.Path)

if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}

if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
//这步才是关键!
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()
//如果当前mux中注册有带主机名的路由,优先进行"主机名+路径"的方式匹配
if mux.hosts {
h, pattern = mux.match(host + path)
}
//如果没有匹配到,就直接匹配路由
if h == nil {
h, pattern = mux.match(path)
}
//404
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
//如果存在路由映射,直接返回
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
//部分匹配:如果是以"/"结尾的就寻找最长匹配路由
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}

所谓部分匹配是指:当不存在匹配的路由时,可以追溯最长的一部分进行路由匹配,这个请求会被导向最长路由的接口

此后,如果想要停止服务,可以使用http.ShutDown()方法实现优雅地关闭,当然,这需要自定义http.server对象进行手动关闭

最后借用一张个人觉得很清晰的网图来总结下整个http服务流程:

pic1

赏

谢谢你请我吃糖果

  • go

扫一扫,分享到微信

微信分享二维码
go unsafe.Pointer和uintptr解析
MapReduce的一些改进
  1. 1. 路由注册
    1. 1.1. Handler
    2. 1.2. ServeMux
    3. 1.3. Handle方法
  2. 2. 服务启动
  3. 3. 处理连接
© 2021 eslody
To be or not to be.
  • 所有文章
  • 关于我

tag:

  • 数据结构与算法
  • java
  • Linux
  • 操作系统
  • 计算机网络
  • 区块链
  • go
  • 工具
  • 分布式
  • 实习记录
  • 随笔
  • http
  • RPC
  • Gin

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • QUIC协议原理浅析

    2021-07-11

    #计算机网络#http

  • 基数树(Radix tree)和前缀树(Trie tree)

    2021-07-10

    #Gin

  • 转发://http2.0多路复用

    2021-07-08

    #http

  • bufio包原理解析

    2021-07-08

    #go

  • 为什么response.Body.Close()是必要的

    2021-06-29

    #go

  • 记一次代码优化的经历

    2021-06-25

    #实习记录#随笔

  • syn/singleflight源码阅读

    2021-06-21

    #go

  • 转发://深入理解Golang

    2021-06-12

    #go

  • go unsafe.Pointer和uintptr解析

    2021-06-09

    #go

  • net/http源码阅读

    2021-06-04

    #go

  • MapReduce的一些改进

    2021-05-25

    #go#分布式

  • 转发://推荐几个巨强的大佬博客(持续更新)

    2021-05-15

  • RPC项目的流程图

    2021-05-14

    #RPC

  • 关于快速选择的尾递归

    2021-03-25

    #数据结构与算法

  • 实现一个MapReduce

    2021-03-22

    #go#分布式

  • 1~n整数中1出现的次数

    2021-03-09

    #数据结构与算法

  • 一些有关快排的小问题

    2021-03-06

    #数据结构与算法

  • IO多路复用详解

    2021-03-05

    #Linux#操作系统#计算机网络

  • 聊聊虚拟内存

    2021-03-04

    #操作系统

  • 正则表达式(Regular Expression)

    2021-03-03

    #工具

  • go指南——练习:web爬虫

    2021-03-02

    #go

  • printf执行原理

    2021-02-24

    #操作系统

  • SYN洪泛攻击原理及防御

    2021-02-23

    #计算机网络

  • Arrays.asList基本用法

    2020-12-12

    #java

  • 从暴力递归到动态规划(二)

    2020-12-11

    #数据结构与算法

  • 从暴力递归到动态规划(一)

    2020-12-11

    #数据结构与算法

  • 线段树(区间修改树)

    2020-12-08

    #数据结构与算法

  • 理解https

    2020-11-18

    #计算机网络

  • Solidity实现以太坊秘密竞拍智能合约

    2020-10-03

    #区块链

热爱跳舞

永远是学徒