这里 使用的是 grpc 和 gin 框架 监听同一端口 的实现方式。

1、方式一:grpc 与 gin-http 分流

主要是 在 请求 刚到达服务端 时 就 自定义 包含 grpc.ServeHTTP(w, r) 和 router.ServeHTTP(w, r) 的 http.Handler 进行分流。

服务端代码如下:

// UserInfoService 用户服务
type UserInfoService struct{
    pb.UnimplementedUserInfoServiceServer
}

// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
    // 通过用户名查询用户信息
    name := req.Name
    fmt.Println("userinfo: ", name)
    // 数据里查用户信息
    if name == "zs" {
        resp = &pb.UserResponse{
            Id:    1,
            Name:  name,
            Age:   22,
            Hobby: []string{"Sing", "Run"},
        }
    }
    return
}

type HelloService struct {
    pb.UnimplementedHelloServiceServer
}

func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
    resp = new(pb.HelloResponse)
    resp.Result  = fmt.Sprintf("hello %s", in.Name)
    return resp, nil
}


// 方案一:grpc与gin-http分流
// grpc无法使用gin的http访问
func main(){

    // 初始化grpc服务
    s := grpc.NewServer()

    // 注册grpc服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    router := gin.Default()

    // api路由
    router.GET("/hello", func(ctx *gin.Context) {
        fmt.Println("已进入到HTTP实现")
        ctx.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "success",
        })
    })

    // 监听端口并处理服务分流
    h2Handler := h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 判断协议是否为http/2 && 是grpc
        if r.ProtoMajor == 2 &&
            strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
            // 按grpc方式来请求
            s.ServeHTTP(w, r)
        } else {
            // 当作普通api
            router.ServeHTTP(w, r)
        }
    }), &http2.Server{})

    // 监听HTTP服务
    // router.Run() 本质就是 err = http.ListenAndServe(address, router.Handler())
    // router.Handler() 本质就是 h2c.NewHandler(engine, &http2.Server{})
    // h2c.NewHandler() 的 第一个 参数 是 实现了 http.Handler{ ServeHTTP(ResponseWriter, *Request) } 接口的 对象
    // 即 h2c.NewHandler(engine, &http2.Server{}) 本质上就是 h2c.NewHandler(http.HandlerFunc(router.ServeHTTP(w, r)), &http2.Server{})
    // 此处,在 请求 刚到达服务端 时 就 自定义 包含 grpc.ServeHTTP(w, r) 和 router.ServeHTTP(w, r) 的 http.Handler 进行分流。
    if err := http.ListenAndServe(":8280", h2Handler); err != nil {
        log.Println("http server done:", err.Error())
    }
}

2、方式二:grpc 与 gin-http 分流,并使用 gin 代理 grpc-gateway 请求

这个方式是在 方式一的基础之上,进行完善,并利用 grpc-gateway 映射成 api 请求 grpc 服务

再使用 gin 中的 路由功能 代理 grpc-gateway 路由

具体代码如下:

// UserInfoService 用户服务
type UserInfoService struct{
    pb.UnimplementedUserInfoServiceServer
}

// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
    // 通过用户名查询用户信息
    name := req.Name
    fmt.Println("userinfo: ", name)
    // 数据里查用户信息
    if name == "zs" {
        resp = &pb.UserResponse{
            Id:    1,
            Name:  name,
            Age:   22,
            Hobby: []string{"Sing", "Run"},
        }
    }
    return
}

type HelloService struct {
    pb.UnimplementedHelloServiceServer
}

func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
    resp = new(pb.HelloResponse)
    resp.Result  = fmt.Sprintf("hello %s", in.Name)
    return resp, nil
}


