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