An overview of the

Go-restful is a web framework developed in go language to quickly build restful style. K8s the most core component kube-Apiserver to use the framework, the framework of the code is relatively simple, here to do a simple function, and then analyze the relevant source code.

Go restful is based on golang’s official NET/HTTP implementation. Before further study, I suggest that we take a look at my previous article on official HTTP source code analysis

Go-restful defines three important data structures:

  • Router: indicates a route, including the URL and callback processing function
  • Webservice: indicates a service
  • Container: Indicates a server

The relationship among the three is as follows:

  • Go restful supports multiple Containers. One Container is like an HTTP server. Different Containers monitor different addresses and ports
  • Each Container can contain multiple WebServices, representing a taxonomy of different services
  • Each WebService contains multiple routers, which are routed to the corresponding Handler function (Handler Func) based on the URL of the HTTP request.

Hd address

Quick learning

The introduction of package:

go get github.com/emicklei/go-restful/v3
Copy the code

Example hello-world code: loclahost:8080/hello

package main

import (
  "github.com/emicklei/go-restful/v3"
  "io"
  "log"
  "net/http"
)

func main(a) {
  / / create a WebService
  ws := new(restful.WebService)
  // Set the route and callback functions for the WebService
  ws.Route(ws.GET("/hello").To(hello))
  // Add WebService to the Container generated by default
  // The default generated container code is in the init method of web_service_container.go
  restful.Add(ws)
  // Start the service
  log.Fatal(http.ListenAndServe(": 8080".nil))}// The callback function corresponding to the route
func hello(req *restful.Request, resp *restful.Response) {
  io.WriteString(resp, "world")}Copy the code

Source code analysis

After creating WebServie and adding it to the default Container, you don’t pass the Container to anyone. You just start the service listener and it automatically identifies the Container.

To uncover the answer, let’s analyze the source code. Before I do that, I recommend reading my previous article on official HTTP source code analysis, because Go restful implements functionality based on official HTTP packages

The figure below is the core logic diagram of the source code.

Hd address

Core data structure

Route

As mentioned earlier, Route is one of the three concepts of GO-restful. The internal data structure is Route. Look at the source code first.

Source: github.com/emicklei/go-restful/router.go

type Route struct {
  Method   string
  Produces []string
  Consumes []string
  // Requested path: root path + described path
  Path     string
  // handler handles the function
  Function RouteFunction
  / / the interceptor
  Filters  []FilterFunction
  If       []RouteSelectionConditionFunction

  // cached values for dispatching
  relativePath string
  pathParts    []string
  pathExpr     *pathExpression // cached compilation of relativePath as RegExp

  // documentation
  Doc                     string
  Notes                   string
  Operation               string
  ParameterDocs           []*Parameter
  ResponseErrors          map[int]ResponseError
  DefaultResponse         *ResponseError
  ReadSample, WriteSample interface{} // structs that model an example request or response payload

  // Extra information used to store custom information about the route.
  Metadata map[string]interface{}

  // marks a route as deprecated
  Deprecated bool

  //Overrides the container.contentEncodingEnabled
  contentEncodingEnabled *bool
}
Copy the code

RouteBuilder

The RouteBuilder is used to construct the Route information, using the builder pattern as known by name

Source: github.com/emicklei/go-restful/router.go

