Start with a Demo

They say everything is hard at the beginning, but implementing an Http Server with Go is really not that hard, how easy is it? Starting a Server and responding to requests, counting package names, imported dependencies, and even empty lines, is only 15 lines of code.

package main

import (
  "io"
  "net/http"
)

func main() {
  http.HandleFunc("/hello", hello)
  http.ListenAndServe(":81", nil)
}

func hello(response http.ResponseWriter, request *http.Request) {
  io.WriteString(response, "hello world")
}

So simple, I'm afraid the only one who can fight with it is Python, and Go can also be compiled into executable binaries, do you think it's a good idea?

How does Http Server handle connections?

Let's start with this line of code

http.ListenAndServe(":81", nil)

From the naming, this method does two things, listens and serves. I don't think it's ok in terms of the single responsibility of the method, how can one method do two things? But this is the code written by the big guy, it makes sense.

The first parameter Addr is to listen to the address and port, the second parameter Handler is generally nil, it is the real logic processing, but we usually use the first line of code like that to register the processor, the code at a glance feel that the path is mapped to the business logic, we first roughly understand, and then look at it later

http.HandleFunc("/hello", hello)

If you know a little bit of network programming basics, you will know that the operating system provides system calls like bind, listen, and accept, and we can assemble a Server by initiating the calls in order.

Go also uses these system calls and encapsulates them in ListenAndServe.

Listen is a system call down the line, so let's focus on Serve.

If we put away the branch code and just look at the main stem, we find a for loop that keeps Accepting, which blocks when there is no connection and starts a new concurrent thread to handle it when there is a connection.

How does Http Server handle requests?

Some pre-work

The line of code that handles the request is, as can be seen, a single concurrent process per connection.

go c.serve(connCtx)

The connCtx here substitutes the current Server object.

ctx := context.WithValue(baseCtx, ServerContextKey, srv)
...
connCtx := ctx
And it also provides a hook method to modify it, srv.ConnContext, which modifies the original context each time it is accepted
if cc := srv.ConnContext; cc != nil {
  connCtx = cc(connCtx, rw)
  if connCtx == nil {
    panic("ConnContext returned nil")
  }
}

It is defined as.

// ConnContext optionally specifies a function that modifies
// the context used for a new connection c. The provided ctx
// is derived from the base context and has a ServerContextKey
// value.
ConnContext func(ctx context.Context, c net.Conn) context.Context

But if you follow the code I gave at the beginning, you can't modify srv.

func main() {
  http.HandleFunc("/hello", hello)
  server := http.Server{
    Addr: ":81",
    ConnContext: func(ctx context.Context, c net.Conn) context.Context {
      return context.WithValue(ctx, "hello", "roshi")
    },
  }
  server.ListenAndServe()
}

Similarly, c.setState also provides a hook that can be set as above to execute the hook method each time the connection state changes.

c.setState(c.rwc, StateNew, runHooks) // before Serve can return
// ConnState specifies an optional callback function that is
// called when a client connection changes state. See the
// ConnState type and associated constants for details.
ConnState func(net.Conn, ConnState)

Get to work for real

To make it clear what the serve method does after Accept, let's simplify it again.

func (c *conn) serve(ctx context.Context) {
  ...
  for {
    w, err := c.readRequest(ctx)
    ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
    ...
  }
}

serve is also a big loop, inside the loop is mainly to read a request, and then the request will be handed to the Handler processing.

Why is it a big loop? Because each serve handles one connection, and one connection can have multiple requests.

Reading the request is tedious, according to the Http protocol, read out the URL, header, body and other information.

Here is a detail is in each read a request, but also opened a concurrent to read the next request, also considered to do optimization it.

for {
  w, err := c.readRequest(ctx)
  ...

  if requestBodyRemains(req.Body) {
    registerOnHitEOF(req.Body, w.conn.r.startBackgroundRead)
  } else {
    w.conn.r.startBackgroundRead()
  }
  ...
}
How are requests routed?
When a request is read, it goes to this line of code.
serverHandler{c.server}.ServeHTTP(w, w.req)

ServeHTTP finds the Handler we registered to handle it, and if the request URI is * or the request Method is OPTIONS, the globalOptionsHandler is used, which means that such requests do not need to be handled manually by us and are returned directly.

For our registered Handler also need to find the route, the rules of this route is still relatively simple, mainly by the following three.

  • If a route with host is registered, it will be searched by host + path, and if a route with host is not registered, it will be searched by path

  • If the last character of the registered routing rule is /, then in addition to the exact match, the route will be found by prefix

A few examples to understand.
  • Matching rules with host
Register the route as
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello", hello2)

At this point, if you execute

curl 'http://127.0.0.1:81/hello'

will match hello2, but if you execute

curl 'http://localhost:81/hello'
The match is hello
  • Prefix Matching

If the registration route is
http.HandleFunc("/hello", hello)
http.HandleFunc("127.0.0.1/hello/", hello2)

Note that there is a / at the end of the second one, if you execute

curl 'http://127.0.0.1:81/hello/roshi'

It also matches hello2, how about that, do you understand?

​After finding the route, we directly call the method we registered at the beginning, and if we write data to the Response, we can return to the client, so that a request is processed.

Summary

Finally, we recall the main points of Go Http Server.

  • Starting an Http Server with Go is very simple

  • The Go Http Server is essentially a big loop that starts a new concurrent process every time a new connection is made

  • The processing of each connection is also a big loop, which does three things: read the request, find the route, and execute the logic