go-restful简析

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

基础概念

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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的内部构造:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()方法:

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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来处理请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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来处理客户端请求,其接口定位为:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
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

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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时,跟其他功能一起使用时,遇到了一些兼容性问题,导致想将其替换掉,具体还见代码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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
作者

hackerain

发布于

2020-09-28

更新于

2023-03-11

许可协议