// Most attributes are the same as Route
type RouteBuilder struct {
  rootPath    string
  currentPath string
  produces    []string
  consumes    []string
  httpMethod  string        // required
  function    RouteFunction // required
  filters     []FilterFunction
  conditions  []RouteSelectionConditionFunction

  typeNameHandleFunc TypeNameHandleFunction // required. }Copy the code

Webservice

A WebService has a set of routes that have a common rootPath and logically group together routing requests that have the same prefix

Source: github.com/emicklei/go-restful/web_service.go

type WebService struct {
  // Routes in Webservice share a rootPath
  rootPath       string
  pathExpr       *pathExpression // cached compilation of rootPath as RegExp
  routes         []Route
  produces       []string
  consumes       []string
  pathParameters []*Parameter
  filters        []FilterFunction
  documentation  string
  apiVersion     string

  typeNameHandleFunc TypeNameHandleFunction

  dynamicRoutes bool

  // Protect routes against concurrency problems in multithreaded write operations
  routesLock sync.RWMutex
}
Copy the code

Container

A Container contains multiple services. Different Containers listen on different IP addresses or ports to provide independent services.

Source: github.com/emicklei/go-restful/container.go

type Container struct {
  webServicesLock        sync.RWMutex
  // There are multiple WebServices inside the Container
  webServices            []*WebService
  ServeMux               *http.ServeMux
  isRegisteredOnRoot     bool
  containerFilters       []FilterFunction
  doNotRecover           bool // default is true
  recoverHandleFunc      RecoverHandleFunction
  serviceErrorHandleFunc ServiceErrorHandleFunction
  router                 RouteSelector // default is a CurlyRouter (RouterJSR311 is a slower alternative)
  contentEncodingEnabled bool          // default is false
}
Copy the code

Comb the core code process

Start with the code in the previous demo and analyze the call flow.

The overall process includes:

  • Create a WebService object
  • Add routing addresses and handlers for the WebService object
  • Add WebService to Container (no Containerr declared, default Container used)
  • Start the service listening port and wait for the service request
func main(a) {
  ws := new(restful.WebService)
  ws.Route(ws.GET("/hello").To(hello))
  restful.Add(ws)
  log.Fatal(http.ListenAndServe(": 8080".nil))}Copy the code

WebService adds routes

Ws.route (ws.get (“/hello”).to (hello))

Construct the RouteBuilder object
// The Get method creates a RouteBuilder to construct the Route object
func (w *WebService) GET(subPath string) *RouteBuilder {
  // Typical builder pattern usage
  return new(RouteBuilder).typeNameHandler(w.typeNameHandleFunc).servicePath(w.rootPath).Method("GET").Path(subPath)
}

// Constructor mode: Assign values to attributes
// The other methods are similar and will not be expanded
func (b *RouteBuilder) typeNameHandler(handler TypeNameHandleFunction) *RouteBuilder {
  b.typeNameHandleFunc = handler
  return b
}

// After the Get method, the attribute is not completely constructed, and the handler function is assigned with a separate To method
func (b *RouteBuilder) To(function RouteFunction) *RouteBuilder {
  b.function = function
  return b
}
Copy the code
Generate a Route object from RouteBuilder
func (w *WebService) Route(builder *RouteBuilder) *WebService {
  w.routesLock.Lock()
  defer w.routesLock.Unlock()
  // Fill in the default values
  builder.copyDefaults(w.produces, w.consumes)
  // Call the Build method of RouteBuilder to construct a Route
  // Add Route to the routes list
  w.routes = append(w.routes, builder.Build())
  return w
}

The Build method returns a Route object
func (b *RouteBuilder) Build(a) Route{... route := Route{ ... } route.postBuild()return route
}
Copy the code

Add WebService to Container

Main analysis restful.Add(WS). Special attention should be paid to:

  • Pass the HTTP DefaultServeMux to the ServeMux of DefaultServeMux
  • Call the Servemux.handlefunc function in Golang’s official HTTP package to process the request
  • The processing function is c.dispatch and dispatch, and then routes are distributed internally

Source: github.com/emicklei/go-restful/web_service_container.go

// Define the global variable as the default Container
var DefaultContainer *Container

// init is automatically triggered when another package is imported. That is, when the Go-restful framework is referenced, there is a Container by default
func init(a) {
  DefaultContainer = NewContainer()
  // The default route object DefaultServeMux under the standard HTTP package in Golang is assigned to the ServeMux of Container
  // Pay special attention here, because the logic of this place can answer the previous question. Go -restful and HTTP libraries, using this assignment to establish an association.
  DefaultContainer.ServeMux = http.DefaultServeMux
}

// Generate the default container
func NewContainer(a) *Container {
  return &Container{
    webServices:            []*WebService{},
    ServeMux:               http.NewServeMux(),
    isRegisteredOnRoot:     false,
    containerFilters:       []FilterFunction{},
    doNotRecover:           true,
    recoverHandleFunc:      logStackOnRecover,
    serviceErrorHandleFunc: writeServiceError,
    // The default route selector uses the CurlyRouter
    router:                 CurlyRouter{},
    contentEncodingEnabled: false}}// Add WebService to default Container
func Add(service *WebService) {
  DefaultContainer.Add(service)
}

// Add
func (c *Container) Add(service *WebService) *Container{...// if rootPath was not set then lazy initialize it
  if len(service.rootPath) == 0 {
    service.Path("/")}// Check whether there is a duplicate RootPath. Different WebServices cannot have the same RootPath
  for _, each := range c.webServices {
    if each.RootPath() == service.RootPath() {
      log.Printf("WebService with duplicate root path detected:['%v']", each)
      os.Exit(1)}}if! c.isRegisteredOnRoot {// Core logic: Add handler handler functions for servcie
    // Here we pass in c.servemux as the argument, which is the previously mentioned http.defaultServemux
    c.isRegisteredOnRoot = c.addHandler(service, c.ServeMux)
  }
  // Add webServices to the WebService list of the Container
  c.webServices = append(c.webServices, service)
  return c
}

// addHandler
func (c *Container) addHandler(service *WebService, serveMux *http.ServeMux) bool {
  pattern := fixedPrefixPath(service.RootPath())
  ...
  // The key function here: servemux.handlefunc, is a function that implements routing in the Golang standard package
  // The route processing function is assigned to the c.dispatch function. It can be seen that this function is the core of the entire Go-restful framework
  if! alreadyMapped { serveMux.HandleFunc(pattern, c.dispatch)if! strings.HasSuffix(pattern,"/") {
      serveMux.HandleFunc(pattern+"/", c.dispatch)
    }
  }
  return false
}
Copy the code

Route dispatch function

How to implement hierarchical distribution by Container -> WebService -> Handler? The gO restful framework uses servemux.handlefunc (pattern, C. Dispatch) functions to connect the official HTTP extension mechanism provided by Golang on one hand, and to distribute routes through a Dispatch on the other. So you don’t have to write a lot of handlers on your own.

The core of this function is c.outer.SelectRoute, which finds the appropriate WebService and route based on the request

Source: github.com/emicklei/go-restful/container.go

func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request){...// Find the most appropriate webService and route according to the request
  // This method is described separately later
  func(a){... webService, route, err = c.router.SelectRoute( c.webServices, httpRequest) }() ...iferr ! =nil {
    // Construct a filter
    chain := FilterChain{Filters: c.containerFilters, Target: func(req *Request, resp *Response) {
      switch err.(type) {
      case ServiceError:
        ser := err.(ServiceError)
        c.serviceErrorHandleFunc(ser, req, resp)
      }
      // TODO
    }}
    // Run the Container filter
    chain.ProcessFilter(NewRequest(httpRequest), NewResponse(writer))
    return
  }

  // Try to convert the Router object to the PathProcessor object
  // We are using the default Container. The default Container is CurlyRouter.
  // One of SelectRoute's implementation classes, RouterJSR311, also implements the PathProcessor. So if you're using RouterJSR311, the interface conversion is the only way to get the value
  // The default CurlyRouter does not implement the PathProcessor interface, so the conversion is null and goes to the next if statement
  pathProcessor, routerProcessesPath := c.router.(PathProcessor)
  if! routerProcessesPath {// Use the default path handler
    pathProcessor = defaultPathProcessor{}
  }
  // Extract parameters from the REQUEST URL request
  pathParams := pathProcessor.ExtractParameters(route, webService, httpRequest.URL.Path)
  wrappedRequest, wrappedResponse := route.wrapRequestResponse(writer, httpRequest, pathParams)
  // Add all filters to the filter chain if there are any
  if size := len(c.containerFilters) + len(webService.filters) + len(route.Filters); size > 0 {
    // compose filter chain
    allFilters := make([]FilterFunction, 0, size)
    allFilters = append(allFilters, c.containerFilters...)
    allFilters = append(allFilters, webService.filters...)
    allFilters = append(allFilters, route.Filters...)
    chain := FilterChain{Filters: allFilters, Target: route.Function}
    chain.ProcessFilter(wrappedRequest, wrappedResponse)
  } else {
    // no filters, handle request by route
    // The request is processed directly through route without filter
    route.Function(wrappedRequest, wrappedResponse)
  }
}
Copy the code

