当前位置: 代码网 > it编程>前端脚本>Golang > 一文带你吃透Golang中net/http标准库服务端

一文带你吃透Golang中net/http标准库服务端

2024年05月15日 Golang 我要评论
前言今天分享下go语言net/http标准库的实现逻辑,文章将从客户端(client)--服务端(server)两个方向作为切入点,进而一步步分析http标准库内部是如何运作的。由于会涉及到不少的代码

前言

今天分享下go语言net/http标准库的实现逻辑,文章将从客户端(client)--服务端(server)两个方向作为切入点,进而一步步分析http标准库内部是如何运作的。

由于会涉及到不少的代码流程的走读,写完后觉得放在一篇文章中会过于长,可能在阅读感受上会不算很好,因此分为【server--client两个篇文章】进行发布。

本文内容是【服务端server部分】,文章代码版本是golang 1.19,文中会涉及较多的代码,需要耐心阅读,不过我会在尽量将注释也逻辑阐述清楚。先看下所有内容的大纲:

go 语言的 net/http 中同时封装好了 http 客户端和服务端的实现,这里分别举一个简单的使用示例。

server启动示例

server和client端的代码实现来自net/http标准库的文档,都是简单的使用,而且用很少的代码就可以启动一个服务!

http.handlefunc("/hello", func(w http.responsewriter, r *http.request) {
    fmt.fprintf(w, "xiaoxu code")
})
http.listenandserve(":8080", nil)

上面代码中:

handlefunc 方法注册了一个请求路径 /hello 的 handler 函数

listenandserve指定了8080端口进行监听和启动一个http服务端

client发送请求示例

http 包一样可以发送请求,我们以get方法来发起请求,这里同样也举一个简单例子:

resp, err := http.get("http://example.com/")
if err != nil {
    fmt.println(err)
    return
}
defer resp.body.close()
body, _ := ioutil.readall(resp.body)
fmt.println(string(body))

是不是感觉使用起来还是很简单的,短短几行代码就完成了http服务的启动和发送http请求,其背后是如何进行封装的,在接下的章节会讲清楚!

服务端 server

我们先预览下图过程,对整个服务端做的事情有个了解

从图中大致可以看出主要有这些流程:

1. 注册handler到map中,map的key是键值路由

2. handler注册完之后就开启循环监听,监听到一个连接就会异步创建一个 goroutine

3. 在创建好的 goroutine 内部会循环的等待接收请求数据

4. 接受到请求后,根据请求的地址去处理器路由表map中匹配对应的handler,然后执行handler

server结构体

type server struct {
    addr string
    handler handler 
    mu         sync.mutex
    readtimeout time.duration
    writetimeout time.duration
    idletimeout time.duration
    tlsconfig *tls.config
    connstate func(net.conn, connstate)
    activeconn map[*conn]struct{}
    donechan   chan struct{}
    listeners  map[*net.listener]struct{}
    ...
}

我们在下图中解释了部分字段代表的意思

servemux结构体

type servemux struct {
    mu sync.rwmutex   
    m map[string]muxentry 
    es []muxentry    
    hosts bool     
}

字段说明:

• sync.rwmutex:这是读写互斥锁,允许goroutine 并发读取路由表,在修改路由map时独占

• map[string]muxentry:map结构维护pattern (路由) 到 handler (处理函数) 的映射关系,精准匹配

• []muxentry:存储 "/" 结尾的路由,切片内按从最长到最短的顺序排列,用作模糊匹配patter的muxentry

• hosts:是否有任何模式包含主机名

mux是【多路复用器】的意思,servemux就是服务端路由http请求的多路复用器。

作用: 管理和处理程序来处理传入的http请求

原理:内部通过一个 map类型 维护了从 pattern (路由) 到 handler (处理函数) 的映射关系,收到请求后根据路径匹配找到对应的处理函数handler,处理函数进行逻辑处理。

