go-restful是一个golang语言实现的RESTful库,因为Kubernetes APIServer使用它实现RESTful API,这里就简单分析下。看它的文档介绍,功能还是挺强大的,主要体现在它的路由策略上,支持静态,谷歌式的自定义方法,正则表达式,以及URL内参数等,此外还支持json/xml等格式化,还有filter过滤器等,虽然有这么多功能,但是Kubernetes使用的比较简单,只用到了它最基础的功能,即路由功能,这里就重点分析下这个功能。

基础概念

首先,还是借go-restful提供的一个小例子,来看下它的核心功能,示例代码见这里

package main

import (
	"log"
	"net/http"

	"github.com/emicklei/go-restful"
)

// This example has the same service definition as restful-user-resource
// but uses a different router (CurlyRouter) that does not use regular expressions
//
// POST http://localhost:8080/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
// GET http://localhost:8080/users/1
//
// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
// DELETE http://localhost:8080/users/1
//

type User struct {
	Id, Name string
}

type UserResource struct {
	// normally one would use DAO (data access object)
	users map[string]User
}

func (u UserResource) Register(container *restful.Container) {
	ws := new(restful.WebService)
	ws.
		Path("/users").
		Consumes(restful.MIME_XML, restful.MIME_JSON).
		Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well

	ws.Route(ws.GET("/{user-id}").To(u.findUser))
	ws.Route(ws.POST("").To(u.updateUser))
	ws.Route(ws.PUT("/{user-id}").To(u.createUser))
	ws.Route(ws.DELETE("/{user-id}").To(u.removeUser))

	container.Add(ws)
}

// GET http://localhost:8080/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	usr := u.users[id]
	if len(usr.Id) == 0 {
		response.AddHeader("Content-Type", "text/plain")
		response.WriteErrorString(http.StatusNotFound, "User could not be found.")
	} else {
		response.WriteEntity(usr)
	}
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
	usr := new(User)
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.Id] = *usr
		response.WriteEntity(usr)
	} else {
		response.AddHeader("Content-Type", "text/plain")
		response.WriteErrorString(http.StatusInternalServerError, err.Error())
	}
}

// POST http://localhost:8080/users
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
	usr := User{Id: request.PathParameter("user-id")}
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.Id] = usr
		response.WriteHeaderAndEntity(http.StatusCreated, usr)
	} else {
		response.AddHeader("Content-Type", "text/plain")
		response.WriteErrorString(http.StatusInternalServerError, err.Error())
	}
}

// DELETE http://localhost:8080/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	delete(u.users, id)
}

func main() {
	wsContainer := restful.NewContainer()
	wsContainer.Router(restful.CurlyRouter{})
	u := UserResource{map[string]User{}}
	u.Register(wsContainer)

	log.Print("start listening on localhost:8080")
	server := &http.Server{Addr: ":8080", Handler: wsContainer}
	log.Fatal(server.ListenAndServe())
}

这个示例代码,就是使用go-restful的核心功能实现了一个简单的RESTful的API,实现了对User的增删查改,其中有这么几个核心概念:Container, WebService, Route。

  • Container可以包含多个WebService,而WebService又可以包含多个Route。
  • 一个WebService其实代表某一个对象相关的服务,如上例中的/users,针对该/users要实现RESTful API,那么需要向其添加增删查改的路由,即Route,它是Route的集合。
  • 每一个Route,根据Method和Path,映射到对应的方法中,即是Method/Path到Function映射关系的抽象,如上例中的ws.Route(ws.GET("/{user-id}").To(u.findUser)),就是针对/users/{user-id}该路径的GET请求,则被路由到findUser方法中进行处理。
  • 而Container则是WebService的集合,可以向Container中添加多个WebService,而Container因为实现了ServeHTTP()方法,其本质上还是一个http Handler,可以直接用在http Server中。

内部构造

简单来看看Container的内部构造:

type Container struct {
    webServicesLock        sync.RWMutex
    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
}

其包含一个WebService数组,一个http.ServeMux,一个RouteSelector。

WebService

WebService数组的作用好理解,因为它是WebService的集合,所以一定要有个数组来存Add进来的WebService:

func (c *Container) Add(service *WebService) *Container {
	c.webServicesLock.Lock()
	defer c.webServicesLock.Unlock()

	// if rootPath was not set then lazy initialize it
	if len(service.rootPath) == 0 {
		service.Path("/")
	}

	// cannot have duplicate root paths
	for _, each := range c.webServices {
		if each.RootPath() == service.RootPath() {
			log.Printf("WebService with duplicate root path detected:['%v']", each)
			os.Exit(1)
		}
	}

	// If not registered on root then add specific mapping
	if !c.isRegisteredOnRoot {
		c.isRegisteredOnRoot = c.addHandler(service, c.ServeMux)
	}
	c.webServices = append(c.webServices, service)
	return c
}

ServeMux