routing

The function of c.outer.SelectRoute, mentioned in the previous Dispatch, is to select the appropriate WebService and route, which is described here.

The Router property in the Container is a RouteSelector interface

type RouteSelector interface {
  SelectRoute finds a route based on the entered HTTP request and the list of WebServices and returns it
  SelectRoute(
    webServices []*WebService,
    httpRequest *http.Request) (selectedService *WebService, selected *Route, err error)
}
Copy the code

The Go-restful framework has two implementation classes:

  • CurlyRouter
  • RouterJSR311

The previous analysis code knows that CurlyRouter is the default implementation, so here we will focus on the SelectRoute function of CurlyRouter

// Select the routing function
func (c CurlyRouter) SelectRoute( webServices []*WebService, httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {
  // Parse the URL into a token list based on '/'
  requestTokens := tokenizePath(httpRequest.URL.Path)
  // Match the tokens list with the Routing table of the WebService to return the most appropriate WebService
  detectedService := c.detectWebService(requestTokens, webServices)
  ...
  // Return the matching set of routes in the WebService
  candidateRoutes := c.selectRoutes(detectedService, requestTokens)
  ...
  // Find the best route from the list above
  selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
  if selectedRoute == nil {
    return detectedService, nil, err
  }
  return detectedService, selectedRoute, nil
}

/ / select webservice
func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
  var best *WebService
  score := - 1
  for _, each := range webServices {
    // Calculates the webService score
    matches, eachScore := c.computeWebserviceScore(requestTokens, each.pathExpr.tokens)
    // Returns the webService with the highest score
    if matches && (eachScore > score) {
      best = each
      score = eachScore
    }
  }
  // Return the webService with the highest score
  return best
}

