HTTP Router

Gin is a kind of strong liquor from the Netherlands.

One oft-mentioned Web framework in Go, gin Web, has high performance, flexible customization features, and since it is so promising, it is worth taking a look at what it is based on before diving into it.

Thinking about drinking: Httprouter

Gin’s high performance is thanks to an httprouter framework, as described by Git authors. We will start with HTTprouter, starting with HTTP routing, as an introduction to Gin.

The Router routing

Before going further, let’s first sort out the concept of routing: Routing roughly means to realize interconnection through forwarding packets. For example, physical router is common in daily life, which refers to the distribution of information flow between the internal network and the external network. Similarly, there is the concept of routing at the software level, which is generally exposed at the business level to forward requests to the appropriate logical processor.

The router matches incoming requests by the request method and the path.

In the application of the program, it is common for the external server to call the external request to the Nginx gateway, and then route (forward) to the internal service or the “control layer” of the internal service, such as The springMVC of Java, the native Router of Go, and so on, to forward different requests to different business layers. Or more concretely, for example, different parameters call the same method, such as Java overloading, can also be understood as a program based on the different parameters to route to the corresponding different methods.

httprouter

Functional phenomena:

Httprouter starts an HTTP server, listens on port 8080, and performs request parameter resolution with just a few lines of code. When I first saw this implementation, I really thought it was pretty elegant.

router.GET("/", Index)
// Pass the parameter name
router.GET("/hello/:name", Hello)

func Hello(w http.ResponseWriter, r *http.Request) {
    // Get the parameters of the Request URL from the context of the http.Request structure
    params := httprouter.ParamsFromContext(r.Context())
    fmt.Fprintf(w, "hello, %s! \n", params.ByName("name"))}// Start the listener
http.ListenAndServe(": 8080", router)

Copy the code

Interface implementation

After looking at how to build a listener, and exploring how this elegance encapsulates the implementation, let’s first understand that in native Go, each Router routing structure implements the HTTP. Handler interface.Handler has only one method body, ServerHTTP, which has only one function. It’s processing the request and responding to it.

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

As an aside, Go tends to KISS or single responsibility, which simplifies the functions of each interface. If necessary, combination will be used instead of inheritance, and it will be regarded as a coding specification later.

net\http\server

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

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

In the Go native library, the ServeHTTP() implementation body HandlerFunc is a func function type, and the implementation is directly handled by HandlerFunc.

All in All, if we want to build an HTTP processing logic using the native HTTP \server package, we can usually do 1:

  1. Define a function whose argument list is (ResponseWriter, *Request)
  2. Register it tohttp.ServerAs itsHandlerMembers of the
  3. The call ListenAndServer listens on http.server

Method 2:

  1. Define a structure and implement the interfaceServeHTTP(w http.ResponseWriter, req *http.Request)
  2. Register it tohttp.ServerAs itsHandlerMembers of the
  3. The call ListenAndServer listens on http.server

The following is an example:

1 / / way
func SelfHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "

Hello!

