go 进阶 http标准库相关: 三. HttpServer 服务启动到Accept等待接收连接

一. http.ListenAndServe() 服务启动基础概述

  1. 在上面我们了解到了一个基础http服务的搭建,多路复用器内部结构以及路由注册原理
  2. 通过"http.ListenAndServe(“:8080”, nil)"启动服务并监听指定端口,查看源码:
func ListenAndServe(addr string, handler Handler) error {
	//1.创建Server
    server := &Server{Addr: addr, Handler: handler}
    //2.调用server下的ListenAndServe()
    return server.ListenAndServe()
}
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
    	// 如果Server已关闭,直接返回 ErrServerClosed
        return ErrServerClosed 
    }
    addr := srv.Addr
    if addr == "" {
    	// 如果不指定服务器地址信息,默认以":http"作为地址信息
        addr = ":http" 
    }
    //多路复用相关初始化
    //创建TCPListener,接收客户端的连接请求
  	//第一个参数network: 可以是 tcp、tcp4、tcp6、 unix 或者 unixpacket,
	// 第二个参数address: 可以用主机名(hostname),但是不建议,因为这样创建的listener(监听器)最多监听主机的一个ip
	//如果 address 参数的 port 为空或者"0",如"127.0.0.1:"或者"[::1]:0",将自动选择一个端口号
    ln, err := net.Listen("tcp", addr)  
    if err != nil {
        return err
    }
    // 调用Server.Serve()函数并返回
    return srv.Serve(ln)  
}
  1. 首先封装了一个Server结构体变量,然后调用Server下的ListenAndServe()方法,在该方法中重点分为两个步骤:
  1. " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
  2. “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
  1. “net.Listen(“tcp”, addr)”: 是golang实现多路复用的核心,内部最终会调用到ListenConfig结构体上的Listen()方法.在该方法中分为3个步骤实现了(后续有专门篇章讲解此处就不再赘述):
  1. 执行poll_runtime_pollServerInit:通过该函数最终调用到netpollinit(),封装一个epoll文件描述符实例epollevent,并且使用sync.Once封装保证程序中只会创建一个
  2. 执行poll_runtime_pollOpen: 调用alloc()初始化总大小约为 4KB的pollDesc结构体,调用netpollopen(),将可读,可写,对端断开,边缘触发 的监听事件注册到epollevent中
    3, 最后封装一个TCPListener结构体返回
  1. 我们现在重点关注一下,拿到TCPListener后,在接口调用时是怎么执行的,srv.Serve(ln)内部逻辑是什么

二. Server 结构体详解

  1. 在ListenAndServe中先封装了一个Server结构体变量,先简单了解一下Server结构体的组成