路由注册

通过对handlefunc的调用追踪,内部的调用核心实现如下:

了解完流程之后接下来继续追函数看代码

var defaultservemux = &defaultservemux
// 默认的servemux
var defaultservemux servemux

// handlefunc注册函数
func handlefunc(pattern string, handler func(responsewriter, *request)) {
    defaultservemux.handlefunc(pattern, handler)
}

defaultservemux是servemux的默认实例。

//接口
type handler interface {
    servehttp(responsewriter, *request)
}

//handlerfunc为函数类型
type handlerfunc func(responsewriter, *request)
//实现了handler接口
func (f handlerfunc) servehttp(w responsewriter, r *request) {
    f(w, r)
}


func (mux *servemux) handlefunc(pattern string, handler func(responsewriter, *request)) {
    ...
    // handler是真正处理请求的函数
    mux.handle(pattern, handlerfunc(handler))
}

handlerfunc函数类型是一个适配器,是handler接口的具体实现类型,因为它实现了servehttp方法。

handlerfunc(handler), 通过类型转换的方式【handler -->handlerfunc】将一个出入参形式为func(responsewriter, *request)的函数转换为handlerfunc类型,而handlerfunc实现了handler接口,所以这个被转换的函数handler可以被当做一个handler对象进行赋值。

好处:handlerfunc(handler)方式实现灵活的路由功能,方便的将普通函数转换为http处理程序,兼容注册不同具体的业务逻辑的处理请求。

你看,mux.handle的第二个参数handler就是个接口,servemux.handle就是路由模式和处理函数在map中进行关系映射。

servemux.handle

func (mux *servemux) handle(pattern string, handler handler) {
    mux.mu.lock()
    defer mux.mu.unlock()
    // 检查路由和处理函数
    ...
    //检查pattern是否存在
    ...
    //如果 mux.m 为nil 进行make初始化 map
    if mux.m == nil {
        mux.m = make(map[string]muxentry)
    }
    e := muxentry{h: handler, pattern: pattern}
    //注册好路由都会存放到mux.m里面
    mux.m[pattern] = e
    //patterm以'/'结尾
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendsorted(mux.es, e)
    }

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

handle的实现主要是将传进来的pattern和handler保存在muxentry结构中,然后将pattern作为key,把muxentry添加到defaultservemux的map里。

如果路由表达式以 '/' 结尾,则将对应的muxentry对象加入到[]muxentry切片中,然后通过appendsorted对路由按从长到短进行排序。

注:

  • map[string]muxentry 的map使用哈希表是用于路由精确匹配
  • []muxentry用于部分匹配模式

到这里就完成了路由和handle的绑定注册了,至于为什么分了两个模式,在后面会说到,接下来就是启动服务进行监听的过程。

监听和服务启动

同样的我用图的方式监听和服务启动的函数调用链路画出来,让大家先有个印象。

结合图会对后续结合代码逻辑更清晰,知道这块代码调用属于哪个阶段!

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

net.listen 实现了tcp协议上监听本地的端口8080 (listenandserve()中传过来的),server.serve接受 net.listener实例传入,然后为每个连接创建一个新的服务goroutine

使用net.listen函数实现网络监听需要经过以下几个步骤:

1. 调用net.listen函数,指定网络类型和监听地址。

2. 使用listener.accept函数接受客户端的连接请求。

3. 在一个独立的goroutine中处理每个连接。

4. 在处理完连接后,调用conn.close()来关闭连接

server.serve:

func (srv *server) serve(l net.listener) error {
    origlistener := l
    //内部实现once是只执行一次动作的对象
    l = &oncecloselistener{listener: l}
    defer l.close()
    ...
    ctx := context.withvalue(basectx, servercontextkey, srv)
    for {
        //rw为可理解为tcp连接
        rw, err := l.accept()
        ...
        connctx := ctx
        ...
        c := srv.newconn(rw)
        //
        go c.serve(connctx)
    }
}

