图文吃透Golang net/http 标准库--服务端

前言

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

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

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

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

Server启动示例

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

go 复制代码
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方法来发起请求,这里同样也举一个简单例子:

css 复制代码
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结构体

go 复制代码
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结构体

go 复制代码
type ServeMux struct {
    mu sync.RWMutex			
    m map[string]muxEntry	
    es []muxEntry 			
    hosts bool 				
}

字段说明:

  • sync.RWMutex:这是读写互斥锁,允许goroutine 并发读取路由表,在修改路由map时独占
  • mapstringmuxEntry:map结构维护pattern (路由) 到 handler (处理函数) 的映射关系,精准匹配
  • \[\]muxEntry:存储 "/" 结尾的路由,切片内按从最长到最短的顺序排列,用作模糊匹配patter的muxEntry
  • hosts:是否有任何模式包含主机名

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

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

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

路由注册

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

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

go 复制代码
var DefaultServeMux = &defaultServeMux
// 默认的ServeMux
var defaultServeMux ServeMux

// HandleFunc注册函数
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMux是ServeMux的默认实例。

scss 复制代码
//接口
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

go 复制代码
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对路由按从长到短进行排序。

🚩 注:

mapstringmuxEntry 的map使用哈希表是用于路由精确匹配

\[\]muxEntry用于部分匹配模式

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

监听和服务启动

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

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

ListenAndServe启动服务:

go 复制代码
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:

go 复制代码
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 方法负责处理
go 复制代码
type onceCloseListener struct {
	net.Listener
	once     sync.Once
	closeErr error
}

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

*conn.serve():

scss 复制代码
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) 其实就是下面函数的实现。

ini 复制代码
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

scss 复制代码
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():

scss 复制代码
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 字段中保存的模糊匹配路由。

go 复制代码
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 方法进行请求的处理和响应,而这个被调用的函数就是我们之前在路由注册时对应的函数。

scss 复制代码
type HandlerFunc func(ResponseWriter, *Request)

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

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

关于Client篇的文章会马上更新,敬请期待,希望看完之后会对net/http有个新认识!

安利一波:

欢迎朋友们关注我的公众号📢📢:【小许code】🤣🤣
欢迎点赞 👍、收藏 💙、关注 💡 三连支持一下~🎈

🎈知道的越多,不知道的也越多,我是小许,下期见~🙇💻

相关推荐
whyfail26 分钟前
前端学 Spring Boot(2):一次点击,如何穿过整个后端?
前端·spring boot·后端
程序员cxuan2 小时前
Loop 还没玩明白,Graph Engineering 又火了。
人工智能·后端·程序员
uzong3 小时前
把一面面试总结做成一个 Skill:重复的事,就别每次都重写
后端·面试
程序员海军4 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
前端·后端·程序员
不能放弃治疗4 小时前
MCP
后端
杏花春雨江南4 小时前
JavaWeb HTTP长连接完整梳理
网络·网络协议·http
江湖十年5 小时前
Go 语言中 YAML to JSON 踩坑笔记
后端·go·json
你驴我5 小时前
WhatsApp 消息撤回与编辑的幂等性设计实践
java·服务器·前端·后端·python
一缕清烟在人间6 小时前
HarmonyOS开发实战:小分享-TextEditPage文字编辑器——Header+TextArea+工具栏
后端·华为·harmonyos·鸿蒙
人间凡尔赛6 小时前
K8s 十年之变:从 Cloud Native 到 AI Native,2026 年后端架构的范式革命
后端·云原生·架构