preface

For Golang, it’s easy to implement a simple HTTP Server in just a few lines of code. At the same time, with the support of the coroutine, the HTTP server implemented by Go can achieve excellent performance. This article will take a deeper look at how the GO standard library net/ HTTP implements HTTP services, in order to learn about common paradigms and design ideas of network programming.

The HTTP service

The network application based on HTTP consists of two ends, namely, the Client and the Server. The interaction between the two ends includes sending request from the client, receiving request for processing and returning response from the server, and processing response from the client. So the job of the HTTP server is to accept the request from the client and return the response to the client.

A typical HTTP server process can be shown in the following figure:

The server receives the request, the first into the routing (router), it is a Multiplexer, routing job is to find the corresponding processor for this request (handler), the processor processes the request, and build the response. Golang’s HTTP Server also follows this process.

Let’s start by looking at how Golang implements a simple HTTP server:

package main



import (

    "fmt"

    "net/http"

)



func indexHandler(w http.ResponseWriter, r *http.Request) {

    fmt.Fprintf(w, "hello world")

}



func main(a) {

    http.HandleFunc("/", indexHandler)

    http.ListenAndServe(": 8000".nil)

}

Copy the code

After running the code, open localhost:8000 in your browser and see Hello World. This code registers an indexHandler on the root route/using http.handleFunc, and then turns on the listening using http.listenAndServe. When a request comes in, the corresponding handler function is executed according to the route.

Let’s look at another common HTTP Server implementation:

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(a) {

    http.Handle("/", &indexHandler{content: "hello world!"})

    http.ListenAndServe(": 8001".nil)

}

Copy the code

The HTTP service implemented by Go is as simple as registering a route, then creating the service and enabling listening. Below, we’ll take a look at how Golang implements the HTTP service by registering the route, enabling the service, and handling the request.

Registered routing

Http. HandleFunc and http.Handle are both used to register routes. The difference is in the second argument, which is a function signed by func(w http.ResponseWriter, r * http.requests). The latter is a structure that implements func(w http.responsewriter, r * HTTP.requests) signature methods. The source code for http.HandleFunc and http.Handle 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))

}

Copy the code
func Handle(pattern string, handler Handler) { 

    DefaultServeMux.Handle(pattern, handler)

}

Copy the code

You can see that both functions end up registering the route by DefaultServeMux calling the Handle method. Here we encounter two types of objects: ServeMux and Handler, but let’s start with Handler.

Handler

Handler is an interface:

type Handler interface {

    ServeHTTP(ResponseWriter, *Request)

}

Copy the code

The Handler interface declares a function signature named ServeHTTP, which means that any structure that implements the ServeHTTP method is a Handler object. In fact, THE HTTP service of GO is processed based on Handler, and the ServeHTTP method of Handler object is also the core logic used to process request and construct response.

Back to the HandleFunc function above, notice this line of code:

mux.Handle(pattern, HandlerFunc(handler))

Copy the code

One might think of HandlerFunc as a function that wraps the handler function passed in and returns a Handler object. HandlerFunc, however, is actually a type conversion to the handler function.

type HandlerFunc func(ResponseWriter, *Request)



// ServeHTTP calls f(w, r).

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
 {

    f(w, r)

}

Copy the code

HandlerFunc is a function type that has a func(ResponseWriter, *Request) signature and implements the ServeHTTP method (which calls itself in the ServeHTTP method). That is, a function of this type is actually an object of type Handler. With this type conversion, we can convert a handler function to a handler object without having to define a structure that implements the ServeHTTP method. The reader can experience this technique.

ServeMux

Golang in routing (namely Multiplexer) based on ServeMux structure, first look at the definition of ServeMux:

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

}

Copy the code

The field M in ServeMux is a map, the key is a routing expression, and the value is a muxEntry structure that stores the corresponding routing expression and handler.

It is worth noting that ServeMux also implements the ServeHTTP method:

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)

}

Copy the code

That is, the ServeMux structure is also a Handler object, except that the ServeMux ServeHTTP method is not used to process the specific request and construct the response, but rather to determine the Handler for the route registration.

Registered routing

Now that we know about handlers and ServeMux, let’s go back to the previous code:

DefaultServeMux.Handle(pattern, handler)

Copy the code

DefaultServeMux represents a default Multiplexer and will automatically use a default Multiplexer when we do not create custom Multiplexer.

Then let’s look at what the Handle method of the ServeMux does:

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 with the current route and handler

    e := muxEntry{h: handler, pattern: pattern}

    // Add a new route matching rule to ServeMux map[string]muxEntry

    mux.m[pattern] = e

    // If the route expression ends with a '/', the corresponding muxEntry object is added to []muxEntry and sorted by the length of the route expression

    if pattern[len(pattern)- 1] = ='/' {

        mux.es = appendSorted(mux.es, e)

    }



    if pattern[0] != '/' {

        mux.hosts = true

    }

}

