脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 一文详解Golang中net/http包的实现原理

一文详解Golang中net/http包的实现原理

2022-08-03 20:241个俗人 Golang

这篇文章主要介绍了如何用 net/http 自己编写实现一个 HTTP Server 并探究其实现原理,具体讲解Go语言是如何接收和处理请求的,希望能够对大家的学习或工作具有一定的帮助

前言

Go语言自带的net/http包提供了HTTP客户端和服务端的实现,实现一个简单的http服务非常容易,其自带了一些列结构和方法来帮助开发者简化HTTP服务开发的相关流程,因此我们不需要依赖任何第三方组件就能构建并启动一个高并发的HTTP服务器,net/http包在编写web应用中有很重要的作用,这篇文章会学习如何用 net/http 自己编写实现一个 HTTP Server 并探究其实现原理,具体讲解Go语言是如何接收和处理请求的,希望能够对大家的学习或工作具有一定的帮助,需要的朋友可以参考下。

http包执行流程

net/http包的web工作原理大致就是如下四个部分组成了:端口监听、请求解析、路由分配、响应处理。更加代码化的说就是:创建 ServerSocket, 绑定并listen,accept连接,创建go协程服务一个连接。

一文详解Golang中net/http包的实现原理

具体流程如下:

  • 服务端创建Listen Socket,绑定并监听指定的IP地址和端口,等待客户端请求到来; ListenAndServer(...)
  • 客户端与Listen Socket连接,确认请求后,客户端得到Client Socket,客户端通过这个与Listen Socket 通信;srv.Server(...)
  • 服务端创建一个协程来处理客户端请求,首先从Client Socket读取Http请求的协议头和数据,交给响应的Handler处理,
  • Handler处理完毕后,结果通过Client Socket写给客户端。c.server(...)

http包源码分析

在上面的流程中中,起关键作用的函数为http.ListenAndServe,Go就是利用这个方法,实现了web服务中的端口监听、请求解析、路由分配、响应处理四大功能。

端口监听

服务器中的http.ListenAndServer()方法启动服务:

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

生成Server结构体,调用其方法ListenAndServer

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
}

net.Listen来进行对地址的监听,返回一个listener,传递给server.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
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
func (srv *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
        fn(srv, l) // call hook with unwrapped listener
    }
 
    origListener := l
    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)
 
    baseCtx := context.Background()
    if srv.BaseContext != nil {
        baseCtx = srv.BaseContext(origListener)
        if baseCtx == nil {
            panic("BaseContext returned a nil context")
        }
    }
 
    var tempDelay time.Duration // how long to sleep on accept failure
 
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for { //循环逻辑,接收请求处理
          //有新的连接
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := err.(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", err, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        //创建新的连接
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        //启动新的goroutine进行处理
        go c.serve(connCtx)
    }
}

主要进行了两项工作,一项是在for死循环里面,通过l.Accept()来接受请求返回连接Conn,并处理相关的报错,根据该连接创建对应的goroutine来执行conn.serve(ctx),各个请求之间是相互不影响的,提高并发性能。 另一项工作是新建context用于管理这些生成的goroutinecontext作为参数被传入;

请求解析

?
1
2
3
4
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    // ...
}

函数代码比较多,就不贴出来了,具体可以到/net/http/server.go文件中查看,其中使用了defer func() {...}()匿名函数来实现错误输入和恢复、连接关闭;

该方法用于服务一个新的连接。首先,通过readRequest来读取数据,解析请求中的HeaderBody和一些校验。接着,使用serverHandler方法来处理request和response。 主要功能实现是serverHandler{c.server}.ServeHTTP(w, w.req),其构造serverHandler类型,并调用ServeHTTP方法;值得注意的是,这里不允许服务器同时处理多个请求;

路由分配

使用者注册一个HandlerFunc,被转为Handler

?
1
2
3
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

ServeMux的Handle方法,将会对pattern和Handler做map映射。

handler会创建一个锁,同时调用match方法返回一个Handler和pattern。

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

响应处理

其实就是如何根据路由,找到对应的handler。

在match方法中,对于handler,优先查找m表,若不能找到结果,则在es表中进行匹配,路径长的优先匹配。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}

服务端处理完请求后,处理器可以通过ResponseWriter接口创建HTTP响应,ResponseWriter接口具有如下三个方法:

?
1
2
3
4
5
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

到此这篇关于一文详解Golang中net/http包的实现原理的文章就介绍到这了,更多相关Golang net http包内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://juejin.cn/post/7127535913483108360

延伸 · 阅读

精彩推荐
  • Golanggolang 设置web请求状态码操作

    golang 设置web请求状态码操作

    这篇文章主要介绍了golang 设置web请求状态码操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    一名路过的小码农10582021-02-25
  • GolangGo 语言的函数与指针

    Go 语言的函数与指针

    函数就是一段基本的代码块,一般用来对需要重复执行的代码进行复用。在 go 中,函数是『一等公民』,这与 js 类似,也就是可以将函数当做一个变量进...

    自然醒的笔记本11042021-04-14
  • Golang使用go xorm来操作mysql的方法实例

    使用go xorm来操作mysql的方法实例

    今天小编就为大家分享一篇关于使用go xorm来操作mysql的方法实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小...

    stpeace6582020-05-24
  • Golanggo语言文件正则表达式搜索功能示例

    go语言文件正则表达式搜索功能示例

    这篇文章主要介绍了go语言文件正则表达式搜索功能,涉及Go语言文件目录的遍历及正则操作相关技巧,需要的朋友可以参考下 ...

    aotian164082020-05-04
  • Golang在go语言中安装与使用protobuf的方法详解

    在go语言中安装与使用protobuf的方法详解

    protobuf以前只支持C++, Python和Java等语言, Go语言出来后, 作为亲儿子, 那有不支持的道理呢? 这篇文章主要给大家介绍了关于在go语言中使用protobuf的相关资料,...

    Dr_Zhang14082020-05-08
  • GolangGolang实现字符串倒序的几种解决方案

    Golang实现字符串倒序的几种解决方案

    给定一个字符串,按单词将该字符串逆序是我们大家在开发中可能会遇到的一个需求,所以下面这篇文章主要给大家介绍了关于Golang如何实现字符串倒序的几...

    小立立4212020-05-09
  • Golanggolang1.16新特性速览(推荐)

    golang1.16新特性速览(推荐)

    这篇文章主要介绍了golang1.16新特性速览,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...

    apocelipes6672021-03-29
  • Golanggolang 实现每隔几分钟执行一个函数

    golang 实现每隔几分钟执行一个函数

    这篇文章主要介绍了golang 实现每隔几分钟执行一个函数,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    chenchongswjtu13842021-03-14