一. http.ListenAndServe() 服务启动基础概述
- 在上面我们了解到了一个基础http服务的搭建,多路复用器内部结构以及路由注册原理
- 通过"http.ListenAndServe(“:8080”, nil)"启动服务并监听指定端口,查看源码:
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.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)
}
- 首先封装了一个Server结构体变量,然后调用Server下的ListenAndServe()方法,在该方法中重点分为两个步骤:
- " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
- “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
- “net.Listen(“tcp”, addr)”: 是golang实现多路复用的核心,内部最终会调用到ListenConfig结构体上的Listen()方法.在该方法中分为3个步骤实现了(后续有专门篇章讲解此处就不再赘述):
- 执行poll_runtime_pollServerInit:通过该函数最终调用到netpollinit(),封装一个epoll文件描述符实例epollevent,并且使用sync.Once封装保证程序中只会创建一个
- 执行poll_runtime_pollOpen: 调用alloc()初始化总大小约为 4KB的pollDesc结构体,调用netpollopen(),将可读,可写,对端断开,边缘触发 的监听事件注册到epollevent中
3, 最后封装一个TCPListener结构体返回
- 我们现在重点关注一下,拿到TCPListener后,在接口调用时是怎么执行的,srv.Serve(ln)内部逻辑是什么
二. Server 结构体详解
- 在ListenAndServe中先封装了一个Server结构体变量,先简单了解一下Server结构体的组成
type Server struct {
Addr string
Handler Handler
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
BaseContext func(net.Listener) context.Context
ConnContext func(ctx context.Context, c net.Conn) context.Context
inShutdown atomicBool
disableKeepAlives int32
nextProtoOnce sync.Once
nextProtoErr error
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
doneChan chan struct{}
onShutdown []func()
}
三. 查看Server.Serve(ln) 源码
- 在Server.Serve(ln) 上一步骤中执行"ln, err := net.Listen(“tcp”, addr)",初试化了socket,添加监听事件,端口连接绑定等操作,拿到TCPListener
- 接下来我们看一下Server.Serve(ln)源码,内部重点逻辑:
- “rw, e := l.Accept()”: 进行accept, 等待客户端进行连接
- “go c.serve(ctx)”: 启动新的goroutine来处理本次请求. 同时主goroutine继续等待客户端连接, 进行高并发操作
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l)
}
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
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)
go c.serve(connCtx)
}
}
- 实际上面Server.Serve(ln) 源码可以简化为以下模拟代码,执行了一个死循环,循环内获取到上一步拿到的TCPListener,执行l.Accept()阻塞等待接收监听到的网络连接,当新连接建立,将返回新的net.Conn实例,然后把服务器实例Server和底层连接(net.Conn)封装为一个内部的conn结构体的实例,并将conn连接的状态标志为StateNew,然后开启一个goroutine执行它的serve()方法,这里对每一个连接开启一个goroutine来处理,这就是并发的体现,然后for循环继续执行等待下一个连接
for {
rw, e := l.Accept()
...
c, err := srv.newConn(rw)
c.setState(c.rwc, StateNew)
go c.serve()
}
连接的state状态
- 当Accept()接收到客户端请求后,首先会执行newConn()获取到net.conn连接对象,调用连接对象上的setState()设置连接状态,然后开启协程,并发的处理多个请求,连接状态有5种
- 初始状态为 http.StateNew
- 当新的请求到达时,state 转换为 http.StateActive
- 在处理完请求后,如果保持长连接,则 state 保持为 http.StateActive;否则,转换为 http.StateIdle。
- 如果连接被接管,则 state 转换为 http.StateHijacked
- 如果连接被关闭,则 state 转换为 http.StateClosed
- 一旦请求处理完毕,即使连接仍然存在,其状态也会从 http.StateActive 转变为 http.StateIdle,表示该连接当前处于空闲状态,可以接收新的请求。当另一个请求到达时,连接的状态再次转变为 http.StateActive,表示该连接正在处理新的请求
四. Listener.Accept 等待连接
- 具体参考go 进阶 多路复用支持: 二. 接收连接与获取连接后的读写
- 这里可以先简述一下: Listener.Accept方法最终会调用到FD下的Accept接受新的 socket 连接,接收到连接后,调用newFD()构造一个新的netfd,并通过init 方法完成初始化
- 在FD.Accept方法中会调用当前系统的,例如 linux下的 accept 接收新连接创建对应的 socket,通过switch等待I/O 事件,如果没有,会调用pollDesc的waitRead
- 通过pollDesc的waitRead函数,最终会执行到netpollblock():
- 根据mode获取对应的信号量地址 gpp,判断当前是否pdReady。
- 判断当gpp的值如果等于 0 时,将gpp的值更替为pdWait,该操作属于原子操作且内部实现了自旋锁。
- 当值为pdWait之后,防止此时可能会有其他的并发操作修改 pd 里的内容,所以需要再次检查错误状态。gopark将当前 goroutine 置于等待状态并等待下一次的调度,但gopark仍有可能因为超时或者关闭会立即返回
- 通过原子操作将gpp的值设置为 0,返回修改前的值并判断是否pdReady
- 最终/把当前 groutine 的抽象数据结构 g 存入 gpp 指针中,netpoll 通过 gopark 在 groutine 中模拟出了阻塞 I/O 的效果,goroutine 挂起后,会被放置在等待队列中等待唤醒
- 注意l.Accept()返回net.Conn为底层TCP连接, 而srv.newConn(rw)返回的http.conn可以理解为HTTP连接
问题
- 被阻塞的请求是如何被唤醒的(唤醒后续要专门篇章讲解)
- 唤醒后怎么执行的参考go 进阶 http标准库相关: 四. HttpServer 服务启动接到连接后的处理逻辑
六. 总结
- 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
- 服务启动后,是怎么接受请求的,这里要看一下"net/http"下的ListenAndServe()函数,在该函数中首先会封装一个Server结构体变量,就是用来建立 HTTP 服务器并处理请求的主要对象,内部有Addr 监听的地址属性,对请求的一下配置设置,例如ReadTimeout读取超时时间 ,WriteTimeout响应超时时间等等,然后调用Server的ListenAndServe()方法,重点:
- " net.Listen(“tcp”, addr)": 多路复用相关初始化,初始化socket,端口连接绑定,开启监听
- “srv.Serve(ln)”: 等待接收客户端连接Accept(),与接收到连接后的处理流程
- 我们现在先不管多路复用,先重点关注一下,srv.Serve(ln)的内部逻辑,该方法内可以重点简化为:
- 方法内通过for开启了一个死循环
- 在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,是阻塞的(该方法内部有多路复用的相关逻辑,此处先不关注)
- 当接收到连接请求Accept()方法返回,拿到一个新的net.Conn连接实例,继续向下执行,封装net.Conn连接,设置连接状态为StateNew
- 通过协程执行连接的serve()方法,每一个连接开启一个goroutine来处理
- 然后for循环继续执行等待下一个连接
- 最后当接收到连接请求Accept()方法返回执行"go c.serve()"通过协程处理每一个请求,怎么处理的,参考后面的文档,此处就不再赘述
引出一个小问题
- 接收用户请求时底层是通过协程去执行的, 那一个服务中同一时间最高可以处理多少个请求?
- 实际在go或者net/http提供的服务中,并没有主动设置同一时间最大并发数,如果真要计算,只能通过协程调度的层面去考虑,但是又不太合理
- 在启动go项目时,底层会调用rt0_go,内部会执行初始化调度相关函数runtime.schedinit(SB), 该函数内部
- 设置m的最大数量是10000
- 调用procresize,调整p的数量,也可以通过环境变量 GOMAXPROCS 来控制 P 的数量。_MaxGomaxprocs 控制了最大的 P 数量只能是 1024
- 同一时间可以运行的协程数量取决于 P 的数量,Go 语言的调度器会根据实际的系统核心数来创建对应数量的 M,并为每个 M 创建一个 P。因此,同一时间可以同时运行的协程数量等于 CPU 核心数
- 问题是: 一个服务同时可以运行的协程数量并不仅仅受到 G、M、P 数量的限制。在处理 HTTP 请求时,还会因为各种因素导致协程的切换和等待阻塞,所以无法直接通过 G、M、P 数量来计算一个服务可以处理的并发请求数量
- 在go的net/http中也可以主动设置
- 在net/http底层的server结构体上有一个MaxConnsPerIP属性,设置每个 IP 的最大并发连接数为 5
s := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(handlerFunc),
MaxConnsPerIP: 5,
}
- 也可以创建专门用来处理并发的Handler处理器,设置到Server结构体上,通过这个处理器来限制最大并发数
- 创建带有缓冲区大小为 maxConcurrentRequests 的通道 requestCh。每当收到一个请求时,向 requestCh 发送一个元素,然后启动一个协程处理该请求,并在处理完成后从通道中取出一个元素,表示该请求已处理完成。
- 当通道 requestCh 已满时,即当前同时处理的请求数已经达到 maxConcurrentRequests,就会阻塞当前的请求,直到协程池中有一个协程空闲下来才会再次处理该请求
maxConcurrentRequests := 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
}()
}),
}
- 另外也可以通过redis等三方工具,三方库进行限流