而http.ServeMux,则是net/http中的内容,见这里,ServeMux,可以直译成”多路器“,即可以向ServeMux中注册多个路径,以及对应的Handler方法,当请求过来时,根据最大匹配原则,请求路径跟注册路径最匹配的,则其对应的Handler来处理该请求。在go-restful中,它的作用主要是为了将Containder实现为Handler,即实现了ServeHTTP()方法:

func (c *Container) ServeHTTP(httpWriter http.ResponseWriter, httpRequest *http.Request) {
	c.ServeMux.ServeHTTP(httpWriter, httpRequest)
}

向ServeMux中注册方法,可以通过两种方式:

一种是通过addHandler()方法,通过向ServeMux中注册进统一的Handler,即c.dispatch()

func (c *Container) addHandler(service *WebService, serveMux *http.ServeMux) bool {
	pattern := fixedPrefixPath(service.RootPath())
	// check if root path registration is needed
	if "/" == pattern || "" == pattern {
		serveMux.HandleFunc("/", c.dispatch)
		return true
	}
	// detect if registration already exists
	alreadyMapped := false
	for _, each := range c.webServices {
		if each.RootPath() == service.RootPath() {
			alreadyMapped = true
			break
		}
	}
	if !alreadyMapped {
		serveMux.HandleFunc(pattern, c.dispatch)
		if !strings.HasSuffix(pattern, "/") {
			serveMux.HandleFunc(pattern+"/", c.dispatch)
		}
	}
	return false
}

可见这种方式,不管路径是什么,其都会由c.dispatch()作为Handler来处理请求,在c.dispatch()中,则会调用go-restful自己的路由逻辑,去WebService中寻找最匹配的Route来处理请求:

func (c *Container) dispatch(httpWriter http.ResponseWriter, httpRequest *http.Request) {
	writer := httpWriter

	// Find best match Route ; err is non nil if no match was found
	var webService *WebService
	var route *Route
	var err error
	func() {
		c.webServicesLock.RLock()
		defer c.webServicesLock.RUnlock()
		webService, route, err = c.router.SelectRoute(
			c.webServices,
			httpRequest)
	}()

    ......

	// pass through filters (if 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
		route.Function(wrappedRequest, wrappedResponse)
	}
}

而另一种是通过Container的Handle()方法,直接向ServeMux中进行注册Handler:

func (c *Container) Handle(pattern string, handler http.Handler) {
	c.ServeMux.Handle(pattern, http.HandlerFunc(func(httpWriter http.ResponseWriter, httpRequest *http.Request) {
		// Skip, if httpWriter is already an CompressingResponseWriter
		if _, ok := httpWriter.(*CompressingResponseWriter); ok {
			handler.ServeHTTP(httpWriter, httpRequest)
			return
		}

		writer := httpWriter

		// CompressingResponseWriter should be closed after all operations are done
		defer func() {
			if compressWriter, ok := writer.(*CompressingResponseWriter); ok {
				compressWriter.Close()
			}
		}()

		if c.contentEncodingEnabled {
			doCompress, encoding := wantsCompressedResponse(httpRequest)
			if doCompress {
				var err error
				writer, err = NewCompressingResponseWriter(httpWriter, encoding)
				if err != nil {
					log.Print("unable to install compressor: ", err)
					httpWriter.WriteHeader(http.StatusInternalServerError)
					return
				}
			}
		}

		handler.ServeHTTP(writer, httpRequest)
	}))
}

RouteSelector

路由选择器,即从注册的WebService中,选择出最合适的Route来处理客户端请求,其接口定位为:

type RouteSelector interface {

	// SelectRoute finds a Route given the input HTTP Request and a list of WebServices.
	// It returns a selected Route and its containing WebService or an error indicating
	// a problem.
	SelectRoute(
		webServices []*WebService,
		httpRequest *http.Request) (selectedService *WebService, selected *Route, err error)
}

go-restful实现了一个叫做CurlyRouter的路由器,可以支持类似"/users/{user-id}"这样的URL路径。

从上面的分析可以看出,路由的入口是由ServeMux提供的,但是可以完全不使用ServeMux提供的路由功能,而是将路由功能交给RouteSelector来实现,比如先向Container添加一个根路径为"/"的WebService,则完全禁用掉了ServeMux的路由功能。当然两者也可以配合使用,一部分路径由ServeMux提供,一部分路径由RouteSelector提供。

Kubernetes中使用方法

Kubernetes中对go-restful的使用,比较基础,就使用到了其最基础的路由功能,我们来看一个简化版的Kubernetes使用go-restful构造的RESTful API的示例,下面的示例实现了如下几个API:

GET   /apis/apps/v1/namespaces/{namespace}/deployments/{name}
POST  /apis/apps/v1/namespaces/{namespace}/deployments

GET   /apis/apps/v1/namespaces/{namespace}/daemonsets/{name}
POST  /apis/apps/v1/namespaces/{namespace}/daemonsets

