设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间01 现在时间01 缓存数据 当你走完一段之后回头看,你会发现,那些真正能被记得的事真的是没有多少,真正无法忘记的人屈指可数,真正有趣的日子不过是那么一些,而真正需要害怕的也是寥寥无几。

当你走完一段之后回头看,你会发现,那些真正能被记得的事真的是没有多少,真正无法忘记的人屈指可数,真正有趣的日子不过是那么一些,而真正需要害怕的也是寥寥无几。

查看: 1975|回复: 3

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

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:204
  • 打卡月天数:0
  • 打卡总奖励:3167
  • 最近打卡:2023-08-27 09:22:23
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
434
主题
406
精华
0
金钱
4462
积分
870
注册时间
2022-12-23
最后登录
2025-9-25

发表于 2024-4-24 01:31:32 | 显示全部楼层 |阅读模式
目录


  • 前言

    • Server启动示例
    • Client发送请求示例

  • 服务端 Server

    • Server结构体
    • ServeMux结构体
    • 路由注册
    • 监听和服务启动


前言

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

由于会涉及到不少的代码流程的走读,写完后觉得放在一篇文章中会过于长,可能在阅读感受上会不算很好,因此分为【Server--Client两个篇文章】进行发布。
本文内容是【服务端Server部分】,文章代码版本是Golang 1.19,文中会涉及较多的代码,需要耐心阅读,不过我会在尽量将注释也逻辑阐述清楚。先看下所有内容的大纲:
2.png

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

Server启动示例

Server和Client端的代码实现来自net/http标准库的文档,都是简单的使用,而且用很少的代码就可以启动一个服务!
  1. http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
  2.     fmt.Fprintf(w, "xiaoxu code")
  3. })
  4. http.ListenAndServe(":8080", nil)
复制代码
上面代码中:
HandleFunc 方法注册了一个请求路径 /hello 的 handler 函数
ListenAndServe指定了8080端口进行监听和启动一个HTTP服务端

Client发送请求示例

HTTP 包一样可以发送请求,我们以Get方法来发起请求,这里同样也举一个简单例子:
  1. resp, err := http.Get("http://example.com/")
  2. if err != nil {
  3.     fmt.Println(err)
  4.     return
  5. }
  6. defer resp.Body.Close()
  7. body, _ := ioutil.ReadAll(resp.Body)
  8. fmt.Println(string(body))
复制代码
是不是感觉使用起来还是很简单的,短短几行代码就完成了http服务的启动和发送http请求,其背后是如何进行封装的,在接下的章节会讲清楚!

服务端 Server

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

从图中大致可以看出主要有这些流程:
1. 注册handler到map中,map的key是键值路由
2. handler注册完之后就开启循环监听,监听到一个连接就会异步创建一个 Goroutine
3. 在创建好的 Goroutine 内部会循环的等待接收请求数据
4. 接受到请求后,根据请求的地址去处理器路由表map中匹配对应的handler,然后执行handler

Server结构体
  1. type Server struct {
  2.     Addr string
  3.     Handler Handler
  4.     mu         sync.Mutex
  5.     ReadTimeout time.Duration
  6.     WriteTimeout time.Duration
  7.     IdleTimeout time.Duration
  8.     TLSConfig *tls.Config
  9.     ConnState func(net.Conn, ConnState)
  10.     activeConn map[*conn]struct{}
  11.     doneChan   chan struct{}
  12.     listeners  map[*net.Listener]struct{}
  13.     ...
  14. }
复制代码
我们在下图中解释了部分字段代表的意思
4.png


ServeMux结构体
  1. type ServeMux struct {
  2.     mu sync.RWMutex   
  3.     m map[string]muxEntry
  4.     es []muxEntry   
  5.     hosts bool     
  6. }
复制代码
字段说明:
• sync.RWMutex:这是读写互斥锁,允许goroutine 并发读取路由表,在修改路由map时独占
• map[string]muxEntry:map结构维护pattern (路由) 到 handler (处理函数) 的映射关系,精准匹配
• []muxEntry:存储 "/" 结尾的路由,切片内按从最长到最短的顺序排列,用作模糊匹配patter的muxEntry
• hosts:是否有任何模式包含主机名
Mux是【多路复用器】的意思,ServeMux就是服务端路由http请求的多路复用器。
作用: 管理和处理程序来处理传入的HTTP请求
原理:内部通过一个 map类型 维护了从 pattern (路由) 到 handler (处理函数) 的映射关系,收到请求后根据路径匹配找到对应的处理函数handler,处理函数进行逻辑处理。
5.png


