Understanding the implementation of HTTP server in golang

Time:2020-12-12

preface

For golang, implementing a simplehttp serverIt’s very easy, just a few lines of code. At the same time, with the blessing of coroutine, go implementshttp serverCan achieve very good performance. This article will be on the go standard librarynet/httpIn order to learn and understand the common paradigm and design ideas of network programming, the principle of HTTP service is explored in depth.

HTTP service

The network application based on HTTP includes two terminals, namely client(Client)And server(Server)。 The interaction between the two ends includes sending out from the clientrequestServer acceptancerequestProcess and returnresponseAnd client processingresponse。 So the job of the HTTP server is how to accept therequestAnd return to the clientresponse

The typical HTTP server processing flow can be shown in the following figure:

When the server receives the request, it will enter the route first(router)This is aMultiplexerThe job of routing is for thisrequestFind the corresponding processor(handler), processor pairrequestProcess and buildresponse。 Implemented by golanghttp serverFollow the same process.

Let’s take a look at how golang implements a simplehttp server


package main

import (
 "fmt"
 "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "hello world")
}

func main() {
 http.HandleFunc("/", indexHandler)
 http.ListenAndServe(":8000", nil)
}

After running the code, open it in the browserlocalhost:8000You can see thathello world。 This code first useshttp.HandleFuncAt root routing/Registered onindexHandlerAnd then usehttp.ListenAndServeTurn on monitoring. When a request comes, the correspondinghandlerFunction.

Let’s take a look at another common onehttp serverImplementation mode:


package main

import (
 "fmt"
 "net/http"
)

type indexHandler struct {
 content string
}

func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, ih.content)
}

func main() {
 http.Handle("/", &indexHandler{content: "hello world!"})
 http.ListenAndServe(":8001", nil)
}

Go implementationhttpThe service steps are very simple. First register the route, then create the service and enable listening. In the following, we will learn how to implement golang from the following steps: registering routes, starting services, and processing requestshttpService.

Register route

http.HandleFuncandhttp.HandleBoth are used to register routes. It can be found that the difference between the two lies in the second parameter: thefunc(w http.ResponseWriter, r *http.Requests)The function of signature, which is a structure, implementsfunc(w http.ResponseWriter, r *http.Requests)Signature method.

http.HandleFuncandhttp.HandleThe source code is as follows:


func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 DefaultServeMux.HandleFunc(pattern, handler)
}

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
 if handler == nil {
  panic("http: nil handler")
 }
 mux.Handle(pattern, HandlerFunc(handler))
}

func Handle(pattern string, handler Handler) { 
 DefaultServeMux.Handle(pattern, handler)
}

You can see that both of these functions end up withDefaultServeMuxcallHandleMethod to complete the registration of routes.

Here we encounter two types of objects:ServeMuxandHandlerLet’s talk firstHandler

Handler

HandlerIs an interface:


type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

HandlerInterface is declared with the nameServeHTTPIn other words, any structure that implements thisServeHTTPMethod, then this structure is aHandlerObject. Actually gohttpServices are based onHandlerProcessing, andHandlerObject’sServeHTTPThe method is also used to deal with itrequestAnd buildresponseThe core logic of.

Back up thereHandleFuncFunction, notice this line of code:


mux.Handle(pattern, HandlerFunc(handler))

Some people may think thatHandlerFuncIs a function that encapsulates thehandlerFunction, which returns aHandlerObject. But here it isHandlerFuncIn fact, it willhandlerThe function does aType conversionTake a lookHandlerFuncDefinition of:


type HandlerFunc func(ResponseWriter, *Request)

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

HandlerFuncIs a type, but represents afunc(ResponseWriter, *Request)The function type of the signature, and this type implementsServeHTTPMethod (inServeHTTPIn other words, this type of function is actually aHandlerObject of type. With this type conversion, we can convert ahandlerConvert to a function

HandlerObject, instead of defining a structure, and then allowing this structure to implementServeHTTPmethod. Readers can experience this technique.

ServeMux

Routing in golang (i.eMultiplexer)Based onServeMuxTake a look at the structureServeMuxDefinition of:


type ServeMux struct {
 mu sync.RWMutex
 m  map[string]muxEntry
 es []muxEntry // slice of entries sorted from longest to shortest.
 hosts bool  // whether any patterns contain hostnames
}

type muxEntry struct {
 h  Handler
 pattern string
}

Here’s the focusServeMuxFields inmThis is amapkeyIs the routing expression,valueIt’s amuxEntryStructure,muxEntryStructure stores the corresponding routing expression andhandler

It is worth noting that,ServeMuxIt’s doneServeHTTPmethod:


func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 if r.RequestURI == "*" {
  if r.ProtoAtLeast(1, 1) {
   w.Header().Set("Connection", "close")
  }
  w.WriteHeader(StatusBadRequest)
  return
 }
 h, _ := mux.Handler(r)
 h.ServeHTTP(w, r)
}