"
)}2 / / way type HelloHandler struct{}HelloHandler implements the ServeHTTP() interface func (* HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, "

Hello!

"
) } s := &http.Server{ Addr: ": 8080".1 / / way Handler: SelfHandler, 2 / / way //Handler: &HelloHandler{}, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } s.ListenAndServe() Copy the code

Localhost :8080/ ABC /1 Q: How do we Get ABC or 1? A: Actually, there is A relatively crude method, which is hard solution:

  • usingnet/urltheParse()function8080I’m going to extract the next paragraph
  • usestings.split(ul, "/")
  • Use subscripts for parameter ranges

The sample is as follows

func TestStartHelloWithHttp(t *testing.T)  {
    //fmt.Println(path.Base(ul))
    
    ul := `https://localhost:8080/pixeldin/123`
    parse, e := url.Parse(ul)
    ife ! =nil {
    	log.Fatalf("%v", e)
    }
    //fmt.Println(parse.Path)	// "/pixeldin/123"
    name := GetParamFromUrl(parse.Path, 1)
    id := GetParamFromUrl(parse.Path, 2)
    fmt.Println("name: " + name + ", id: " + id)	
}

// Specify that the subscript returns a value relative to the URL
func GetParamFromUrl(base string, index int) (ps string) {
    kv := strings.Split(base, "/")
    assert(index < len(kv), errors.New("index out of range."))
    return kv[index]
}

func assert(ok bool, err error)  {
    if! ok {panic(err)
    }
}
Copy the code

Output:

name: pixeldin, id: 123
Copy the code

This approach feels violent, requires remembering the location and value of each parameter, and multiple urls cannot be managed uniformly, traversing each address. Although the Go library does provide some generic functions, such as the following: GET url: https://localhost:8080/? Key =hello, which can be obtained by *http.Request. This Request method is to declare the key/value pair in the URL, and then extract it based on the Request key.

Harvest since: / / https://golangcode.com/get-a-url-parameter-from-a-request/
func handler(w http.ResponseWriter, r *http.Request) {
    
    keys, ok := r.URL.Query()["key"]
    
    if! ok ||len(keys[0]) < 1 {
        log.Println("Url Param 'key' is missing")
        return
    }
    
    // Query()["key"] will return an array of items, 
    // we only want the single item.
    key := keys[0]
    
    log.Println("Url Param 'key' is: " + string(key))
}
Copy the code

However, the sea has been difficult to water. Httprouter: Httprouter: HttprOuter: HttprOuter: HttprOuter: HttprOuter: HttprOuter: HttprOuter: HttprOuter: HttprOuter

router.GET("/hello/:name", Hello)
router.GET("/hello/*name", HelloWorld)
Copy the code

I’ll leave it at that, and we’ll trace the underlying implementation behind them and how url parameters are planned.


An implementation of Httprouter to ServerHTTP()

As mentioned earlier, all routing structures implement the ServeHTTP() method of the HTTP.handler interface. Let’s take a look at httprOuter based on that.

julienschmidt\httprouter

Httprouter ServeHTTP() is an httprouter that provides a Router and a tree for retrieving urls. It is also an HTTP.

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ifr.PanicHandler ! =nil {
    	defer r.recv(w, req)
    }
    
    path := req.URL.Path
    
    ifroot := r.trees[req.Method]; root ! =nil {
        //getValue() returns a list of processing methods and arguments
    	ifhandle, ps, tsr := root.getValue(path); handle ! =nil {
    	    // Match execution
    		handle(w, req, ps)
    		return
    	} else ifreq.Method ! = http.MethodConnect && path ! ="/" {
    		/ /...}}if req.Method == http.MethodOptions && r.HandleOPTIONS {
    	// Handle OPTIONS requests
    	/ /...
    } else if r.HandleMethodNotAllowed { // Handle 405
    	// Execute the default handler...
    }
    
    // Handle 404
    ifr.NotFound ! =nil {
    	r.NotFound.ServeHTTP(w, req)
    } else {
    	http.NotFound(w, req)
    }
}
Copy the code

From here you can roughly guess that it injects the processing method into the internal trees structure, uses the incoming URL to match the lookup in the trees, and executes the execution chain accordingly. We can guess that the Router. Trees contains handle and the corresponding parameters. Then we can go into its routing index function to see how it implements ++ URL matching ++ and ++ parameter parsing ++.

The key is HTTP methods(e.g. GET, HEAD, POST, PUT, etc.). The method is the node bound to the current method

I add some comments in the source code, I believe you can easily understand.

// Handle registers a new request handle with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
// ...
func (r *Router) Handle(method, path string, handle Handle) {
    if len(path) < 1 || path[0] != '/' {
    	panic("path must begin with '/' in path '" + path + "'")}// Register the URL for the first time and initialize trees
    if r.trees == nil {
    	r.trees = make(map[string]*node)
    }
    
    // Bind the HTTP methods root node. Methods can be GET, POST, PUT, etc
    root := r.trees[method]
    if root == nil {
    	root = new(node)
    	r.trees[method] = root
    
    	r.globalAllowed = r.allowed("*"."")}// The HTTP methods method tree path partition
    root.addRoute(path, handle)
}
Copy the code

The router.treesmap key-value pair is a node structure. Each HTTP METHOD is a root node. The main path assignment is by the addRoute() function on these nodes. In simple terms, eventually paths with the same prefix are bound to the same branch of the tree, directly improving the efficiency of indexing.

Let me first list some important members of Node:

type node struct {
    path      string
    // Indicates whether path is followed by ':', which is used to determine parameters
    wildChild bool
    /* The current node type, default is 0, (root/param/catchAll) (root/ parameter/full path) */
    nType     nodeType
    maxParams uint8
    // The priority of the current node. The more child nodes that hang on it, the higher the priority
    priority  uint32
    indices   string
    // The child node that satisfies the prefix can be extended
    children  []*node
    // The processing block bound to the current node
    handle    Handle
}
Copy the code