type Server struct {
    // 监听的TCP地址 格式为: host:port 如果为空 默认使用 :http 端口为 80
    Addr string

    // 调用的 handler(路由处理器), 设为 nil 表示 http.DefaultServeMux
    Handler Handler // handler to invoke, http.DefaultServeMux if nil

    // 如果服务需要支持https协议 需要相应的配置
    TLSConfig *tls.Config

    // 请求超时时间,包含请求头和请求体。
    ReadTimeout time.Duration

    // 请求头超时时间。读取请求头之后,连接的读取截止日期会被重置.
    // Handler可以再根据 ReadTimeout 判断Body的超时时间。
    // 如果ReadHeaderTimeout为零,则使用ReadTimeout的值。如果两者都为零,则不会超时。
    ReadHeaderTimeout time.Duration

    // 响应超时时间。 每当读取新请求的Header时,它将重置。 
    // 与ReadTimeout一样,它也不允许处理程序根据每个请求做出决策。
    WriteTimeout time.Duration

    // 启用keep-alives时,保持连接等待下一个请求的最长时间。
    // 如果IdleTimeout为零,则使用ReadTimeout的值。如果两者都为零,则不会超时。
    IdleTimeout time.Duration

    // 解析请求头的key和value(包括请求行)时将读取的最大字节数。
    // 它不限制请求Body大小。如果为零,则使用DefaultMaxHeaderBytes。
    MaxHeaderBytes int

    // 当 '应用层协议协商 (NPN/ALPN)' 时发生协议升级时,TLSNextProto 需要指定可选的 function 去接管 TLS 连接
    // map的key为: 协商的协议名称。
    // Handler参数应用于处理HTTP请求,如果尚未设置,它将初始化请求的TLS和RemoteAddr。 
    // 函数返回时,连接将自动关闭。 如果TLSNextProto不为nil,则不会自动启用HTTP/2支持。
    TLSNextProto map[string]func(*Server, *tls.Conn, Handler)

    //  当客户端连接状态改变时调用的回调函数。有关详细信息,请参阅ConnState类型常量。
    ConnState func(net.Conn, ConnState)

    // 日志记录对象
    ErrorLog *log.Logger

    // 为来到此服务器的请求指定 context 上下文 ,不设就是 context.Background()
    // 如果BaseContext为nil,则默认值为context.Background()。 
    // 如果为非nil,则它必须返回非nil上下文。
    BaseContext func(net.Listener) context.Context

    // 提供一个函数用于修改新连接的context上下文,提供的ctx是从基上下文派生的,具有ServerContextKey值。
    ConnContext func(ctx context.Context, c net.Conn) context.Context

    // 当服务器关闭时为true
    inShutdown atomicBool // true when when server is in shutdown

    // 关闭 keep-alives
    disableKeepAlives int32     // accessed atomically.
    nextProtoOnce     sync.Once // guards setupHTTP2_* init
    nextProtoErr      error     // result of http2.ConfigureServer if used

    // 互斥锁 保证资源的安全
    mu         sync.Mutex
    // 监听socket表
    listeners  map[*net.Listener]struct{}
    // 存活的客户端链接表
    activeConn map[*conn]struct{}
    // 用于通知服务关闭 信道
    doneChan   chan struct{}
    // 注册服务器关闭执行的一些行为
    // 通过 RegisterOnShutdown 注册,在 Shutdown 时调用当中的钩子函数
    onShutdown []func()
}

三. 查看Server.Serve(ln) 源码

  1. 在Server.Serve(ln) 上一步骤中执行"ln, err := net.Listen(“tcp”, addr)",初试化了socket,添加监听事件,端口连接绑定等操作,拿到TCPListener
  2. 接下来我们看一下Server.Serve(ln)源码,内部重点逻辑:
  1. “rw, e := l.Accept()”: 进行accept, 等待客户端进行连接
  2. “go c.serve(ctx)”: 启动新的goroutine来处理本次请求. 同时主goroutine继续等待客户端连接, 进行高并发操作
func (srv *Server) Serve(l net.Listener) error {
    // 测试用的钩子函数,一般用不到
    if fn := testHookServerServe; fn != nil {
        fn(srv, l) // call hook with unwrapped listener
    }

    origListener := l
    // onceCloseListener 包装 net.Listener,用于防止多次关闭链接
    // sync.Once.Do(f func()) 能保证once只执行一次
    l = &onceCloseListener{Listener: l}
    // 结束的时候关闭监听socket
    defer l.Close()

    // http2相关的设置
    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    // 把监听socket添加监听表
    if !srv.trackListener(&l, true) {
        return ErrServerClosed
    }
    // 结束的时候从监听表删除
    defer srv.trackListener(&l, false)

    // 获取一个非 nil 的 上下文。
    baseCtx := context.Background()
    if srv.BaseContext != nil {
        baseCtx = srv.BaseContext(origListener)
        if baseCtx == nil {
            panic("BaseContext returned a nil context")
        }
    }

    // 设置临时过期时间,当accept发生 错误的时候等待一段时间
    var tempDelay time.Duration // how long to sleep on accept failure

    // 把server本身放入上下文,用于参数传递,可以在 其他子协程中 根据 ServerContextKey 获取到 srv
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    // 循环监听客户端到来
    for {
        // accept 阻塞等待客户单到来
        rw, err := l.Accept()
        // 如果发生错误后的处理逻辑
        if err != nil {
            // 如果从server.doneChan中读取到内容,代表服务已关闭,返回 ErrServerClosed
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            // 如果 e 是 net.Error, 并且错误是临时性的
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                // 第一次没接收到数据,睡眠5毫秒
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    // 每次睡眠结束,唤醒后还是没接收到数据,睡眠时间加倍
                    tempDelay *= 2
                }
                // 单次睡眠时间上限设定为1秒
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                // 输出重新等待
                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
                // 休眠一段时间
                time.Sleep(tempDelay)
                continue
            }
            // 如果e不是net.Error,或者不是临时性错误,就返回错误
            return err
        }
        // 如果接收到数据就做如下处理:
        // 如果指定了server的ConnContext,就用它修改连接的context
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        // 休眠定时器归零
        tempDelay = 0
        // 使用当前的Conn接口构建新的 conn 实例,它包含了srv服务器和rw连接。
        // conn 实例代表一个 HTTP 连接的服务端,rw是底层的网络连接
        c := srv.newConn(rw)
        // 更新连接状态
        // 第一次更新连接状态会初始化srv.activeConn为一个map[*conn]struct{} 并将活跃连接添加进去。
        // srv.activeConn是一个集合,保存服务器的活跃连接,当连接关闭 或者 被劫持 会从集合中删除。
        // 详细可以往里点看  trackConn函数。
        // 如果 server.ConnState 不为nil 也会在连接状态改变时,执行该回调函数
        c.setState(c.rwc, StateNew) // before Serve can return
        // 启动goroutine处理socket
        go c.serve(connCtx)
    }
}
  1. 实际上面Server.Serve(ln) 源码可以简化为以下模拟代码,执行了一个死循环,循环内获取到上一步拿到的TCPListener,执行l.Accept()阻塞等待接收监听到的网络连接,当新连接建立,将返回新的net.Conn实例,然后把服务器实例Server和底层连接(net.Conn)封装为一个内部的conn结构体的实例,并将conn连接的状态标志为StateNew,然后开启一个goroutine执行它的serve()方法,这里对每一个连接开启一个goroutine来处理,这就是并发的体现,然后for循环继续执行等待下一个连接