in other wordsServeMuxSo is the structureHandlerObject, butServeMuxOfServeHTTPMethods are not used to deal with specificrequestAnd buildresponseIs used to determine the route registrationhandler

Register route

Find outHandlerandServeMuxAfter that, let’s go back to the previous code:


DefaultServeMux.Handle(pattern, handler)

thereDefaultServeMuxRepresents a defaultMultiplexer, when we didn’t create a customMultiplexer, a defaultMultiplexer

And then take a lookServeMuxOfHandleWhat is done in this method

func (mux *ServeMux) Handle(pattern string, handler Handler) {
 mux.mu.Lock()
 defer mux.mu.Unlock()

 if pattern == "" {
  panic("http: invalid pattern")
 }
 if handler == nil {
  panic("http: nil handler")
 }
 if _, exist := mux.m[pattern]; exist {
  panic("http: multiple registrations for " + pattern)
 }

 if mux.m == nil {
  mux.m = make(map[string]muxEntry)
 }
 //Create a muxentry object using the current route and handler
 e := muxEntry{h: handler, pattern: pattern}
 //Add a new route matching rule to the map [string] muxentry of servemux
 mux.m[pattern] = e
 //If the routing expression ends with '/', the corresponding muxentry object is added to the [] muxentry and sorted according to the length of the routing expression
 if pattern[len(pattern)-1] == '/' {
  mux.es = appendSorted(mux.es, e)
 }

 if pattern[0] != '/' {
  mux.hosts = true
 }
}

HandleMethods mainly do two things: one is toServeMuxOfmap[string]muxEntryAdd the given route matching rule; then, if the routing expression uses'/'At the end, the correspondingmuxEntryObject added to[]muxEntryAccording to the length of the routing expression. The former is easy to understand, but the latter may not be easy to see what role it has. This issue will be analyzed later.

Custom servemux

We can also create customServeMuxReplace the defaultDefaultServeMux


package main

import (
 "fmt"
 "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "hello world")
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
 w.Header().Set("Content-Type", "text/html")
 html := `<!doctype html>
 <META http-equiv="Content-Type" content="text/html" charset="utf-8">
 <html lang="zh-CN">
   <head>
     <title>Golang</title>
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;" />
   </head>
   <body>
    <div>Welcome!</div>
   </body>
 </html>`
 fmt.Fprintf(w, html)
}

func main() {
 mux := http.NewServeMux()
 mux.Handle("/", http.HandlerFunc(indexHandler))
 mux.HandleFunc("/welcome", htmlHandler)
 http.ListenAndServe(":8001", mux)
}

NewServeMux()You can create aServeMuxExample, mentioned earlierServeMuxIt’s doneServeHTTPMethod, somuxIt’s also aHandlerObject. aboutListenAndServe()Method, if thehandlerParameters are customServeMuxexamplemux, soServerThe routing object received by the instance will no longer beDefaultServeMuxIt ismux

Open service

First fromhttp.ListenAndServeThis method begins:


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(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

Here we first create oneServerObject with the address andhandlerParameters are then calledServerobjectListenAndServe()method.

to glance atServerThis structure,ServerThere are many fields in the structure. You can have a general understanding first:


type Server struct {
 Addr string // TCP address to listen on, ":http" if empty
 Handler Handler // handler to invoke, http.DefaultServeMux if nil
 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

 disableKeepAlives int32  // accessed atomically.
 inShutdown  int32  // accessed atomically (non-zero means we're in Shutdown)
 nextProtoOnce  sync.Once // guards setupHTTP2_* init
 nextProtoErr  error  // result of http2.ConfigureServer if used

 mu   sync.Mutex
 listeners map[*net.Listener]struct{}
 activeConn map[*conn]struct{}
 doneChan chan struct{}
 onShutdown []func()
}

stayServerOfListenAndServeMethod, the listening address is initializedAddrAt the same timeListenMethod to set up listening. The last incoming TCP object will listenServemethod:

func (srv *Server) Serve(l net.Listener) error {
 ...

 baseCtx := context.Background() // base is always background, per Issue 16220
 ctx := context.WithValue(baseCtx, ServerContextKey, srv)
 for {
  RW, e: = l.accept() // wait for new connection to be established

  ...

  c := srv.newConn(rw)
  c.setState(c.rwc, StateNew) // before Serve can return
  Go c.serve (CTX) // create a new coroutine to process the request
 }
}

There are some details hidden here to understandServeThe main logic of the method. First, create a context object and then call it.ListenerOfAccept()Wait for a new connection to be established; once a new connection is established, callServerOfnewConn()Create a new connection object and mark the status of the connection asStateNew, and then open a new onegoroutineProcess connection requests.

Processing connections

We continue to exploreconnOfserve()Method, this method is also very long, we only look at the key logic. Hold on. We’ll see the sea soon.

func (c *conn) serve(ctx context.Context) {

 ...

 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)
  }

  ...

  // 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. Set state (c.rwc, stateidle) // after processing the request, set the connection state to idle
  C. curReq.Store (((* response) (NIL)) // sets the current request to null

  ...
 }
}

