Go is a universal programming language. If you want to learn Web development in Go, you must know how to use Go to start an HTTP server to receive and respond to HTTP requests from clients. It is very easy to implement an HTTP server with Go. The Go language standard library net/ HTTP comes with a series of structures and methods to help developers simplify the process of developing HTTP services. Therefore, we can build and start a highly concurrent HTTP server without relying on any third party components. This article will learn how to write and implement an HTTP Serve with NET/HTTP and explore its implementation principles to learn common paradigms and design ideas for network programming.
HTTP service processing flow
The service standard model based on HTTP consists of two sides, Client side and Server side. The HTTP request is sent from the client, the server receives the request, processes it, and returns the response to the client. So the job of the HTTP server is to accept the request from the client and return the response to the client.
The processing flow of a typical HTTP service is as follows:
When a server receives a request, it first enters a router, also known as a Multiplexe. The job of routing is to find the corresponding handler for the request. The handler constructs the response after processing the received request and returns it to the client. The HTTP Server implemented by Go follows the same process.
Let’s look at how Go implements a simple HTTP server that returns “Hello World” :
package main
import (
"fmt"
"net/http"
)
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")}func main (a) {
http.HandleFunc("/", HelloHandler)
http.ListenAndServe(": 8000".nil)}Copy the code
After running the code, open localhost:8000 in your browser and see Hello World. This code registers a HelloHandler on the root route/using http.handlefunc, then starts the server using http.listenandServe and listens on port 8000 locally. When a request comes in, the corresponding handler function is executed according to the route.
Let’s look at another common implementation:
package main
import (
"fmt"
"net/http"
)
type HelloHandlerStruct struct {
content string
}
func (handler *HelloHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, handler.content)
}
func main(a) {
http.Handle("/", &HelloHandlerStruct{content: "Hello World"})
http.ListenAndServe(": 8000".nil)}Copy the code
Instead of using the http.handlefunc function, this code calls http.handle directly and passes in an instance of our custom HTTP.handler interface.
The HTTP service implemented by Go is as simple as registering a route, then creating the service and enabling listening. Below, we’ll look at how Go implements the HTTP service by registering the route, starting the service, processing the request, and closing the service.
Routing registered
Both http.HandleFunc and http.Handle are used to specify handlers for routing rules, The first parameter to http.handlefunc is the routing pattern. The second parameter is a function signed func(w http.responsewriter, r * http.requests). The second argument to HTTP. Handle is an instance of the type that implements the http.Handler interface.
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))
}
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
Copy the code
You can see that both functions end up registering the route handler by DefaultServeMux calling the Handle method. Here we encounter two types of objects: ServeMux and Handler.
Handler
Http. Handler is the interface defined in NET/HTTP to represent HTTP requests:
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. The ServeHTTP method of the Handler object reads the Request for logical processing and then writes the header information and response content to ResponseWriter.
Returning to the HandleFunc function above, it calls * servemux.handlefunc to register the handler with the specified routing rule:
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
Notice this line of code:
mux.Handle(pattern, HandlerFunc(handler))
Copy the code
HandlerFunc converts the handler function to http.handlerfunc. Take a look at the HandlerFunc definition:
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
Copy the code
The HandlerFunc type represents a function type with a func(ResponseWriter, *Request) signature that implements the ServeHTTP method (which calls the converted function itself in its implementation of the ServeHTTP method). That is, a function of this type is actually an object of type Handler. With this type conversion, we can convert an ordinary function with a func(ResponseWriter, *Request) signature into a Handler object without having to define a structure that implements the ServeHTTP method.
Service Multiplexer
As you can see from the above code, both http.HandleFunc and http.Handle are used to register route handlers using the ServerMux construct’s Handle method.
Let’s start with 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
In ServeMux, field M is a map, key is a route expression, and value is a muxEntry structure. The muxEntry structure stores the route expression and the corresponding handler. The map corresponding to field M is used for exact route matching and the SLICE of es field is used for partial route matching, which will be discussed in detail in the route matching section.
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 a specific request and construct a response. Instead, it is used to find the corresponding routing Handler object through the route. Call the route handler’s ServeHTTP method to process the request and build the Reponse.
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 here represents a DefaultServeMux instance. In the example above, we did not create a custom ServeMux, so DefaultServeMux will be used automatically
The Handle method in ServeMux registers the route handler function:
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")}// The route has already registered the handler function, the direct panic
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 route pattern and handler
e := muxEntry{h: handler, pattern: pattern}
// Add a new route matching rule to the M field of the ServeMux
mux.m[pattern] = e
if pattern[len(pattern)- 1] = ='/' {
// If route patterm ends in '/', the corresponding muxEntry object is added to []muxEntry with route length at the beginning of the slice
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true}}Copy the code
When the Handle method registers a route, it does two things: one is to add a given route matching 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 in reverse order of route expression length. The former is used for exact route matching, while the latter is used for partial route matching.
Custom ServeMux
Using http.newServemux (), you can create a ServeMux instance instead of the default DefaultServeMux
ListenAndServe() = ListenAndServe(); ListenAndServe() = ListenAndServe(); ListenAndServe() = ListenAndServe(); Add a /welcome route (Handle and HandleFunc are used to register the route) :
package main
import (
"fmt"
"net/http"
)
type WelcomeHandlerStruct struct{}func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")}func (*WelcomeHandlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome")}func main (a) {
mux := http.NewServeMux()
mux.HandleFunc("/", HelloHandler)
mux.Handle("/welcome", &WelcomeHandlerStruct{})
http.ListenAndServe(": 8080", mux)
}
Copy the code
As mentioned earlier, ServeMux also implements ServeHTTP methods, so mux is also a Handler object. For the ListenAndServe() method, if the second argument is a custom ServeMux instance, the ServeMux service multiplexer object received by the Server instance will no longer be DefaultServeMux but muX.
Start the service
After the route is registered, use the HTTP.ListenAndServe method to start the server to listen for requests coming from the specified port.
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
This first creates a Server object, passes in the address and handler parameters (the handler parameters here are ServeMux instances), and then calls the Server object ListenAndServe() method.
Server (Server object)
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
nextProtoOnce sync.Once
nextProtoErr error
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}// Active connection
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, the MONITORED TCP object is passed to its Serve method. The Serve method of the Server object receives incoming connections from the Listener, creates a Goroutine for each connection, and in the Goroutine the request is processed with a routing Handler and the response is constructed.
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.acept ()// Receives a network connection request from the listener...... C := srv.newconn (rw) c.setState(c.wc, StateNew) // Put the connection in the map of server.activeconn go c.serve(CTX)// Create a 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 receive the network connection being listened on. 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 Goroutine to process the connection request.
Deal with connection
Conn’s serve() in the enabled Goroutine does a route match find the route handler and call the handler. This method is long, so let’s keep the key logic.
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)
}
...
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. The serve() method loops through the readRequest() method to read the next request for processing. The most critical logic is the following line of code:
serverHandler{c.server}.ServeHTTP(w, w.req)
Copy the code
ServerHandler is a structure type that proxies the Server object:
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 ServeHTTP() method implemented by serverHandler is the Handler parameter 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.
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(r *Request) (h Handler, pattern string) {
if r.Method == "CONNECT" {
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
return mux.handler(r.Host, r.URL.Path)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
ifpath ! = r.URL.Path { _, pattern = mux.handler(host, path) url := *r.URL url.Path = pathreturn RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
return mux.handler(host, r.URL.Path)
}
// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
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.
ServeHTTP method, which finally calls the Handler’s ServeHTTP method to process the request and build the write response:
h.ServeHTTP(w, r)
Copy the code
In fact, if the Handler can’t be found based on the route, the NotFoundHandler is also returned:
func NotFound(w ResponseWriter, r *Request) { Error(w, "404 page not found", StatusNotFound) }
func NotFoundHandler(a) Handler { return HandlerFunc(NotFound) }
Copy the code
In this way, a call to H.sever HTTP(w, r) will result in a 404 error message being written to the response.
Stop the service
We have written HTTP Servers that can listen to network connections, route requests to processor functions to process them and return responses, but we need to be able to gracefully shut down the service. In a production environment, we need to restart the service when we need to update the server program, but some of the requests may be halfway through. These requests can have unexpected results if forcibly interrupted.
Starting with Go 1.8, net/ HTTP native support uses http.shutdown to gracefully ShutDown HTTP services. This scenario also requires the user to create a custom HTTP. Server object, because the Shutdown method cannot be called any other way.
Let’s look at the following code, which gracefully stops the server by combining Signal capture, Goroutine capture, and Channel capture:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
func main(a) {
mux := http.NewServeMux()
mux.Handle("/", &helloHandler{})
server := &http.Server{
Addr: ": 8081",
Handler: mux,
}
// Create a system signal receiver
done := make(chan os.Signal)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func(a) {
<-done
iferr := server.Shutdown(context.Background()); err ! =nil {
log.Fatal("Shutdown server:", err)
}
}()
log.Println("Starting HTTP server...")
err := server.ListenAndServe()
iferr ! =nil {
if err == http.ErrServerClosed {
log.Print("Server closed under request")}else {
log.Fatal("Server closed unexpected")}}}type helloHandler struct{}
func (*helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")}Copy the code
This code calls server.shutdown by catching the os.interrupt signal (Ctrl+C) and the Syscall,SIGTERM signal (the signal passed to the process when it kills) Method tells the server that it should stop accepting new requests and shut down the server after processing the currently accepted requests. To distinguish it from normal errors, the library provides a specific error type, http.errServerClosed, which we can determine in our code whether the server was shut down normally or accidentally.
Net/HTTP standard library has many structures and methods to improve HTTP Server. After learning these basic methods, it will be much clearer when looking at the code of other Web frameworks. Even if the framework is too complex to use, you can wrap a scaffolding for HTTP services by yourself. (I used Echo and Gin and found it quite simple. They are scaffolding compared to PHP’s Laravel framework. Keep an eye out for my Laravel users.
Reference article:
Juejin. Cn/post / 684490…
Github.com/unknwon/bui…
Medium.com/honestbee-t…