Among them, the more child nodes, or the more root nodes bound to handle, the higher priority will be. The author consciously prioritized each registration completion. To quote the author’s note:

This helps in two ways:

  • Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
  • It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.

Nodes with high priority are conducive to rapid positioning of Handle. I believe it is easy to understand. In reality, places with dense human traffic are often crossroads, similar to transportation hubs. Matching based on the prefix tree helps improve efficiency by starting the addressing in dense places.

We will register the router with some GET processing logic provided by the author. Then we will debug the router to see how this tree member changes as the URL is added.

router.Handle("GET"."/user/ab/".func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    //do nothing, just add path+handler
})

router.Handle("GET"."/user/abc/".func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    //do nothing, just add path+handler
})

router.Handle(http.MethodGet, "/user/query/:name".func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    routed = true
    want := httprouter.Params{httprouter.Param{"name"."gopher"}}
    if! reflect.DeepEqual(ps, want) { t.Fatalf("wrong wildcard values: want %v, got %v", want, ps)
    }
})
Copy the code

Router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () {router.handler () { So let’s just go through the logic


// Place the current URL and processing logic on the current node
func (n *node) addRoute(path string, handle Handle) {
	fullPath := path
	n.priority++
	// Extract the number of current URL parameters
	numParams := countParams(path)

	// If the current node already has a registered link
	if len(n.path) > 0 || len(n.children) > 0 {
	walk:
		for {
			// Update the maximum number of parameters
			if numParams > n.maxParams {
				n.maxParams = numParams
			}

			// Determine whether the url to be registered overlaps with the existing URL and extract the longest overlapped index
			// This also implies that the common prefix contains no ':' or '*'
			// since the existing key can't contain those chars.
			i := 0
			max := min(len(path), len(n.path))
			for i < max && path[i] == n.path[i] {
				i++
			}

			/* If the current node is /user/ab/ func1 and the new node is /user/ ABC/func2, then we need to create /user/ab child nodes/and c/ tree as follows: | - / user/ab | -- -- -- -- -- -- -- - | - / func1 | -- -- -- -- -- -- -- - | c/func2 if then register a/user/a/and func3 - a non-class function tree will eventually adjusted as: Priority 3 | - / user/a priority 2 | -- -- -- -- -- -- -- - | | priority 1 b -- -- -- -- -- -- -- -- -- - | | - / func1 priority 1 -- -- -- -- -- -- -- -- -- - | | - c/func2 priority 1 -- -- -- -- -- -- -- - | - / func3 - a non-class function * /
			if i < len(n.path) {
				child := node{
					path:      n.path[i:],
					wildChild: n.wildChild,
					nType:     static,
					indices:   n.indices,
					children:  n.children,
					handle:    n.handle,
					priority:  n.priority - 1,}// Iterate over the child node and take the highest priority as the parent node's priority
				for i := range child.children {
					if child.children[i].maxParams > child.maxParams {
						child.maxParams = child.children[i].maxParams
					}
				}

				n.children = []*node{&child}
				// []byte for proper unicode char conversion, see #65
				n.indices = string([]byte{n.path[i]})
				n.path = path[:i]
				n.handle = nil
				n.wildChild = false
			}

			// Make new node a child of this node
			if i < len(path) {
				path = path[i:]

				if n.wildChild {
					n = n.children[0]
					n.priority++

					// Update maxParams of the child node
					if numParams > n.maxParams {
						n.maxParams = numParams
					}
					numParams--

					// Check if the wildcard matches
					if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
						// Adding a child to a catchAll is not possiblen.nType ! = catchAll &&// Check for longer wildcard, e.g. :name and :names
						(len(n.path) >= len(path) || path[len(n.path)] == '/') {
						continue walk
					} else {
						// Wildcard conflict
						var pathSeg string
						if n.nType == catchAll {
							pathSeg = path
						} else {
							pathSeg = strings.SplitN(path, "/".2) [0]
						}
						prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
						panic("'" + pathSeg +
							"' in new path '" + fullPath +
							"' conflicts with existing wildcard '" + n.path +
							"' in existing prefix '" + prefix +
							"'")
					}
				}

				c := path[0]

				// slash after param
				if n.nType == param && c == '/' && len(n.children) == 1 {
					n = n.children[0]
					n.priority++
					continue walk
				}

				// Check if a child with the next path byte exists
				for i := 0; i < len(n.indices); i++ {
					if c == n.indices[i] {
					    // Increase the priority of the current node and adjust its position
						i = n.incrementChildPrio(i)
						n = n.children[i]
						continue walk
					}
				}

				// Otherwise insert it
				ifc ! =':'&& c ! =The '*' {
					// []byte for proper unicode char conversion, see #65
					n.indices += string([]byte{c})
					child := &node{
						maxParams: numParams,
					}
					n.children = append(n.children, child)
					n.incrementChildPrio(len(n.indices) - 1)
					n = child
				}
				n.insertChild(numParams, path, fullPath, handle)
				return

			} else if i == len(path) { // Make node a (in-path) leaf
				ifn.handle ! =nil {
					panic("a handle is already registered for path '" + fullPath + "'")
				}
				n.handle = handle
			}
			return}}else { // Empty tree
		n.insertChild(numParams, path, fullPath, handle)
		n.nType = root
	}
}
Copy the code

The general idea above is to register each Handle func() to a URL prefix tree and branch according to the same matching degree of URL prefix to improve routing efficiency.

Httprouter: httprouter: httprouter: httprouter: httprouter: httprouter: httprouter: httprouter: httprouter:

type Param struct {
	Key   string
	Value string
}
Copy the code

The concept of context is also common in other languages, such as application-Context in the Java Spring framework, which is used throughout the program lifecycle to manage global properties. Go context also has various implementations in different frameworks. Here we first learn that the top context of Go program is background(), which is the source of all subcontexts, similar to the Init () process in Linux system.

Context context context context context context

func TestContext(t *testing.T) {
	// Get the top-level context
	ctx := context.Background()
	// Write the string value in the context. Note that a new value context is returned
	valueCtx := context.WithValue(ctx, "hello"."pixel")
	value := valueCtx.Value("hello")
	ifvalue ! =nil {
	    P, _ := ctx.Value(ParamsKey).(Params) The underscore is the bool returned by the go assertion */
		fmt.Printf("Params type: %v, value: %v.\n", reflect.TypeOf(value), value)
	}
}
Copy the code

Output:

Params type: string, value: pixel.
Copy the code

In HttprOuter, the context encapsulated in http.request is a valueCtx, which is of the same type as valueCtx. The framework provides a method to retrieve Params key/value pairs from the context.

func ParamsFromContext(ctx context.Context) Params {
	p, _ := ctx.Value(ParamsKey).(Params)
	return p
}
Copy the code

Using the returned Params to get our target value based on the key,

params.ByName(key)
Copy the code

Params is derived from a function called getValue(Path String) (handle handle, p Params, TSR bool).

// part of the ServeHTTP() function
ifroot := r.trees[req.Method]; root ! =nil {
    /** getValue(), returns a list of methods and arguments **/
	ifhandle, ps, tsr := root.getValue(path); handle ! =nil {
	    // Match execution, where handle is the anonymous function func above
		handle(w, req, ps)
		return
	} else ifreq.Method ! = http.MethodConnect && path ! ="/" {
		/ /...}}Copy the code

ServeHTTP() has a getValue function that returns two important members: the processing logic of the current route and the URL parameter list, so we need to pass params as an input parameter during route registration. Something like this:

router.Handle(http.MethodGet, "/user/query/:name".// The anonymous function func, where the ps argument is extracted for you at ServeHttp()
func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	fmt.Println(params.ByName("name"))})Copy the code

GetValue () checks the type of node N. If the node is of the param type (it has been classified by URL in addRoute), it fills in the parameter Params to be returned.

//-----------go
/ /... github.com/julienschmidt/[email protected]/tree.go:367
switch n.nType {
case param:
	// find param end (either '/' or path end)
	end := 0
	for end < len(path) && path[end] ! ='/' {
		end++
	}
	
    // Node parameters are encountered
	if p == nil {
		// lazy allocation
		p = make(Params, 0, n.maxParams)
	}
	i := len(p)
	p = p[:i+1] // expand slice within preallocated capacity
	p[i].Key = n.path[1:]
	p[i].Value = path[:end]
/ /...
Copy the code

Httprouter: httprouter: httprouter: httprouter:

  1. Initializing the creation of a router router
  2. The registration signature is:type Handle func(http.ResponseWriter, *http.Request, Params)To the router
  3. Invoke the HTTP generic interfaceServeHTTP(), used to extract the expected parameters of the current URL and for use by the business layer

Httprouter is an extension of Httprouter. It is an extension of Httprouter. It is an extension of Httprouter.

Reference links:

Julienschmidt/httprouter github.com/julienschmi…