//1.开启死循环(注意循环内部通过Accept()阻塞监控客户端连接,如果没有连接会一直阻塞)
for {
	//2.阻塞接收客户端连接
    rw, e := l.Accept()
    ...
    c, err := srv.newConn(rw)
    c.setState(c.rwc, StateNew)
    //3.当接收到客户端连接请求后,将请求通过协程执行(并发的体现)
    go c.serve()
	//4.循环继续执行,继续等待接收新的连接
}

连接的state状态

  1. 当Accept()接收到客户端请求后,首先会执行newConn()获取到net.conn连接对象,调用连接对象上的setState()设置连接状态,然后开启协程,并发的处理多个请求,连接状态有5种
  1. 初始状态为 http.StateNew
  2. 当新的请求到达时,state 转换为 http.StateActive
  3. 在处理完请求后,如果保持长连接,则 state 保持为 http.StateActive;否则,转换为 http.StateIdle。
  4. 如果连接被接管,则 state 转换为 http.StateHijacked
  5. 如果连接被关闭,则 state 转换为 http.StateClosed
  1. 一旦请求处理完毕,即使连接仍然存在,其状态也会从 http.StateActive 转变为 http.StateIdle,表示该连接当前处于空闲状态,可以接收新的请求。当另一个请求到达时,连接的状态再次转变为 http.StateActive,表示该连接正在处理新的请求

四. Listener.Accept 等待连接

  1. 具体参考go 进阶 多路复用支持: 二. 接收连接与获取连接后的读写
  2. 这里可以先简述一下: Listener.Accept方法最终会调用到FD下的Accept接受新的 socket 连接,接收到连接后,调用newFD()构造一个新的netfd,并通过init 方法完成初始化
  3. 在FD.Accept方法中会调用当前系统的,例如 linux下的 accept 接收新连接创建对应的 socket,通过switch等待I/O 事件,如果没有,会调用pollDesc的waitRead
  4. 通过pollDesc的waitRead函数,最终会执行到netpollblock():
  1. 根据mode获取对应的信号量地址 gpp,判断当前是否pdReady。
  2. 判断当gpp的值如果等于 0 时,将gpp的值更替为pdWait,该操作属于原子操作且内部实现了自旋锁。
  3. 当值为pdWait之后,防止此时可能会有其他的并发操作修改 pd 里的内容,所以需要再次检查错误状态。gopark将当前 goroutine 置于等待状态并等待下一次的调度,但gopark仍有可能因为超时或者关闭会立即返回
  4. 通过原子操作将gpp的值设置为 0,返回修改前的值并判断是否pdReady
  1. 最终/把当前 groutine 的抽象数据结构 g 存入 gpp 指针中,netpoll 通过 gopark 在 groutine 中模拟出了阻塞 I/O 的效果,goroutine 挂起后,会被放置在等待队列中等待唤醒
  2. 注意l.Accept()返回net.Conn为底层TCP连接, 而srv.newConn(rw)返回的http.conn可以理解为HTTP连接