Copy the code

The Handle method does two main things: one is to add a given routing rule to the Map [string]muxEntry of the ServeMux; Then, if the route expression ends with a ‘/’, the corresponding muxEntry object is added to []muxEntry and sorted by the length of the route expression. The former is easy to understand, but the latter may not be so easy to see what the effect is, which will be discussed later.

Custom ServeMux

We can also create custom ServeMux instead of the default DefaultServeMux:

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 id="app">Welcome! </div>

            </body>

    </html>`


    fmt.Fprintf(w, html)

}



func main(a) {

    mux := http.NewServeMux()

    mux.Handle("/", http.HandlerFunc(indexHandler))

    mux.HandleFunc("/welcome", htmlHandler)

    http.ListenAndServe(": 8001", mux)

}

Copy the code

NewServeMux() creates a ServeMux instance. As mentioned earlier, ServeMux also implements ServeHTTP methods, so mux is also a Handler object. For the ListenAndServe() method, if the handler argument passed in is a custom ServeMux instance MUX, the route object received by the Server instance will no longer be DefaultServeMux but muX.

Open the service

Start with the HTTP.ListenAndServe method:

func ListenAndServe(addr string, handler Handler) error {

    server := &Server{Addr: addr, Handler: handler}

    return server.ListenAndServe()

}



func (srv *Server) ListenAndServe(a) error {

    if srv.shuttingDown() {

        return ErrServerClosed

    }

    addr := srv.Addr

    if addr == "" {

        addr = ":http"

    }

    ln, err := net.Listen("tcp", addr)

    iferr ! =nil {

        return err

    }

    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})

}

Copy the code

You create a Server object, pass in the address and handler parameters, and then call the Server object ListenAndServe() method.

SQL > select * from ‘Server’; SQL > select * from ‘Server’;

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(a)

}

Copy the code

In the ListenAndServe method of the Server, the listener address Addr is initialized and the Listen method is called to set up the listener. Finally, pass the TCP object to the Serve method:

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 a 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

    }

}

Copy the code

Some details have been left out to understand the main logic of the Serve method. First create a context object, then call Accept() on the Listener to wait for a new connection to be established. Once a new connection is established, newConn() on the Server is called to create a new connection object, mark the connection’s status as StateNew, and then open a new Goroutine to process the connection request.

Deal with connection

Let’s continue exploring Conn’s serve() method, which is also very long, again focusing on the key logic. Hang in there, you’ll see the sea soon.

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



.



    for {

        w, err := c.readRequest(ctx)

        ifc.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.setState(c.rwc, StateIdle)

        c.curReq.Store((*response)(nil))



.

    }

}

Copy the code

When a connection is established, all requests for that connection are processed in the coroutine until the connection is closed. In the serve() method, the readRequest() method is looped to read the next request for processing. The most critical piece of logic is one line of code:

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

Copy the code

To further explain serverHandler:

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)

}

Copy the code

The sh.srv.handler in the serverHandler ServeHTTP() method is actually the Handler object we originally passed in http.listenandServe (), which is our custom ServeMux object. If the Handler object is nil, the default DefaultServeMux is used. Finally, ServeMux’s ServeHTTP() method is called to match the handler method corresponding to the current route.

The logic behind this is relatively straightforward: call ServeMux’s match method to match the corresponding registered routing expression and handler.

// 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.""

}

Copy the code

In the match method we see the mux m fields (type map[string]muxEntry) and es(type []muxEntry) mentioned earlier. In this method, first of all, exact matching is used to find whether there is a corresponding routing rule in map[string]muxEntry. If there is no matching routing rule, es is used for approximate matching.

As mentioned earlier, routes ending in ‘/’ (which can be called node routes) are added to []muxEntry in the ES field during route registration. For routes such as /path1/path2/path3, if the exact matching rule cannot be found, the registered parent node route closest to the current route will be matched. Therefore, if the route /path1/path2/ is registered, the route will be matched. Otherwise, the next parent node route will be matched. Until the root route /.

The muxEntry in []muxEntry is sorted according to the route expression from long to short, so the node route matched during approximate matching must be the closest one among the routes of the registered parent node.

At this point, Go implementation of HTTP server’s general principle is introduced!

conclusion

Golang uses ServeMux to define a multiplexer to manage routes, and defines a unified specification for route handling functions through the Handler interface, that is, handlers must implement ServeHTTP methods. At the same time, the Handler interface provides a powerful expansibility, convenient developers through the Handler interface to achieve a variety of middleware. Handler objects are ubiquitous in the implementation of server services. With an understanding of the fundamentals of server implementation, you can read about some third-party HTTP Server frameworks, as well as middleware for writing specific functions.

The above.

The resources

Golang Standard Library documentation – NET/HTTP