手机版 欢迎访问it开发者社区(www.mfbz.cn)网站

当前位置: > 开发

Go 如何使得 Web 工作

时间:2021/6/4 19:01:16|来源:|点击: 次

一 web 工作方式的几个概念

以下几个均是服务器端的概念。

Request:用户请求的信息,用来解析用户的请求信息,包括 post、get、cookie、url 等信息。

Response:服务器需要反馈给客户端的信息。

Conn:用户的每次请求链接。

Handler:处理请求和生成返回信息的处理逻辑。

二 http 包运行机制

Go 实现 Web 服务的工作模式的流程图

http包执行流程

1 创建 Listen Socket,监听指定的端口, 等待客户端请求到来。

2 Listen Socket 接受客户端的请求,得到 Client Socket,接下来通过 Client Socket 与客户端通信。

3 处理客户端的请求。

首先从 Client Socket 读取 HTTP 请求的协议头,如果是 POST 方法,还可能要读取客户端提交的数据,然后交给相应的 handler 处理请求,handler 处理完毕准备好客户端需要的数据。通过 Client Socket 写给客户端。

整个过程需要了解清楚下面三个问题。

  • 如何监听端口?

  • 如何接收客户端请求?

  • 如何分配 handler?

Go 是通过一个函数 ListenAndServe 来处理这些事情的。

首先,初始化一个 server 对象,然后调用 ListenAndServe() 函数。

func ListenAndServe(addr string, handler Handler) error {
   server := &Server{Addr: addr, Handler: handler}
   return server.ListenAndServe()
}

其次,调用了 net.Listen("tcp", addr),也就是底层用 TCP 协议搭建了一个服务,然后监控我们设置的端口。然后调用 srv.Serve() 函数,对 http 请求进行处理。