// Calculate the WebService score
func (c CurlyRouter) computeWebserviceScore(requestTokens []string, tokens []string) (bool.int) {
  if len(tokens) > len(requestTokens) {
    return false.0
  }
  score := 0
  for i := 0; i < len(tokens); i++ {
    each := requestTokens[i]
    other := tokens[i]
    if len(each) == 0 && len(other) == 0 {
      score++
      continue
    }
    if len(other) > 0 && strings.HasPrefix(other, "{") {
      // no empty match
      if len(each) == 0 {
        return false, score
      }
      score += 1
    } else {
      // not a parameter
      ifeach ! = other {return false, score
      }
      score += (len(tokens) - i) * 10 //fuzzy}}return true, score
}

// Primary: matches path and returns a batch of routes as alternatives
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
  // Store the selected Route in sortableCurlyRoutes
  candidates := make(sortableCurlyRoutes, 0.8)
  // Run the webService through all the routes
  for _, each := range ws.routes {
    // paramCount: matches the regex
    // staticCount: A complete match
    matches, paramCount, staticCount := c.matchesRouteByPathTokens(each.pathParts, requestTokens, each.hasCustomVerb)
    // If it matches, add it to the alternate list
    if matches {
      candidates.add(curlyRoute{each, paramCount, staticCount}) // TODO make sure Routes() return pointers?}}// Sort alternate routes
  sort.Sort(candidates)
  return candidates
}

// Secondary filter: matches attributes and other information. Returns the most appropriate Route
func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
  // tracing is done inside detectRoute
  return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
}

// If multiple attributes match: method, content-type, accept
func (r RouterJSR311) detectRoute(routes []Route, httpRequest *http.Request) (*Route, error) {
  candidates := make([]*Route, 0.8)
  for i, each := range routes {
    ok := true
    for _, fn := range each.If {
      if! fn(httpRequest) { ok =false
        break}}if ok {
      candidates = append(candidates, &routes[i])
    }
  }
  if len(candidates) == 0 {
    if trace {
      traceLogger.Printf("no Route found (from %d) that passes conditional checks".len(routes))
    }
    return nil, NewError(http.StatusNotFound, "404: Not Found")}// Check whether the HTTP method matches
  previous := candidates
  candidates = candidates[:0]
  for _, each := range previous {
    if httpRequest.Method == each.Method {
      candidates = append(candidates, each)
    }
  }
  if len(candidates) == 0 {
    if trace {
      traceLogger.Printf("no Route found (in %d routes) that matches HTTP method %s\n".len(previous), httpRequest.Method)
    }
    allowed := []string{}
  allowedLoop:
    for _, candidate := range previous {
      for _, method := range allowed {
        if method == candidate.Method {
          continue allowedLoop
        }
      }
      allowed = append(allowed, candidate.Method)
    }
    header := http.Header{"Allow": []string{strings.Join(allowed, ",")}}
    return nil, NewErrorWithHeader(http.StatusMethodNotAllowed, "405: Method Not Allowed", header)
  }

  // Check whether the content-type matches
  contentType := httpRequest.Header.Get(HEADER_ContentType)
  previous = candidates
  candidates = candidates[:0]
  for _, each := range previous {
    if each.matchesContentType(contentType) {
      candidates = append(candidates, each)
    }
  }
  if len(candidates) == 0 {
    if trace {
      traceLogger.Printf("no Route found (from %d) that matches HTTP Content-Type: %s\n".len(previous), contentType)
    }
    if httpRequest.ContentLength > 0 {
      return nil, NewError(http.StatusUnsupportedMediaType, "415: Unsupported Media Type")}}// Determine whether accept matches
  previous = candidates
  candidates = candidates[:0]
  accept := httpRequest.Header.Get(HEADER_Accept)
  if len(accept) == 0 {
    accept = "* / *"
  }
  for _, each := range previous {
    if each.matchesAccept(accept) {
      candidates = append(candidates, each)
    }
  }
  if len(candidates) == 0 {
    if trace {
      traceLogger.Printf("no Route found (from %d) that matches HTTP Accept: %s\n".len(previous), accept)
    }
    available := []string{}
    for _, candidate := range previous {
      available = append(available, candidate.Produces...)
    }
    return nil, NewError(
      http.StatusNotAcceptable,
      fmt.Sprintf("406: Not Acceptable\n\nAvailable representations: %s", strings.Join(available, ",")))}// If there are multiple matches, return the first one
  return candidates[0].nil
}
Copy the code

Start the service

DefaultServeMux, the golang standard library HTTP route object for gO restful direct operation, so this step only needs to call the HTTP standard service start, no additional processing is required. Namely HTTP. ListenAndServe (” : “8080, nil)

conclusion

Go-restful is not a hot Golang Web framework, but K8S used it, this article through the source code analysis of the internal implementation of go-restful do a simple analysis. From the point of view of the analysis process, it is indeed a compact framework. Internal deeper function we did not continue to study, as long as to achieve the purpose of understanding k8S Kube-Apiserver component source code on the line.

The internal core implementation is as long as:

  • Add the handler dispatch via DefaultServeMux, the default routing object of the HTTP package
  • All route distribution functions are transferred to Dispatch
  • Dispatch internally calls the SelectRoute method of CurlyRouter, RouteSelector’s default implementation class, to select the appropriate Route
  • The handler method registered with Route is called to process the request