// 方式二:grpc与gin-http分流,并使用 gin 代理 grpc-gateway 请求
// 代理方式有以下两种:
// A、使用 gin 中的 NoRoute 方法,在gin中未找到 路由时,再去 grpc-gateway 中寻找
// B、通过 gin use 全局优先使用 grpc-gateway 路由
func main2(){
    // 初始化grpc服务
    s := grpc.NewServer()

    // 注册grpc服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    // 开启 grpc-gateway
    // runtime.NewServeMux() 返回一个实现了 http.Handler 的 ServeMux 
    gwMux := runtime.NewServeMux()

    ctx := context.Background()
    dopts := []grpc.DialOption{grpc.WithInsecure()}

    endpoint := fmt.Sprintf("127.0.0.1:%v", 8280)

    // 绑定 grpc 服务 到 gateway 路由
    // 此处通过 grpc-gateway 实现了 http 到 grpc 的服务映射
    // 其本质是 gwMux 实现了 http.Handler,在 http 请求进来后,匹配 gateway 映射 的 grpc 绑定关系,并发起 grpc 请求
    if err := pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dopts); err != nil {
        log.Fatal("Failed to register gw server: ", err)
    }

    if err := pb.RegisterUserInfoServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dopts); err != nil {
        log.Fatal("Failed to register gw server: ", err)
    }


    // 实例化 gin 路由
    router := gin.Default()

    router.Use(func(ctx *gin.Context) {
        fmt.Println(ctx.Request)
        ctx.Next()
    })

    // api路由
    router.GET("/hello", func(ctx *gin.Context) {
        fmt.Println("已进入到HTTP实现")
        ctx.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "success",
        })
    })

    // 方式A:全局拦截,在此之后 注册的 gin 路由 会被 grpc-gateway 中 路由 覆盖,无法请求
    router.Use(func(ctx *gin.Context) {
        ctx.Status(http.StatusOK)
        gwMux.ServeHTTP(ctx.Writer, ctx.Request)
        ctx.Abort()
    })

    // 方式B:无法在 gin 中找到路由时,再根据 grpc-gateway 进行路由
    //router.NoRoute(wrapH(gwMux))
    //router.NoRoute(func(c *gin.Context) {
    //    // reset 404,用于重置未找到路由时的状态返回码
    //    c.Status(http.StatusOK) 
    //    gwMux.ServeHTTP(c.Writer, c.Request)
    //})

    // 使用 方式A 时,这里的 gin 路由 不会被请求到 
    router.POST("/example/echo", func(c *gin.Context) {
        c.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "success",
        })
    })


    // 监听端口并处理服务分流
    h2Handler := h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("1、进入处理服务分流")
        // 判断协议是否为http/2 && 是grpc
        if r.ProtoMajor == 2 &&
            strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
            // 按grpc方式来请求
            fmt.Println("A、进入grpc请求")
            s.ServeHTTP(w, r)
            fmt.Println("=================")
        } else {
            // 当作普通api
            fmt.Println("B、进入gin请求")
            router.ServeHTTP(w, r)
            fmt.Println("=================")
        }
    }), &http2.Server{})

    // 监听HTTP服务 
    // router.Run() 本质就是 err = http.ListenAndServe(address, router.Handler())
    // router.Handler() 本质就是 h2c.NewHandler(engine, &http2.Server{})
    // h2c.NewHandler() 的 第一个 参数 是 实现了 http.Handler{ ServeHTTP(ResponseWriter, *Request) } 接口的 对象
    // 即 h2c.NewHandler(engine, &http2.Server{}) 本质上就是 h2c.NewHandler(http.HandlerFunc(router.ServeHTTP(w, r)), &http2.Server{})
    // 此处,在 请求 刚到达服务端 时 就 自定义 包含 grpc.ServeHTTP(w, r) 和 router.ServeHTTP(w, r) 的 http.Handler 进行分流。
    if err := http.ListenAndServe(":8280", h2Handler); err != nil {
        log.Println("http server done:", err.Error())
    }
}

代理方式有以下两种:

  • A、使用 gin 中的 NoRoute 方法,在gin中未找到 路由时,再去 grpc-gateway 中寻找
  • B、通过 gin use 全局优先使用 grpc-gateway 路由

3、方式三:不分流方案实现,不区分 grpc 和 gin-http 流

备注:此方式 统一使用 gin 入口 访问 grpc 和 普通 api 接口,必须使用 https。

主要是由于 grpc是基于 HTTP2.0 协议的, 而使用 HTTP2.0 基本需要使用 https。

即此方式需要配置证书。

http标准库会为配置了证书的http服务自动选用http/2协议,grpc建立在http/2协议上,所以没有配置SSL证书时请勿使用该方式

服务端代码:

// UserInfoService 用户服务
type UserInfoService struct{
    pb.UnimplementedUserInfoServiceServer
}

// GetUserInfo 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
    // 通过用户名查询用户信息
    name := req.Name
    fmt.Println("userinfo: ", name)
    // 数据里查用户信息
    if name == "zs" {
        resp = &pb.UserResponse{
            Id:    1,
            Name:  name,
            Age:   22,
            Hobby: []string{"Sing", "Run"},
        }
    }
    return
}

type HelloService struct {
    pb.UnimplementedHelloServiceServer
}

func (h *HelloService) SayHello(ctx context.Context, in *pb.HelloRequest) (resp *pb.HelloResponse, err error) {
    resp = new(pb.HelloResponse)
    resp.Result  = fmt.Sprintf("hello %s", in.Name)
    return resp, nil
}


// grpcHandlerFunc 根据数据流 区分是 grpc 还是 普通的 http
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    if otherHandler == nil {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            grpcServer.ServeHTTP(w, r)
        })
    }
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            fmt.Println("A.进入grpcServer.ServeHTTP")
            fmt.Println(time.Now().UnixNano())
            grpcServer.ServeHTTP(w, r)
        } else {
            fmt.Println("B.进入otherHandler.ServeHTTP")
            fmt.Println(time.Now().UnixNano())
            otherHandler.ServeHTTP(w, r)
        }
    })
}

// wrapH overwrite gin.WrapH
func wrapH(h http.Handler) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Status(http.StatusOK) // reset 404
        h.ServeHTTP(c.Writer, c.Request)
    }
}