func (srv *Server) ListenAndServe() error {
   addr := srv.Addr
   if addr == "" {
      addr = ":http"
   }
   ln, err := net.Listen("tcp", addr)
   if err != nil {
      return err
   }
   return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

下面的代码可以看到整个的 http 处理过程。

func (srv *Server) Serve(l net.Listener) error {
   defer l.Close()
   if fn := testHookServerServe; fn != nil {
      fn(srv, l)
   }
   var tempDelay time.Duration // how long to sleep on accept failure

   if err := srv.setupHTTP2_Serve(); err != nil {
      return err
   }

   srv.trackListener(l, true)
   defer srv.trackListener(l, false)


   baseCtx := context.Background() // base is always background, per Issue 16220
   ctx := context.WithValue(baseCtx, ServerContextKey, srv)
   for {
      rw, e := l.Accept()
      if e != nil {
         select {
         case <-srv.getDoneChan():
            return ErrServerClosed
         default:
         }
         if ne, ok := e.(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", e, tempDelay)
            time.Sleep(tempDelay)
            continue
         }
         return e
      }
      tempDelay = 0
      c := srv.newConn(rw)
      c.setState(c.rwc, StateNew) // before Serve can return
      go c.serve(ctx)
   }
}

这个函数里面有一个for{},首先通过 Listener 接收请求,其次创建一个 Conn,最后单独开了一个 goroutine,把这个请求的数据当做参数扔给这个 conn 去服务:go c.serve()。这个就是高并发体现了,用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。

那么如何具体分配到相应的函数来处理请求呢?见下面代码。

func (c *conn) serve(ctx context.Context) {
   c.remoteAddr = c.rwc.RemoteAddr().String()
   ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
   defer func() {
      if err := recover(); err != nil && err != ErrAbortHandler {
         const size = 64 << 10
         buf := make([]byte, size)
         buf = buf[:runtime.Stack(buf, false)]
         c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
      }
      if !c.hijacked() {
         c.close()
         c.setState(c.rwc, StateClosed)
      }
   }()

   if tlsConn, ok := c.rwc.(*tls.Conn); ok {
      if d := c.server.ReadTimeout; d != 0 {
         c.rwc.SetReadDeadline(time.Now().Add(d))
      }
      if d := c.server.WriteTimeout; d != 0 {
         c.rwc.SetWriteDeadline(time.Now().Add(d))
      }
      if err := tlsConn.Handshake(); err != nil {
         c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
         return
      }
      c.tlsState = new(tls.ConnectionState)
      *c.tlsState = tlsConn.ConnectionState()
      if proto := c.tlsState.NegotiatedProtocol; validNPN(proto) {
         if fn := c.server.TLSNextProto[proto]; fn != nil {
            h := initNPNRequest{tlsConn, serverHandler{c.server}}
            fn(c.server, tlsConn, h)
         }
         return
      }
   }

   // HTTP/1.x from here on.

   ctx, cancelCtx := context.WithCancel(ctx)
   c.cancelCtx = cancelCtx
   defer cancelCtx()

   c.r = &connReader{conn: c}
   c.bufr = newBufioReader(c.r)
   c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)
   for {
      w, err := c.readRequest(ctx)
      if c.r.remain != c.server.initialReadLimitSize() {
         // If we read any bytes off the wire, we're active.
         c.setState(c.rwc, StateActive)
      }
      if err != nil {
         const errorHeaders = "\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: close\r\n\r\n"

         if err == errTooLarge {
            // Their HTTP client may or may not be
            // able to read this if we're
            // responding to them and hanging up
            // while they're still writing their
            // request. Undefined behavior.
            const publicErr = "431 Request Header Fields Too Large"
            fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
            c.closeWriteAndWait()
            return
         }
         if isCommonNetReadError(err) {
            return // don't reply
         }

         publicErr := "400 Bad Request"
         if v, ok := err.(badRequestError); ok {
            publicErr = publicErr + ": " + string(v)
         }

         fmt.Fprintf(c.rwc, "HTTP/1.1 "+publicErr+errorHeaders+publicErr)
         return
      }

      // Expect 100 Continue support
      req := w.req
      if req.expectsContinue() {
         if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 {
            // Wrap the Body reader with one that replies on the connection
            req.Body = &expectContinueReader{readCloser: req.Body, resp: w}
         }
      } else if req.Header.get("Expect") != "" {
         w.sendExpectationFailed()
         return
      }

      c.curReq.Store(w)

      if requestBodyRemains(req.Body) {
         registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
      } else {
         if w.conn.bufr.Buffered() > 0 {
            w.conn.r.closeNotifyFromPipelinedRequest()
         }
         w.conn.r.startBackgroundRead()
      }

      // HTTP cannot have multiple simultaneous active requests.[*]
      // Until the server replies to this request, it can't read another,
      // so we might as well run the handler in this goroutine.
      // [*] Not strictly true: HTTP pipelining. We could let them all process
      // in parallel even if their responses need to be serialized.
      // But we're not going to implement HTTP pipelining because it
      // was never deployed in the wild and the answer is HTTP/2.
      serverHandler{c.server}.ServeHTTP(w, w.req)
      w.cancelCtx()
      if c.hijacked() {
         return
      }
      w.finishRequest()
      if !w.shouldReuseConnection() {
         if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
            c.closeWriteAndWait()
         }
         return
      }
      c.setState(c.rwc, StateIdle)
      c.curReq.Store((*response)(nil))

      if !w.conn.server.doKeepAlives() {
         // We're in shutdown mode. We might've replied
         // to the user without "Connection: close" and
         // they might think they can send another
         // request, but such is life with HTTP/1.1.
         return
      }

      if d := c.server.idleTimeout(); d != 0 {
         c.rwc.SetReadDeadline(time.Now().Add(d))
         if _, err := c.bufr.Peek(4); err != nil {
            return
         }
      }
      c.rwc.SetReadDeadline(time.Time{})
   }
}

处理器分配代码。

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.ServeHTTP(rw, req)
}

conn 首先会解析 request:c.readRequest(),然后获取相应的handler:handler := sh.srv.Handler。也就是函数 ListenAndServe 的第二个参数,当传递的是 nil,也就是为空时,那么默认获取 handler = DefaultServeMux。那么这个变量用来做什么的呢?这个变量就是一个路由器,它用来匹配 url 跳转到其相应的 handle 函数,那么这个有设置过吗?有,当我们调用 http.HandleFunc("/", sayhelloName) 时设置的。它的作用就是注册请求/的路由规则,当请求uri为"/",路由就会转到函数 sayhelloName,DefaultServeMux会调用 ServeHTTP 方法,这个方法内部其实就是调用 sayhelloName 本身,最后通过写入response的信息反馈到客户端。

整个流程如下。

 

 

 

 

Copyright © 2002-2019 某某自媒体运营 版权所有