diff --git a/README.md b/README.md index 46510160..801574ce 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,11 @@ 1. 支持 生成数据表 CURD、控制器方法 等代码生成器 1. 支持 [cron](https://github.com/jakecoffman/cron) 定时任务,在后台可界面配置 1. 支持 [websocket](https://github.com/gorilla/websocket) 实时通讯,在后台有界面演示 +1. 支持 [gRPC](https://google.golang.org/grpc) 服务,包含健康检查和示例服务 1. 支持 web 界面,使用的 [Light Year Admin 模板](https://gitee.com/yinqi/Light-Year-Admin-Using-Iframe) +说明: +- 当前仓库主干功能以 HTTP/REST、GraphQL、WebSocket 为主,暂未内置 gRPC 服务端或 `.proto` 定义。 +- 如需在项目中接入 gRPC,请按业务需要自行扩展(例如新增 `cmd/grpc` 服务入口与对应协议定义)。 ## 文档索引(可加入交流群) diff --git a/api/proto/service.proto b/api/proto/service.proto new file mode 100644 index 00000000..fd714ac9 --- /dev/null +++ b/api/proto/service.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package api; + +option go_package = "github.com/xinliangnote/go-gin-api/internal/grpc/pb"; + +// HealthService 健康检查服务 +service HealthService { + // Check 健康检查 + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + } + ServingStatus status = 1; +} + +// HelloService 示例 gRPC 服务 +service HelloService { + // SayHello 简单的 unary RPC + rpc SayHello(HelloRequest) returns (HelloResponse); +} + +message HelloRequest { + string name = 1; +} + +message HelloResponse { + string message = 1; + string trace_id = 2; +} diff --git a/configs/constants.go b/configs/constants.go index ae5d98bc..3f1175eb 100644 --- a/configs/constants.go +++ b/configs/constants.go @@ -18,6 +18,9 @@ const ( // ProjectPort 项目端口 ProjectPort = ":9999" + // ProjectGRPCPort gRPC 服务端口 + ProjectGRPCPort = ":9998" + // ProjectAccessLogFile 项目访问日志存放文件 ProjectAccessLogFile = "./logs/" + ProjectName + "-access.log" diff --git a/en.md b/en.md index f5106626..06e1ac44 100644 --- a/en.md +++ b/en.md @@ -21,6 +21,10 @@ Features: 1. Standard RESTful API return value 1. CURD code generator , controller generator, etc. 1. Web interface, supported by [Light Year Admin template](https://gitee.com/yinqi/Light-Year-Admin-Using-Iframe) +Notes: +- The current repository focuses on HTTP/REST, GraphQL, and WebSocket capabilities. +- Built-in gRPC server support and `.proto` definitions are not included at this time. +- If you need gRPC, add it as an extension module for your own service boundaries. diff --git a/go.mod b/go.mod index 656d3ca9..5a061436 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( go.uber.org/zap v1.19.1 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 golang.org/x/tools v0.1.7 + google.golang.org/grpc v1.46.0 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index 62383322..cb52bae5 100644 --- a/go.sum +++ b/go.sum @@ -94,7 +94,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -119,6 +123,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= @@ -863,6 +868,7 @@ google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71 h1:z+ErRPu0+KS02Td3fOAgdX+lnPDh/VyaABEJPD4JRQs= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -889,6 +895,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/internal/grpc/interceptor/interceptor.go b/internal/grpc/interceptor/interceptor.go new file mode 100644 index 00000000..613a42d7 --- /dev/null +++ b/internal/grpc/interceptor/interceptor.go @@ -0,0 +1,56 @@ +package interceptor + +import ( + "context" + "fmt" + "runtime/debug" + "time" + + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// UnaryLogInterceptor 日志拦截器,记录每次 unary RPC 调用 +func UnaryLogInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ts := time.Now() + + resp, err := handler(ctx, req) + + cost := time.Since(ts) + fields := []zap.Field{ + zap.String("method", info.FullMethod), + zap.Duration("cost", cost), + } + + if err != nil { + fields = append(fields, zap.Error(err)) + logger.Warn("grpc-request", fields...) + } else { + logger.Info("grpc-request", fields...) + } + + return resp, err + } +} + +// UnaryRecoveryInterceptor panic 恢复拦截器 +func UnaryRecoveryInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { + defer func() { + if r := recover(); r != nil { + stackInfo := string(debug.Stack()) + logger.Error("grpc panic recovered", + zap.String("method", info.FullMethod), + zap.String("panic", fmt.Sprintf("%+v", r)), + zap.String("stack", stackInfo), + ) + err = status.Errorf(codes.Internal, "internal server error") + } + }() + + return handler(ctx, req) + } +} diff --git a/internal/grpc/pb/service.pb.go b/internal/grpc/pb/service.pb.go new file mode 100644 index 00000000..d2631b73 --- /dev/null +++ b/internal/grpc/pb/service.pb.go @@ -0,0 +1,37 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: api/proto/service.proto +// +// To regenerate, run: +// protoc --go_out=. --go-grpc_out=. api/proto/service.proto + +package pb + +// HealthCheckRequest 健康检查请求 +type HealthCheckRequest struct { + Service string `json:"service,omitempty"` +} + +// HealthCheckResponse_ServingStatus 服务状态 +type HealthCheckResponse_ServingStatus int32 + +const ( + HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0 + HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 + HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 +) + +// HealthCheckResponse 健康检查响应 +type HealthCheckResponse struct { + Status HealthCheckResponse_ServingStatus `json:"status,omitempty"` +} + +// HelloRequest 问候请求 +type HelloRequest struct { + Name string `json:"name,omitempty"` +} + +// HelloResponse 问候响应 +type HelloResponse struct { + Message string `json:"message,omitempty"` + TraceId string `json:"trace_id,omitempty"` +} diff --git a/internal/grpc/pb/service_grpc.pb.go b/internal/grpc/pb/service_grpc.pb.go new file mode 100644 index 00000000..8c268f34 --- /dev/null +++ b/internal/grpc/pb/service_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// source: api/proto/service.proto +// +// To regenerate, run: +// protoc --go_out=. --go-grpc_out=. api/proto/service.proto + +package pb + +import ( + "context" + + "google.golang.org/grpc" +) + +// HealthServiceServer 健康检查服务接口 +type HealthServiceServer interface { + Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) +} + +// UnimplementedHealthServiceServer 未实现的健康检查服务(用于向前兼容) +type UnimplementedHealthServiceServer struct{} + +func (UnimplementedHealthServiceServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, grpc.Errorf(grpc.Code(nil), "method Check not implemented") +} + +// RegisterHealthServiceServer 注册健康检查服务到 gRPC 服务器 +func RegisterHealthServiceServer(s *grpc.Server, srv HealthServiceServer) { + s.RegisterService(&_HealthService_serviceDesc, srv) +} + +func _HealthService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HealthServiceServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.HealthService/Check", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HealthServiceServer).Check(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _HealthService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "api.HealthService", + HandlerType: (*HealthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _HealthService_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/proto/service.proto", +} + +// HelloServiceServer 示例 gRPC 服务接口 +type HelloServiceServer interface { + SayHello(context.Context, *HelloRequest) (*HelloResponse, error) +} + +// UnimplementedHelloServiceServer 未实现的示例服务 +type UnimplementedHelloServiceServer struct{} + +func (UnimplementedHelloServiceServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) { + return nil, grpc.Errorf(grpc.Code(nil), "method SayHello not implemented") +} + +// RegisterHelloServiceServer 注册示例服务到 gRPC 服务器 +func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) { + s.RegisterService(&_HelloService_serviceDesc, srv) +} + +func _HelloService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HelloServiceServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/api.HelloService/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HelloServiceServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _HelloService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "api.HelloService", + HandlerType: (*HelloServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _HelloService_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/proto/service.proto", +} diff --git a/internal/grpc/server/server.go b/internal/grpc/server/server.go new file mode 100644 index 00000000..f87ca3ff --- /dev/null +++ b/internal/grpc/server/server.go @@ -0,0 +1,77 @@ +package server + +import ( + "context" + "net" + + grpcInterceptor "github.com/xinliangnote/go-gin-api/internal/grpc/interceptor" + "github.com/xinliangnote/go-gin-api/internal/grpc/pb" + "github.com/xinliangnote/go-gin-api/internal/grpc/service" + + "go.uber.org/zap" + "google.golang.org/grpc" +) + +// GRPCServer gRPC 服务器封装 +type GRPCServer struct { + server *grpc.Server + listener net.Listener + logger *zap.Logger +} + +// New 创建 gRPC 服务器 +func New(logger *zap.Logger, addr string) (*GRPCServer, error) { + lis, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + // 链式拦截器:recovery -> logging + chainedInterceptor := chainUnaryInterceptors( + grpcInterceptor.UnaryRecoveryInterceptor(logger), + grpcInterceptor.UnaryLogInterceptor(logger), + ) + + s := grpc.NewServer( + grpc.UnaryInterceptor(chainedInterceptor), + ) + + // 注册服务 + pb.RegisterHealthServiceServer(s, service.NewHealthService()) + pb.RegisterHelloServiceServer(s, service.NewHelloService()) + + return &GRPCServer{ + server: s, + listener: lis, + logger: logger, + }, nil +} + +// Serve 启动 gRPC 服务(阻塞) +func (s *GRPCServer) Serve() error { + s.logger.Info("grpc server starting", zap.String("addr", s.listener.Addr().String())) + return s.server.Serve(s.listener) +} + +// GracefulStop 优雅停止 gRPC 服务 +func (s *GRPCServer) GracefulStop() { + s.logger.Info("grpc server stopping") + s.server.GracefulStop() +} + +// chainUnaryInterceptors 将多个 unary 拦截器串联 +func chainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + chain := handler + for i := len(interceptors) - 1; i >= 0; i-- { + currentInterceptor := interceptors[i] + next := chain + chain = func(currentCtx context.Context, currentReq interface{}) (interface{}, error) { + return currentInterceptor(currentCtx, currentReq, info, func(ctx context.Context, req interface{}) (interface{}, error) { + return next(ctx, req) + }) + } + } + return chain(ctx, req) + } +} diff --git a/internal/grpc/service/health.go b/internal/grpc/service/health.go new file mode 100644 index 00000000..212a0aaa --- /dev/null +++ b/internal/grpc/service/health.go @@ -0,0 +1,24 @@ +package service + +import ( + "context" + + "github.com/xinliangnote/go-gin-api/internal/grpc/pb" +) + +// HealthService 健康检查服务实现 +type HealthService struct { + pb.UnimplementedHealthServiceServer +} + +// NewHealthService 创建健康检查服务 +func NewHealthService() *HealthService { + return &HealthService{} +} + +// Check 执行健康检查 +func (s *HealthService) Check(ctx context.Context, req *pb.HealthCheckRequest) (*pb.HealthCheckResponse, error) { + return &pb.HealthCheckResponse{ + Status: pb.HealthCheckResponse_SERVING, + }, nil +} diff --git a/internal/grpc/service/hello.go b/internal/grpc/service/hello.go new file mode 100644 index 00000000..4140242a --- /dev/null +++ b/internal/grpc/service/hello.go @@ -0,0 +1,47 @@ +package service + +import ( + "context" + "fmt" + + "github.com/xinliangnote/go-gin-api/internal/grpc/pb" + "github.com/xinliangnote/go-gin-api/pkg/trace" + + "google.golang.org/grpc/metadata" +) + +// HelloService 示例 gRPC 服务实现 +type HelloService struct { + pb.UnimplementedHelloServiceServer +} + +// NewHelloService 创建示例服务 +func NewHelloService() *HelloService { + return &HelloService{} +} + +// SayHello 问候 +func (s *HelloService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { + name := req.Name + if name == "" { + name = "World" + } + + // 从 metadata 中获取 trace id + var traceId string + if md, ok := metadata.FromIncomingContext(ctx); ok { + if vals := md.Get(trace.Header); len(vals) > 0 { + traceId = vals[0] + } + } + + if traceId == "" { + t := trace.New("") + traceId = t.ID() + } + + return &pb.HelloResponse{ + Message: fmt.Sprintf("Hello, %s!", name), + TraceId: traceId, + }, nil +} diff --git a/main.go b/main.go index ec1573bb..91945135 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/xinliangnote/go-gin-api/configs" + grpcServer "github.com/xinliangnote/go-gin-api/internal/grpc/server" "github.com/xinliangnote/go-gin-api/internal/router" "github.com/xinliangnote/go-gin-api/pkg/env" "github.com/xinliangnote/go-gin-api/pkg/logger" @@ -78,6 +79,18 @@ func main() { } }() + // 初始化 gRPC 服务 + gs, err := grpcServer.New(accessLogger, configs.ProjectGRPCPort) + if err != nil { + accessLogger.Fatal("grpc server startup err", zap.Error(err)) + } + + go func() { + if err := gs.Serve(); err != nil { + accessLogger.Fatal("grpc server serve err", zap.Error(err)) + } + }() + // 优雅关闭 shutdown.NewHook().Close( // 关闭 http server @@ -118,5 +131,12 @@ func main() { s.CronServer.Stop() } }, + + // 关闭 gRPC server + func() { + if gs != nil { + gs.GracefulStop() + } + }, ) }