路由注册

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

了解完流程之后接下来继续追函数看代码
  1. var DefaultServeMux = &defaultServeMux
  2. // 默认的ServeMux
  3. var defaultServeMux ServeMux

  4. // HandleFunc注册函数
  5. func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  6.     DefaultServeMux.HandleFunc(pattern, handler)
  7. }
复制代码
DefaultServeMux是ServeMux的默认实例。
  1. //接口
  2. type Handler interface {
  3.     ServeHTTP(ResponseWriter, *Request)
  4. }

  5. //HandlerFunc为函数类型
  6. type HandlerFunc func(ResponseWriter, *Request)
  7. //实现了Handler接口
  8. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  9.     f(w, r)
  10. }


  11. func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  12.     ...
  13.     // handler是真正处理请求的函数
  14.     mux.Handle(pattern, HandlerFunc(handler))
  15. }
复制代码
HandlerFunc函数类型是一个适配器,是Handler接口的具体实现类型,因为它实现了ServeHTTP方法。
  1. HandlerFunc(handler), 通过类型转换的方式【handler -->HandlerFunc】将一个出入参形式为func(ResponseWriter, *Request)的函数转换为HandlerFunc类型,而HandlerFunc实现了Handler接口,所以这个被转换的函数handler可以被当做一个Handler对象进行赋值。
复制代码
好处:HandlerFunc(handler)方式实现灵活的路由功能,方便的将普通函数转换为Http处理程序,兼容注册不同具体的业务逻辑的处理请求。
你看,mux.Handle的第二个参数Handler就是个接口,ServeMux.Handle就是路由模式和处理函数在map中进行关系映射。
ServeMux.Handle
  1. func (mux *ServeMux) Handle(pattern string, handler Handler) {
  2.     mux.mu.Lock()
  3.     defer mux.mu.Unlock()
  4.     // 检查路由和处理函数
  5.     ...
  6.     //检查pattern是否存在
  7.     ...
  8.     //如果 mux.m 为nil 进行make初始化 map
  9.     if mux.m == nil {
  10.         mux.m = make(map[string]muxEntry)
  11.     }
  12.     e := muxEntry{h: handler, pattern: pattern}
  13.     //注册好路由都会存放到mux.m里面
  14.     mux.m[pattern] = e
  15.     //patterm以'/'结尾
  16.     if pattern[len(pattern)-1] == '/' {
  17.         mux.es = appendSorted(mux.es, e)
  18.     }

  19.     if pattern[0] != '/' {
  20.         mux.hosts = true
  21.     }
  22. }
复制代码
Handle的实现主要是将传进来的pattern和handler保存在muxEntry结构中,然后将pattern作为key,把muxEntry添加到DefaultServeMux的Map里。
如果路由表达式以 '/' 结尾,则将对应的muxEntry对象加入到[]muxEntry切片中,然后通过appendSorted对路由按从长到短进行排序。
注:

  • map[string]muxEntry 的map使用哈希表是用于路由精确匹配
  • []muxEntry用于部分匹配模式
到这里就完成了路由和handle的绑定注册了,至于为什么分了两个模式,在后面会说到,接下来就是启动服务进行监听的过程。

监听和服务启动

同样的我用图的方式监听和服务启动的函数调用链路画出来,让大家先有个印象。
结合图会对后续结合代码逻辑更清晰,知道这块代码调用属于哪个阶段!
7.png

ListenAndServe启动服务:
  1. func (srv *Server) ListenAndServe() error {
  2.     if srv.shuttingDown() {
  3.         return ErrServerClosed
  4.     }
  5.     addr := srv.Addr
  6.     if addr == "" {
  7.         addr = ":http"
  8.     }
  9.     // 指定网络地址并监听
  10.     ln, err := net.Listen("tcp", addr)
  11.     if err != nil {
  12.         return err
  13.     }
  14.     // 接收处理请求
  15.     return srv.Serve(ln)
  16. }
复制代码
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:
  1. func (srv *Server) Serve(l net.Listener) error {
  2.     origListener := l
  3.     //内部实现Once是只执行一次动作的对象
  4.     l = &onceCloseListener{Listener: l}
  5.     defer l.Close()
  6.     ...
  7.     ctx := context.WithValue(baseCtx, ServerContextKey, srv)
  8.     for {
  9.         //rw为可理解为tcp连接
  10.         rw, err := l.Accept()
  11.         ...
  12.         connCtx := ctx
  13.         ...
  14.         c := srv.newConn(rw)
  15.         //
  16.         go c.serve(connCtx)
  17.     }
  18. }