When a connection is established, all requests in the connection are processed in the process until the connection is closed. stayserve()Method is called in a loopreadRequest()Method reads the next request for processing. The most critical logic is a line of code:


serverHandler{c.server}.ServeHTTP(w, w.req)

Further explanationserverHandler


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

stayserverHandlerOfServeHTTP()In the methodsh.srv.HandlerIn fact, we were inhttp.ListenAndServe()In theHandlerObject, which is our customServeMuxObject. IfHandlerThe object isnil, the defaultDefaultServeMux。 Last callServeMuxOfServeHTTP()Method to match the current routehandlermethod.

The latter logic is relatively simple and clear, mainly in callingServeMuxOfmatchMethod to the corresponding registered routing expression andhandler


// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
 if r.RequestURI == "*" {
  if r.ProtoAtLeast(1, 1) {
   w.Header().Set("Connection", "close")
  }
  w.WriteHeader(StatusBadRequest)
  return
 }
 h, _ := mux.Handler(r)
 h.ServeHTTP(w, r)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
 mux.mu.RLock()
 defer mux.mu.RUnlock()

 // Host-specific pattern takes precedence over generic ones
 if mux.hosts {
  h, pattern = mux.match(host + path)
 }
 if h == nil {
  h, pattern = mux.match(path)
 }
 if h == nil {
  h, pattern = NotFoundHandler(), ""
 }
 return
}

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
 // Check for exact match first.
 v, ok := mux.m[path]
 if ok {
  return v.h, v.pattern
 }

 // Check for longest valid match. mux.es contains all patterns
 // that end in / sorted from longest to shortest.
 for _, e := range mux.es {
  if strings.HasPrefix(path, e.pattern) {
   return e.h, e.pattern
  }
 }
 return nil, ""
}

staymatchIn the method, we see the previous mentionedmap[string]muxEntryand[]muxEntry。 In this method, the first step is to use themap[string]muxEntryIf there is no matching routing rule, approximate matching will be performed.

For similar/path1/path2/path3For such a route, if an exact matching routing rule cannot be found, it will match the registered parent route closest to the current route/path1/path2/If it is registered, then the route will be matched. Otherwise, continue to match the parent route and know the root route/

because[]muxEntryMediummuxEntryAccording to the routing expression, it is sorted from long to short, so the route to be matched must be the closest among the registered parent routes.

So far, go implements thehttp serverThe general principle of the introduction finished!

summary

Golang passedServeMuxA multiplexer is defined to manage the routing, and theHandlerThe interface defines the uniform specification of routing processing function, that isHandlerMust be realizedServeHTTPMethods; at the same timeHandlerThe interface provides powerful extensibility, which is convenient for developers to pass throughHandlerThe interface implements all kinds of middleware. I believe you can feel it when you read itHandlerObject inserverService implementation is really ubiquitous. I understandserverThe basic principle of implementation, you can read some third-party on this basishttp serverFramework, as well as writing middleware for specific functions.

above.

reference material

[golang standard library document — net / HTTP]

The above is the whole content of this article, I hope to help you in your study, and I hope you can support developeppaer more.

Recommended Today

JS generate guid method

JS generate guid method https://blog.csdn.net/Alive_tree/article/details/87942348 Globally unique identification(GUID) is an algorithm generatedBinaryCount Reg128 bitsNumber ofidentifier , GUID is mainly used in networks or systems with multiple nodes and computers. Ideally, any computational geometry computer cluster will not generate two identical guids, and the total number of guids is2^128In theory, it is difficult to make two […]