diff --git a/grpcmux/server.go b/grpcmux/server.go index 52e0b59..05ee096 100644 --- a/grpcmux/server.go +++ b/grpcmux/server.go @@ -4,16 +4,14 @@ import ( "context" "flag" "fmt" + "github.com/skema-dev/skema-go/config" + "github.com/skema-dev/skema-go/logging" "io/ioutil" "log" "net" "net/http" "os" "strings" - "text/template" - - "github.com/skema-dev/skema-go/config" - "github.com/skema-dev/skema-go/logging" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" @@ -60,6 +58,10 @@ func NewServerWithConfig(conf *config.Config, opts ...grpc.ServerOption) *grpcSe } logging.Infow("service port", "gprc", port, "http", httpPort) + if !validateHttpConfig(conf) { + logging.Fatalf("duplicated url path found. please fix the grpc config file") + } + // connect to grpc port conn, err := grpc.DialContext( context.Background(), @@ -86,7 +88,6 @@ func NewServerWithConfig(conf *config.Config, opts ...grpc.ServerOption) *grpcSe gatewayPathPrefix += "/" } } - logging.Infof("gateway path is set to %s", gatewayPathPrefix) } initComponents(conf) @@ -136,6 +137,28 @@ func LoadLocalConfig() *config.Config { return config.NewConfigWithFile(path) } +func validateHttpConfig(conf *config.Config) bool { + gatewayPath := conf.GetString("http.gateway.path", "") + staticPath := conf.GetString("http.static.path", "") + swaggerPath := conf.GetString("http.swagger.path", "") + + values := []string{gatewayPath, staticPath, swaggerPath} + for i := range values { + if values[i] == "" { + continue + } + j := i + 1 + for j < len(values) { + if values[i] == values[j] { + logging.Errorf("duplicated url prefix path: %s, %s\n", values[i], values[j]) + return false + } + j += 1 + } + } + return true +} + // Requeired for grpc service registration func (g *grpcServer) RegisterService(desc *grpc.ServiceDesc, impl interface{}) { g.server.RegisterService(desc, impl) @@ -152,7 +175,7 @@ func (g *grpcServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.URL.Path = strings.TrimPrefix(r.URL.Path, path) r.RequestURI = strings.TrimPrefix(r.RequestURI, path) } - + logging.Infof("server http %s\n", r.URL.Path) g.gatewayMux.ServeHTTP(w, r) } @@ -161,6 +184,32 @@ func (g *grpcServer) Serve() error { reflection.Register(g.server) g.httpMux.Handle(g.gatewayRoutePath, g) + logging.Infof("grpc-gateway path: %s", g.gatewayRoutePath) + + if g.conf.GetString("http.static.path", "") != "" { + staticPath := g.conf.GetString("http.static.path") + if !strings.HasSuffix(staticPath, "/") { + staticPath += "/" + } + staticFilepath := g.conf.GetString("http.static.filepath") + staticHandler := http.FileServer(http.Dir(staticFilepath)) + + g.httpMux.Handle(staticPath, http.StripPrefix(staticPath, staticHandler)) + + logging.Infof("static content(%s) path: %s", staticFilepath, staticPath) + } + + if g.conf.GetString("http.swagger.path", "") != "" { + swaggerPath := g.conf.GetString("http.swagger.path") + swaggerPath = strings.TrimSuffix(swaggerPath, "/") + swaggerFilepath := g.conf.GetString("http.swagger.filepath") + swaggerHandler, openapiHandler := g.getSwaggerHandler(swaggerFilepath) + + g.httpMux.Handle(swaggerPath, swaggerHandler) + g.httpMux.Handle(fmt.Sprintf("%s/openapi", swaggerPath), openapiHandler) + + logging.Infof("swagger path: %s", swaggerPath) + } lis, err := net.Listen("tcp", fmt.Sprintf(":%d", g.port)) if err != nil { @@ -184,11 +233,8 @@ func (g *grpcServer) GetGatewayInfo() (context.Context, *runtime.ServeMux, grpc. return g.ctx, g.gatewayMux, g.clientConn } -func (g *grpcServer) EnableSwagger(serviceName string, openapiDescFilepath string) error { - swaggerUrl := fmt.Sprintf("/%s/swagger/openapi", serviceName) - swaggerServingUrl := fmt.Sprintf("/%s/swagger", serviceName) - - getSwagger := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { +func (g *grpcServer) getSwaggerHandler(openapiDescFilepath string) (http.HandlerFunc, http.HandlerFunc) { + openapiHandler := func(w http.ResponseWriter, r *http.Request) { if content, err := ioutil.ReadFile(openapiDescFilepath); err == nil { fmt.Fprint(w, string(content)) } else { @@ -196,18 +242,9 @@ func (g *grpcServer) EnableSwagger(serviceName string, openapiDescFilepath strin } } - swaggerServing := func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { - t1, err := template.New("index").Parse(swaggerTpl) - if err != nil { - panic(err) - } - t1.Execute(w, swaggerUrl) + swaggerHandler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, swaggerTpl) } - g.gatewayMux.HandlePath("GET", swaggerUrl, getSwagger) - g.gatewayMux.HandlePath("GET", swaggerServingUrl, swaggerServing) - - logging.Infof("swagger enabled at url: %s\n", swaggerServingUrl) - - return nil + return swaggerHandler, openapiHandler } diff --git a/sample/webserver/grpc.yaml b/sample/webserver/grpc.yaml new file mode 100644 index 0000000..76707b2 --- /dev/null +++ b/sample/webserver/grpc.yaml @@ -0,0 +1,25 @@ +port: 9991 # for grpc service +http: + port: 9992 # for http service + gateway: + path: "/" + static: + path: "/web" + filepath: "./static" +# Another setting (you should modify the js code for backend api url) +# gateway: +# path: "/backend" +# static: +# path: "/" +# filepath: "./static" + swagger: + path: "/swagger" + filepath: "./swagger.json" +client: + address: "localhost:9993" # for grpc client url and port + + +logging: + level: debug # info | debug + encoding: console # console | json + output: "./log/default.log" diff --git a/sample/webserver/main.go b/sample/webserver/main.go new file mode 100644 index 0000000..33cbdb5 --- /dev/null +++ b/sample/webserver/main.go @@ -0,0 +1,32 @@ +package main + +import ( + _ "embed" + + "github.com/skema-dev/skema-go/config" + "github.com/skema-dev/skema-go/grpcmux" + "github.com/skema-dev/skema-go/logging" + pb "github.com/skema-dev/skema-go/sample/api/skema/test" +) + +//go:embed grpc.yaml +var yamlConfig []byte + +func main() { + grpcSrv := grpcmux.NewServerWithConfig( + config.NewConfigWithString(string(yamlConfig)), + ) + + pb.RegisterTestServer(grpcSrv, NewServer()) + + // for http gateway only. + ctx, mux, conn := grpcSrv.GetGatewayInfo() + pb.RegisterTestHandlerClient(ctx, mux, pb.NewTestClient(conn)) + + //grpcSrv.EnableStaticContent("/", "./static") + //grpcSrv.EnableStaticContent("/script", "./static/script") + + if err := grpcSrv.Serve(); err != nil { + logging.Fatalf("Serve error %v", err.Error()) + } +} diff --git a/sample/webserver/server.go b/sample/webserver/server.go new file mode 100644 index 0000000..9248d92 --- /dev/null +++ b/sample/webserver/server.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "log" + + pb "github.com/skema-dev/skema-go/sample/api/skema/test" +) + +type rpcTestServer struct { + pb.UnimplementedTestServer +} + +// NewServer: Create new grpc server instance +func NewServer() pb.TestServer { + svr := &rpcTestServer{ + // init custom fileds + } + return svr +} + +// Heathcheck +func (s *rpcTestServer) Heathcheck( + ctx context.Context, + req *pb.HealthcheckRequest, +) (rsp *pb.HealthcheckResponse, err error) { + // implement business logic here ... + // ... + + log.Printf("Received from Heathcheck request: %v", req) + rsp = &pb.HealthcheckResponse{ + Result: "health check ok", + } + + return rsp, err +} + +// Helloworld +func (s *rpcTestServer) Helloworld(ctx context.Context, req *pb.HelloRequest) (rsp *pb.HelloReply, err error) { + // implement business logic here ... + // ... + + log.Printf("Received from Helloworld request: %v", req) + rsp = &pb.HelloReply{ + Msg: "Hello world", + Code: "0", + } + return rsp, err +} diff --git a/sample/webserver/static/index.html b/sample/webserver/static/index.html new file mode 100644 index 0000000..4c5f0c9 --- /dev/null +++ b/sample/webserver/static/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + +
+ + + diff --git a/sample/webserver/static/script/main.js b/sample/webserver/static/script/main.js new file mode 100644 index 0000000..cc257bc --- /dev/null +++ b/sample/webserver/static/script/main.js @@ -0,0 +1,92 @@ +'use strict'; + +class Lesson extends React.Component { + constructor(props) { + console.log(props) + super(props); + this.state = { + lessonId: props.lessonId, + name: props.name, + description: props.description + } + } + + loadLession() { + let lessons = new Map() + lessons.set("lesson1", ) + lessons.set("lesson2", ) + lessons.set("lesson3", ) + root.render(lessons.get(this.state.name)); + } + + render() { + return ( +
+ +
+ { this.state.description } +
+ ); + } +} + +class ApiList extends React.Component { + constructor(props) { + console.log(props) + super(props); + this.state = { + result: "" + } + this.healthCheck = this.healthCheck.bind(this); + this.helloWorld = this.helloWorld.bind(this); + } + + healthCheck() { + axios + .get('/api/healthcheck') + .then(response => { + this.setState({result: "healthcheck: " + response.data.result}); + }) + .catch(function (error) { // 请求失败处理 + console.log(error); + }); + } + + helloWorld() { + axios + .post('/api/helloworld') + .then(response => { + this.setState({result: "helloworld:" + response.data.msg}); + }) + .catch(function (error) { // 请求失败处理 + console.log(error); + }); + } + + render() { + return ( +
+ API List +
+ +
+
+ +
+
+ { this.state.result } +
+ ); + } +} + + +const domContainer1 = document.querySelector('#main_container'); +const root = ReactDOM.createRoot(domContainer1); +root.render(); diff --git a/sample/webserver/swagger.json b/sample/webserver/swagger.json new file mode 100644 index 0000000..a01ba67 --- /dev/null +++ b/sample/webserver/swagger.json @@ -0,0 +1,141 @@ +{ + "swagger": "2.0", + "info": { + "title": "Hello.proto", + "description": "Generated by skemabuild. DO NOT EDIT.", + "version": "version not set" + }, + "tags": [ + { + "name": "Hello" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/healthcheck": { + "get": { + "operationId": "Hello_Heathcheck", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pack1HealthcheckResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Hello" + ] + } + }, + "/api/helloworld": { + "post": { + "operationId": "Hello_Helloworld", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/pack1HelloworldResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/pack1HelloworldRequest" + } + } + ], + "tags": [ + "Hello" + ] + } + } + }, + "definitions": { + "pack1HealthcheckResponse": { + "type": "object", + "example": { + "result": "ok" + }, + "properties": { + "result": { + "type": "string" + } + } + }, + "pack1HelloworldRequest": { + "type": "object", + "example": { + "msg": "hello world" + }, + "properties": { + "msg": { + "type": "string" + } + } + }, + "pack1HelloworldResponse": { + "type": "object", + "example": { + "msg": "hello world from server", + "code": "0" + }, + "properties": { + "msg": { + "type": "string" + }, + "code": { + "type": "string" + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} \ No newline at end of file