// 获取tls配置
func getTLSConfig() *tls.Config {
    cert, _ := ioutil.ReadFile("./framework/grpc/keys/server.pem")
    key, _ := ioutil.ReadFile("./framework/grpc/keys/server.key")
    var demoKeyPair *tls.Certificate
    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        log.Fatal("TLS KeyPair err: ", err)
    }
    demoKeyPair = &pair
    return &tls.Config{
        Certificates: []tls.Certificate{*demoKeyPair},
        NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
    }
}


// 方案三:不分流方案实现,不区分 grpc 和 gin-http 流。
// 备注: 统一使用 gin 入口时,必须使用 https 
func main(){

    // 1.实例化gRPC
    s := grpc.NewServer()

    // 注册grpc服务
    var u = UserInfoService{}
    pb.RegisterUserInfoServiceServer(s, &u)

    var h = HelloService{}
    pb.RegisterHelloServiceServer(s, &h)

    // 开启 grpc-gateway
    // runtime.NewServeMux() 返回一个实现了 http.Handler 的 ServeMux 
    gwMux := runtime.NewServeMux()

    ctx := context.Background()

    // 使用 tls
    dcreds, err := credentials.NewClientTLSFromFile("./framework/grpc/keys/server.pem", "www.admincms.com")
    if err != nil {
        log.Fatal("Failed to create client TLS credentials", err)
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}

    //endpoint := fmt.Sprintf("0.0.0.0:%v", 8280)
    endpoint := fmt.Sprintf("127.0.0.1:%v", 8280)

    // 绑定 grpc 服务 到 gateway 路由
    // 此处通过 grpc-gateway 实现了 http 到 grpc 的服务映射
    // 其本质是 gwMux 实现了 http.Handler,在 http 请求进来后,匹配 gateway 映射 的 grpc 绑定关系,并发起 grpc 请求
    if err := pb.RegisterHelloServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dopts); err != nil {
        log.Fatal("Failed to register gw server: ", err)
    }

    if err := pb.RegisterUserInfoServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dopts); err != nil {
        log.Fatal("Failed to register gw server: ", err)
    }


    // 实例化 gin 路由
    router := gin.Default()

    // api路由
    router.GET("/hello", func(ctx *gin.Context) {
        fmt.Println("已进入到HTTP实现")
        ctx.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "success",
        })
    })

    router.POST("/example/echo", func(ctx *gin.Context) {
        fmt.Println("已进入到HTTP实现")
        ctx.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "example/echo",
        })
    })

    // 方式A:使用gin全局代理 grpc 和 grpc-gateway, 在此之后 注册的 gin 路由无效 
    //router.Use(func(ctx *gin.Context) {
    //    if ctx.Request.ProtoMajor == 2 &&
    //        strings.HasPrefix(ctx.Request.Header.Get("Content-Type"), "application/grpc") {
    //        ctx.Status(http.StatusOK)
    //        s.ServeHTTP(ctx.Writer, ctx.Request)
    //        // 不要再往下请求了,防止继续链式调用拦截器
    //        ctx.Abort()
    //        return
    //    }else{
    //        ctx.Status(http.StatusOK)
    //        gwMux.ServeHTTP(ctx.Writer, ctx.Request)
    //        // 不要再往下请求了,防止继续链式调用拦截器
    //        ctx.Abort()
    //        return
    //    }
    //})

    // 方式B,优先使用 gin 的路由,待找不到时,分流才查找 grpc服务 和 grpc-gateway 路由
    router.NoRoute(wrapH(grpcHandlerFunc(s, gwMux)))

    // 使用 方式A 时,这里的 gin 路由 不会被请求到 
    router.GET("/test", func(ctx *gin.Context) {
        fmt.Println("已进入到HTTP实现")
        ctx.JSON(200, map[string]interface{}{
            "code": 200,
            "msg:": "success",
        })
    })

    // 必须启动 gin tls 服务
    // tls 启动实现方式1:
    err = router.RunTLS(
        ":8280",
        "./framework/grpc/keys/server.pem",
        "./framework/grpc/keys/server.key",
    )
    if err != nil {
        log.Println("https server done:", err.Error())
    }

    // tls 启动的实现方式2:(实际就是方式1的底层实现)
    //srv := &http.Server{
    //    Addr:        endpoint,
    //    Handler:     router,
    //    TLSConfig:     getTLSConfig(),
    //}
    //conn, err := net.Listen("tcp", endpoint)
    //if err != nil {
    //    log.Fatal("Failed to listen: ", err)
    //}
    //if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
    //    log.Fatal("ListenAndServe: ", err)
    //}
}

总结

  • 当不需要使用 SSL 证书 或 SSL 证书 配置在 Nginx 等服务上时,请使用 方式一 或 方式二。

  • 方式三仅限给Gin配置了SSL证书。

建议使用 方式二,可以灵活在 Nginx 等服务端配置证书。

作者:joker.liu  创建时间:2023-05-27 15:46
最后编辑:joker.liu  更新时间:2023-05-27 16:06