diff --git a/engine/builder.go b/engine/builder.go index 71a4297..deb09e5 100644 --- a/engine/builder.go +++ b/engine/builder.go @@ -4,8 +4,8 @@ import ( "net/http" "sync" - "github.com/julienschmidt/httprouter" "github.com/vizee/gapi/internal/slices" + "github.com/vizee/pathrouter" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) @@ -41,7 +41,7 @@ func NewBuilder() *Builder { }, }, } - b.engine.router.Store(httprouter.New()) + b.engine.routers.Store(&namedList[pathrouter.Router[*grpcRoute]]{}) return b } diff --git a/engine/context.go b/engine/context.go index 632e8ba..02a0a6c 100644 --- a/engine/context.go +++ b/engine/context.go @@ -4,20 +4,11 @@ import ( "net/http" "net/url" - "github.com/julienschmidt/httprouter" "github.com/vizee/gapi/internal/ioutil" + "github.com/vizee/pathrouter" ) -type Params httprouter.Params - -func (ps Params) Get(name string) (string, bool) { - for i := range ps { - if ps[i].Key == name { - return ps[i].Value, true - } - } - return "", false -} +type Params = pathrouter.Params type Context struct { req *http.Request diff --git a/engine/context_test.go b/engine/context_test.go index 23e4227..0afa1f8 100644 --- a/engine/context_test.go +++ b/engine/context_test.go @@ -5,8 +5,6 @@ import ( "net/http" "reflect" "testing" - - "github.com/julienschmidt/httprouter" ) func TestContext_Get(t *testing.T) { @@ -44,7 +42,7 @@ func TestContext_reset(t *testing.T) { ctx := &Context{ req: &http.Request{}, resp: nil, - params: []httprouter.Param{}, + params: Params{}, query: map[string][]string{}, values: map[string]string{}, body: []byte{}, diff --git a/engine/engine.go b/engine/engine.go index 50f196d..ce0f9a2 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -7,9 +7,9 @@ import ( "sync" "sync/atomic" - "github.com/julienschmidt/httprouter" "github.com/vizee/gapi/log" "github.com/vizee/gapi/metadata" + "github.com/vizee/pathrouter" "google.golang.org/grpc" ) @@ -32,7 +32,7 @@ type Engine struct { notFound HandleFunc ctxpool *sync.Pool - router atomic.Pointer[httprouter.Router] + routers atomic.Pointer[namedList[pathrouter.Router[*grpcRoute]]] clients map[string]*grpc.ClientConn routeLock sync.Mutex } @@ -63,7 +63,7 @@ func (e *Engine) ClearRouter() { e.routeLock.Lock() clients := e.clients e.clients = nil - e.router.Store(nil) + e.routers.Store(nil) e.routeLock.Unlock() for _, cc := range clients { cc.Close() @@ -77,7 +77,9 @@ type routesSliceIter struct { func (it *routesSliceIter) NextRoute() *metadata.Route { if it.i < len(it.rs) { - return it.rs[it.i] + r := it.rs[it.i] + it.i++ + return r } return nil } @@ -86,16 +88,6 @@ func (e *Engine) RebuildRouter(routes []*metadata.Route, ignoreError bool) error return RebuildEngineRouter(e, &routesSliceIter{rs: routes}, ignoreError) } -func registerRoute(router *httprouter.Router, method string, path string, handle httprouter.Handle) (err error) { - defer func() { - if e := recover(); e != nil { - err = fmt.Errorf("router.Handle: %v", e) - } - }() - router.Handle(method, path, handle) - return -} - func (e *Engine) Execute(w http.ResponseWriter, req *http.Request, params Params, chain []HandleFunc, handle HandleFunc) { ctx := e.ctxpool.Get().(*Context) ctx.req = req @@ -118,21 +110,32 @@ func (e *Engine) NotFound(w http.ResponseWriter, req *http.Request) { e.Execute(w, req, nil, e.uses, e.notFound) } +var resultsPool sync.Pool + +func getMatchResult() *pathrouter.MatchResult[*grpcRoute] { + res := resultsPool.Get() + if res != nil { + return res.(*pathrouter.MatchResult[*grpcRoute]) + } + return &pathrouter.MatchResult[*grpcRoute]{} +} + func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { log.Debugf("Route %s %s", req.Method, req.URL.Path) - router := e.router.Load() - if router != nil { - path := req.URL.Path - handle, ps, tsr := router.Lookup(req.Method, path) - if handle != nil { - handle(w, req, ps) - return - } else if tsr && path != "/" { - log.Debugf("Trailing slash redirect %s", req.URL.Path) - req.URL.Path = path + "/" - http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently) - return + routers := e.routers.Load() + if routers != nil { + rotuer, ok := routers.lookup(req.Method) + if ok { + res := getMatchResult() + matched := rotuer.Match(req.URL.Path, res) + if matched { + e.Execute(w, req, res.Params, res.Value.mws, res.Value.handle) + } + resultsPool.Put(res) + if matched { + return + } } } @@ -159,7 +162,7 @@ func RebuildEngineRouter[R RouteIter](e *Engine, routeIter R, ignoreError bool) // 在同一次 router 构建中尽可能复用重复的 chain,在大量路由的情况下会带来一些内存节约 chainCache := make(map[string][]HandleFunc) - router := httprouter.New() + routers := &namedList[pathrouter.Router[*grpcRoute]]{} for { route := routeIter.NextRoute() if route == nil { @@ -190,7 +193,7 @@ func RebuildEngineRouter[R RouteIter](e *Engine, routeIter R, ignoreError bool) clients[route.Call.Server] = client } - middlewares, err := e.generateMiddlewareChain(chainCache, route.Use) + mws, err := e.generateMiddlewareChain(chainCache, route.Use) if err != nil { if ignoreError { continue @@ -198,14 +201,12 @@ func RebuildEngineRouter[R RouteIter](e *Engine, routeIter R, ignoreError bool) return fmt.Errorf("middleware of %s: %v", route.Path, err) } - gr := &grpcRoute{ - engine: e, - middlewares: middlewares, - call: route.Call, - ch: ch, - client: client, - } - err = registerRoute(router, route.Method, route.Path, gr.handleRoute) + err = routers.get(route.Method).Add(route.Path, &grpcRoute{ + mws: mws, + call: route.Call, + ch: ch, + client: client, + }) if err != nil { if ignoreError { log.Warnf("registerRoute(%s %s): %v", route.Method, route.Path, err) @@ -215,7 +216,7 @@ func RebuildEngineRouter[R RouteIter](e *Engine, routeIter R, ignoreError bool) } } - e.router.Store(router) + e.routers.Store(routers) e.clients = clients for server, cc := range old { if clients[server] == nil { diff --git a/engine/engine_test.go b/engine/engine_test.go index 2299ffe..d598ba8 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1,14 +1,36 @@ package engine import ( + "fmt" "net/http" "strings" "testing" + "github.com/vizee/gapi/log" "github.com/vizee/gapi/metadata" ) +type logger struct { +} + +// Debugf implements log.Logger. +func (*logger) Debugf(format string, args ...any) { + fmt.Printf(format+"\n", args...) +} + +// Errorf implements log.Logger. +func (*logger) Errorf(format string, args ...any) { + fmt.Printf(format+"\n", args...) +} + +// Warnf implements log.Logger. +func (*logger) Warnf(format string, args ...any) { + fmt.Printf(format+"\n", args...) +} + func TestEngine_RebuildRouter(t *testing.T) { + log.SetLogger(&logger{}) + builder := NewBuilder() builder.RegisterHandler("mock-handler", &mockHandler{}) builder.RegisterMiddleware("auth", func(ctx *Context) error { @@ -29,9 +51,14 @@ func TestEngine_RebuildRouter(t *testing.T) { return nil }) e := builder.Build() - e.RebuildRouter([]*metadata.Route{ + + err := e.RebuildRouter([]*metadata.Route{ {Method: "POST", Path: "/add", Use: []string{"auth"}, Call: mockAddCall()}, }, true) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest("POST", "http://localhost/add?uid=1", strings.NewReader(`{"a":1,"b":2}`)) if err != nil { t.Fatal(err) diff --git a/engine/named_list.go b/engine/named_list.go new file mode 100644 index 0000000..dc823e0 --- /dev/null +++ b/engine/named_list.go @@ -0,0 +1,30 @@ +package engine + +type namedItem[T any] struct { + name string + v T +} + +type namedList[T any] struct { + items []namedItem[T] +} + +func (l *namedList[T]) lookup(name string) (*T, bool) { + for i := range l.items { + if l.items[i].name == name { + return &l.items[i].v, true + } + } + return nil, false +} + +func (l *namedList[T]) get(name string) *T { + v, ok := l.lookup(name) + if ok { + return v + } + l.items = append(l.items, namedItem[T]{ + name: name, + }) + return &l.items[len(l.items)-1].v +} diff --git a/engine/route.go b/engine/route.go index 1231999..f974522 100644 --- a/engine/route.go +++ b/engine/route.go @@ -2,9 +2,7 @@ package engine import ( "context" - "net/http" - "github.com/julienschmidt/httprouter" "github.com/vizee/gapi/metadata" "google.golang.org/grpc" ) @@ -27,12 +25,13 @@ func (*passthroughCodec) Name() string { return "passthrough" } +var passthroughCodecOpt = grpc.ForceCodec(&passthroughCodec{}) + type grpcRoute struct { - engine *Engine - middlewares []HandleFunc - call *metadata.Call - ch CallHandler - client *grpc.ClientConn + mws []HandleFunc + call *metadata.Call + ch CallHandler + client *grpc.ClientConn } func (r *grpcRoute) handle(ctx *Context) error { @@ -49,7 +48,7 @@ func (r *grpcRoute) handle(ctx *Context) error { callctx, cancel = context.WithTimeout(callctx, call.Timeout) } var respData []byte - err = r.client.Invoke(callctx, call.Method, reqData, &respData, grpc.ForceCodec(&passthroughCodec{})) + err = r.client.Invoke(callctx, call.Method, reqData, &respData, passthroughCodecOpt) if cancel != nil { cancel() } @@ -59,8 +58,3 @@ func (r *grpcRoute) handle(ctx *Context) error { return r.ch.WriteResponse(call, ctx, respData) } - -func (r *grpcRoute) handleRoute(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - // 封装闭包可能带来一点点内存开销 - r.engine.Execute(w, req, Params(params), r.middlewares, r.handle) -} diff --git a/engine/route_test.go b/engine/route_test.go deleted file mode 100644 index 8973429..0000000 --- a/engine/route_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package engine - -import ( - "net/http" - "strings" - "testing" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -func Test_grpcRoute_handleRoute(t *testing.T) { - cc, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - t.Fatal(err) - } - gr := &grpcRoute{ - engine: NewBuilder().Build(), - middlewares: []HandleFunc{}, - call: mockAddCall(), - ch: &mockHandler{}, - client: cc, - } - req, err := http.NewRequest("POST", "http://localhost/add", strings.NewReader(`{"a":1,"b":2}`)) - if err != nil { - t.Fatal(err) - } - gr.handleRoute(&mockResponse{}, req, nil) -} diff --git a/go.mod b/go.mod index c2996bb..23686e5 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,21 @@ module github.com/vizee/gapi -go 1.20 +go 1.21.0 + +toolchain go1.22.0 require ( - github.com/julienschmidt/httprouter v1.3.0 github.com/vizee/gapi-proto-go v0.0.0-20230505112701-bca324ad1c1e github.com/vizee/jsonpb v0.2.0 - google.golang.org/grpc v1.55.0 - google.golang.org/protobuf v1.30.0 + github.com/vizee/pathrouter v0.1.0 + google.golang.org/grpc v1.62.0 + google.golang.org/protobuf v1.32.0 ) require ( github.com/golang/protobuf v1.5.3 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect ) diff --git a/go.sum b/go.sum index 88139bc..3152842 100644 --- a/go.sum +++ b/go.sum @@ -2,25 +2,26 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/vizee/gapi-proto-go v0.0.0-20230505112701-bca324ad1c1e h1:3HTmMrUx7Peptebd+xM4p43vhWoNZET2axv1WPLF7Gs= github.com/vizee/gapi-proto-go v0.0.0-20230505112701-bca324ad1c1e/go.mod h1:KljpUV/yxawROIwk/Q6eSS7+pLiSlsJsw8cQhaInOQM= github.com/vizee/jsonpb v0.2.0 h1:k/GFVAvnMW/AEegLR1YC3BVp1UPmz0D8hw7r3zirfkQ= github.com/vizee/jsonpb v0.2.0/go.mod h1:ewTuTSldbqAAE6fEkSWH8vqDKlnB+JpRg7vvYIPuiWM= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +github.com/vizee/pathrouter v0.1.0 h1:uHtagLliKbVJIUALAHKZ0+ioiDYSxIYcCvR6Od3y5CM= +github.com/vizee/pathrouter v0.1.0/go.mod h1:2d5HPZIo31hOMbGr8bMLeQAXZGpc5q2q847prDiOHYA= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=