复制代码
使用 for + listener.accept 处理客户端请求
• 在for 循环调用 Listener.Accept 方法循环读取新连接
• 读取到客户端请求后会创建一个 goroutine 异步执行 conn.serve 方法负责处理
  1. type onceCloseListener struct {
  2.     net.Listener
  3.     once     sync.Once
  4.     closeErr error
  5. }
复制代码
  1. onceCloseListener 是sync.Once的一次执行对象,当且仅当第一次被调用时才执行函数。
复制代码
*conn.serve():
  1. func (c *conn) serve(ctx context.Context) {
  2.     ...
  3.     // 初始化conn的一些参数
  4.     c.remoteAddr = c.rwc.RemoteAddr().String()
  5.     c.r = &connReader{conn: c}
  6.     c.bufr = newBufioReader(c.r)
  7.     c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
  8.     for {
  9.         // 读取客户端请求
  10.         w, err := c.readRequest(ctx)
  11.         ...
  12.         // 调用ServeHTTP来处理请求
  13.         serverHandler{c.server}.ServeHTTP(w, w.req)
  14.     }
  15. }
复制代码
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) 其实就是下面函数的实现。
  1. type serverHandler struct {
  2.     srv *Server
  3. }

  4. func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
  5.     handler := sh.srv.Handler
  6.     if handler == nil {
  7.         handler = DefaultServeMux
  8.     }
  9.     if req.RequestURI == "*" && req.Method == "OPTIONS" {
  10.         handler = globalOptionsHandler{}
  11.     }
  12.     ...
  13.     // handler传的是nil就执行 DefaultServeMux.ServeHTTP() 方法
  14.     handler.ServeHTTP(rw, req)
  15. }
复制代码
获取Server的handler流程:
1. 先获取 sh.srv.Handler 的值,判断是否为nil
2. 如果为nil则取全局单例 DefaultServeMux这个handler
3. PTIONS Method 请求且 URI 是 *,就使用globalOptionsHandler
注:这个handler其实就是在ListenAndServe()中的第二个参数
ServeMux.ServeHTTP
  1. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
  2.     ....
  3.     h, _ := mux.Handler(r)
  4.     // 执行匹配到的路由的ServeHTTP方法
  5.     h.ServeHTTP(w, r)
  6. }
复制代码
ServeMux.ServeHTTP()方法主要代码可以分为两步:
1. 通过 ServerMux.Handler() 方法获取到匹配的处理函数 h
2. 调用 Handler.ServeHTTP() 执行匹配到该路由的函数来处理请求 (h实现了ServeHTTP方法)
8.png

ServerMux.Handler():
  1. func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
  2.     ...
  3.     //在mux.m和mux.es中
  4.     //根据host/url.path寻找对应的handler
  5.     return mux.handler(host, r.URL.Path)
  6. }
复制代码
在 ServeMux.Handler() 方法内部,会调用 ServerMux.handler(host, r.URL.Path) 方法来查找匹配的处理函数。
ServeMux.match
ServeMux.match()方法用于根据给定的具体路径 path 找到最佳匹配的路由,并返回Handler和路径。
值得一提的是,如果 mux.m 中不存在 path 完全匹配的路由时,会继续遍历 mux.es 字段中保存的模糊匹配路由。
  1. func (mux *ServeMux) match(path string) (h Handler, pattern string) {
  2.     // 是否完全匹配
  3.     v, ok := mux.m[path]
  4.     if ok {
  5.         return v.h, v.pattern
  6.     }
  7.     // mux.es是按pattern从长到短排列
  8.     for _, e := range mux.es {
  9.         if strings.HasPrefix(path, e.pattern) {
  10.             return e.h, e.pattern
  11.         }
  12.     }
  13.     return nil, ""
  14. }
复制代码
最后调用 handler.ServeHTTP 方法进行请求的处理和响应,而这个被调用的函数就是我们之前在路由注册时对应的函数。
  1. type HandlerFunc func(ResponseWriter, *Request)

  2. func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  3.     f(w, r)
  4. }
复制代码
到这里整个服务的流程就到这里了,现在有对这块有印象了吗?
以上就是一文带你吃透Golang中net/http标准库服务端的详细内容,更多关于Go net/http标准库的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
20
积分
20
注册时间
2022-12-28
最后登录
2022-12-28

发表于 2025-1-19 09:45:10 | 显示全部楼层
感谢楼主,顶。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2025-3-17 07:16:28 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
11
积分
2
注册时间
2023-6-25
最后登录
2023-6-25

发表于 4 天前 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表