问题

  1. 被阻塞的请求是如何被唤醒的(唤醒后续要专门篇章讲解)
  2. 唤醒后怎么执行的参考go 进阶 http标准库相关: 四. HttpServer 服务启动接到连接后的处理逻辑

六. 总结

  1. 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
  2. 服务启动后,是怎么接受请求的,这里要看一下"net/http"下的ListenAndServe()函数,在该函数中首先会封装一个Server结构体变量,就是用来建立 HTTP 服务器并处理请求的主要对象,内部有Addr 监听的地址属性,对请求的一下配置设置,例如ReadTimeout读取超时时间 ,WriteTimeout响应超时时间等等,然后调用Server的ListenAndServe()方法,重点:
  1. " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
  2. “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
  1. 我们现在先不管多路复用,先重点关注一下,srv.Serve(ln)的内部逻辑,该方法内可以重点简化为:
  1. 方法内通过for开启了一个死循环
  2. 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
  3. 当接收到连接请求Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
  4. 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
  5. 然后for循环继续执行等待下一个连接
  1. 最后当接收到连接请求Accept()方法返回执行"go c.serve()"通过协程处理每一个请求,怎么处理的,参考后面的文档,此处就不再赘述

引出一个小问题

  1. 接收用户请求时底层是通过协程去执行的, 那一个服务中同一时间最高可以处理多少个请求?
  2. 实际在go或者net/http提供的服务中,并没有主动设置同一时间最大并发数,如果真要计算,只能通过协程调度的层面去考虑,但是又不太合理
  1. 在启动go项目时,底层会调用rt0_go,内部会执行初始化调度相关函数runtime.schedinit(SB), 该函数内部
  2. 设置m的最大数量是10000
  3. 调用procresize,调整p的数量,也可以通过环境变量 GOMAXPROCS 来控制 P 的数量。_MaxGomaxprocs 控制了最大的 P 数量只能是 1024
  4. 同一时间可以运行的协程数量取决于 P 的数量,Go 语言的调度器会根据实际的系统核心数来创建对应数量的 M,并为每个 M 创建一个 P。因此,同一时间可以同时运行的协程数量等于 CPU 核心数
  1. 问题是: 一个服务同时可以运行的协程数量并不仅仅受到 G、M、P 数量的限制。在处理 HTTP 请求时,还会因为各种因素导致协程的切换和等待阻塞,所以无法直接通过 G、M、P 数量来计算一个服务可以处理的并发请求数量
  2. 在go的net/http中也可以主动设置
  3. 在net/http底层的server结构体上有一个MaxConnsPerIP属性,设置每个 IP 的最大并发连接数为 5
s := &http.Server{
    Addr:           ":8080",
    Handler:        http.HandlerFunc(handlerFunc),
    MaxConnsPerIP:  5, // 同时限制每个 IP 的最大并发连接数为 5
}
  1. 也可以创建专门用来处理并发的Handler处理器,设置到Server结构体上,通过这个处理器来限制最大并发数
  1. 创建带有缓冲区大小为 maxConcurrentRequests 的通道 requestCh。每当收到一个请求时,向 requestCh 发送一个元素,然后启动一个协程处理该请求,并在处理完成后从通道中取出一个元素,表示该请求已处理完成。
  2. 当通道 requestCh 已满时,即当前同时处理的请求数已经达到 maxConcurrentRequests,就会阻塞当前的请求,直到协程池中有一个协程空闲下来才会再次处理该请求
maxConcurrentRequests := 10 // 同时处理的最大请求数为 10

requestCh := make(chan struct{}, maxConcurrentRequests)
s := &http.Server{
    Addr:    ":8080",
    Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestCh <- struct{}{}
        defer func() {
            <-requestCh
        }()
        // 处理请求
    }),
}
  1. 另外也可以通过redis等三方工具,三方库进行限流
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值