使用 for + listener.accept 处理客户端请求

• 在for 循环调用 listener.accept 方法循环读取新连接

• 读取到客户端请求后会创建一个 goroutine 异步执行 conn.serve 方法负责处理

type oncecloselistener struct {
    net.listener
    once     sync.once
    closeerr error
}

oncecloselistener 是sync.once的一次执行对象,当且仅当第一次被调用时才执行函数。

*conn.serve():

func (c *conn) serve(ctx context.context) {
    ...
    // 初始化conn的一些参数
    c.remoteaddr = c.rwc.remoteaddr().string()
    c.r = &connreader{conn: c}
    c.bufr = newbufioreader(c.r)
    c.bufw = newbufiowritersize(checkconnerrorwriter{c}, 4<<10)
    for {
        // 读取客户端请求
        w, err := c.readrequest(ctx)
        ...
        // 调用servehttp来处理请求
        serverhandler{c.server}.servehttp(w, w.req)
    }
}

conn.serve是处理客户端连接的核心方法,主要是通过for循环不断循环读取客户端请求,然后根据请求调用相应的处理函数。

c.readrequest(ctx)方法是用来读取客户端的请求,然后返回一个response类型的w和一个错误err

最终是通过serverhandler{c.server}.servehttp(w, w.req) 调用servehttp处理连接客户端发送的请求。

ok,经历了前面监听的过程,现在客户端请求已经拿到了,接下来就是到了核心的处理请求的逻辑了,打起十二分精神哦!

serverhandler.servehttp:

上面说到的 serverhandler{c.server}.servehttp(w, w.req) 其实就是下面函数的实现。

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传的是nil就执行 defaultservemux.servehttp() 方法
    handler.servehttp(rw, req)
}

获取server的handler流程:

1. 先获取 sh.srv.handler 的值,判断是否为nil

2. 如果为nil则取全局单例 defaultservemux这个handler

3. ptions method 请求且 uri 是 *,就使用globaloptionshandler

注:这个handler其实就是在listenandserve()中的第二个参数

servemux.servehttp

func (mux *servemux) servehttp(w responsewriter, r *request) {
    ....
    h, _ := mux.handler(r)
    // 执行匹配到的路由的servehttp方法
    h.servehttp(w, r)
}

servemux.servehttp()方法主要代码可以分为两步:

1. 通过 servermux.handler() 方法获取到匹配的处理函数 h

2. 调用 handler.servehttp() 执行匹配到该路由的函数来处理请求 (h实现了servehttp方法)

servermux.handler():

func (mux *servemux) handler(r *request) (h handler, pattern string) {
    ...
    //在mux.m和mux.es中
    //根据host/url.path寻找对应的handler
    return mux.handler(host, r.url.path)
}

在 servemux.handler() 方法内部,会调用 servermux.handler(host, r.url.path) 方法来查找匹配的处理函数。

servemux.match

servemux.match()方法用于根据给定的具体路径 path 找到最佳匹配的路由,并返回handler和路径。

值得一提的是,如果 mux.m 中不存在 path 完全匹配的路由时,会继续遍历 mux.es 字段中保存的模糊匹配路由。

func (mux *servemux) match(path string) (h handler, pattern string) {
    // 是否完全匹配
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }
    // mux.es是按pattern从长到短排列
    for _, e := range mux.es {
        if strings.hasprefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil, ""
}

最后调用 handler.servehttp 方法进行请求的处理和响应,而这个被调用的函数就是我们之前在路由注册时对应的函数。

type handlerfunc func(responsewriter, *request)

func (f handlerfunc) servehttp(w responsewriter, r *request) {
    f(w, r)
}

到这里整个服务的流程就到这里了,现在有对这块有印象了吗?

以上就是一文带你吃透golang中net/http标准库服务端的详细内容,更多关于go net/http标准库的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com