GET   /apis/batch/v1beta1/namespaces/{namespace}/jobs/{name}
POST  /apis/batch/v1beta1/namespaces/{namespace}/jobs

代码如下:

package main

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

type MyHandler struct {
	GoRestfulContainer *restful.Container
	name               string
}

func NewMyHandler(name string) *MyHandler {
	gorestfulContainer := restful.NewContainer()
	gorestfulContainer.ServeMux = http.NewServeMux()
	gorestfulContainer.Router(restful.CurlyRouter{})

	return &MyHandler{
		GoRestfulContainer: gorestfulContainer,
		name:               name,
	}
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	h.GoRestfulContainer.Dispatch(w, req)
	return
}

func NewWebService(group string, version string) *restful.WebService {
	ws := new(restful.WebService)
	ws.Path("/apis/" + group + "/" + version)
	ws.Doc("API at /apis/apps/v1")
	ws.Consumes(restful.MIME_XML, restful.MIME_JSON)
	ws.Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well
	return ws
}

func registerHandler(resource string, ws *restful.WebService) {
	routes := []*restful.RouteBuilder{}

	nameParam := ws.PathParameter("name", "name of the resource").DataType("string")
	namespaceParam := ws.PathParameter("namespace", "object name and auth scope, such as for teams and projects").DataType("string")

	route := ws.GET("namespaces" + "/{namespace}/" + resource + "/{name}").To(getHandler)
	route.Param(namespaceParam)
	route.Param(nameParam)
	route.Writes(Foo{})
	routes = append(routes, route)

	route2 := ws.POST("namespaces" + "/{namespace}/" + resource).To(postHandler)
	route2.Param(namespaceParam)
	routes = append(routes, route2)

	for _, route := range routes {
		ws.Route(route)
	}
}

type Foo struct {
	namespace string
	name      string
}

func getHandler(req *restful.Request, res *restful.Response) {
	namespace := req.PathParameter("namespace")
	name := req.PathParameter("name")
	log.Println("GET: " + namespace + "/" + name)
	res.WriteEntity(Foo{namespace: namespace, name: name})
}

func postHandler(req *restful.Request, res *restful.Response) {
	namespace := req.PathParameter("namespace")
	log.Println("POST: " + namespace)
	res.WriteEntity(Foo{namespace: namespace, name: ""})
}

func main() {
    handler := NewMyHandler("foo")

	ws1 := NewWebService("apps", "v1")
	registerHandler("deployments", ws1)
	registerHandler("daemonsets", ws1)

	ws2 := NewWebService("batch", "v1beta1")
	registerHandler("jobs", ws2)

	handler.GoRestfulContainer.Add(ws1)
	handler.GoRestfulContainer.Add(ws2)

	s := &http.Server{
		Addr:           ":8080",
		Handler:        handler,
		ReadTimeout:    10 * time.Second,
		WriteTimeout:   10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	log.Fatal(s.ListenAndServe())
}

可见,针对每一个Group和Version,Kubernetes使用了一个WebService来组织其资源,全部添加到一个Container中,而在MyHandler的ServeHTTP()方法中,则直接调用了Container的Dispatch()方法:h.GoRestfulContainer.Dispatch(w, req),即直接使用了go-restful内置的路由功能,并没有使用到它的ServeMux的路由功能。此外,还使用到了路径参数的功能。

总结

本篇简单介绍了下go-restful这个golang语言实现的构造restful api的库,以及其内部的逻辑,然后通过一个简化版的Kubernetes RESTful API的示例,来说明下Kubernetes中是如何使用go-restful的,不过貌似Kubernetes在使用go-restful时,跟其他功能一起使用时,遇到了一些兼容性问题,导致想将其替换掉,具体还见代码注释:

// Director is here so that we can properly handle fall through and proxy cases.
// This looks a bit bonkers, but here's what's happening.  We need to have /apis handling registered in gorestful in order to have
// swagger generated for compatibility.  Doing that with `/apis` as a webservice, means that it forcibly 404s (no defaulting allowed)
// all requests which are not /apis or /apis/.  We need those calls to fall through behind goresful for proper delegation.  Trying to
// register for a pattern which includes everything behind it doesn't work because gorestful negotiates for verbs and content encoding
// and all those things go crazy when gorestful really just needs to pass through.  In addition, openapi enforces unique verb constraints
// which we don't fit into and it still muddies up swagger.  Trying to switch the webservices into a route doesn't work because the
//  containing webservice faces all the same problems listed above.
// This leads to the crazy thing done here.  Our mux does what we need, so we'll place it in front of gorestful.  It will introspect to
// decide if the route is likely to be handled by goresful and route there if needed.  Otherwise, it goes to PostGoRestful mux in
// order to handle "normal" paths and delegation. Hopefully no API consumers will ever have to deal with this level of detail.  I think
// we should consider completely removing gorestful.
// Other servers should only use this opaquely to delegate to an API server.
Director http.Handler