本文是基于之前介绍 gRPC 开发文章的延续,代码模块介绍,也是基于之前示例代码的延续。

1. ManagedChannel

ManagedChannel 是 gRPC 中用于管理客户端和服务器之间通信的核心组件。它负责连接的创建、管理、负载均衡、流量控制等功能。以下是 ManagedChannel 的主要功能和属性。

  1. 连接管理

    • ManagedChannel 负责与服务器节点建立和维护 TCP 连接。
    • 支持 HTTP/2 协议的多路复用。
  2. 负载均衡

    • 支持多种负载均衡策略,如轮询(Round Robin)、随机、哈希等。
    • 可以通过服务发现机制动态选择服务器节点。
  3. 名称解析

    • 支持通过 DNS、Etcd 等进行服务名称解析,以确定目标服务器地址。
  4. 流量控制

    • 支持基于流的流量控制机制,以避免流量过载。
  5. 拦截器

    • 支持客户端拦截器,用于在请求发送和响应接收时进行自定义处理。
  6. 安全性

    • 支持 TLS/SSL 加密,以确保数据传输的安全性。
  7. 重试策略

    • 可以配置重试策略,以应对临时的网络故障或服务器错误。
  8. 超时和截止时间

    • 支持请求的超时设置和截止时间控制。

当然,ManagedChannelBuilder 是用于构建 ManagedChannel 的构建器类,它提供了多种方法来配置和定制通道的行为。下面是 ManagedChannelBuilder 的一些重要属性和方法的详细介绍,包括 forTarget 方法。

1.1. ManagedChannelBuilder

  1. forAddress(String name, int port)

    • 用于指定目标服务器的主机名和端口。
    • 适合直接连接到单个服务器节点的场景。
  2. forTarget(String target)

    • 用于指定目标服务的 URI,这个 URI 可以包含服务名称、负载均衡策略、端口等。
    • 适合使用服务发现和负载均衡的场景。例如,dns:///example.com:8080
  3. usePlaintext()

    • 启用明文通信,不使用 TLS/SSL 加密。
    • 适用于开发和测试环境。在生产环境中,通常应使用加密通信。
  4. useTransportSecurity()

    • 启用 TLS/SSL 加密,以确保数据传输的安全性。
    • 适用于生产环境。
  5. defaultLoadBalancingPolicy(String policy)

    • 设置默认的负载均衡策略,如 round_robinpick_first 等。
    • 这会影响请求如何在多个服务器节点之间分配。
  6. enableRetry()

    • 启用重试机制,以提高请求的可靠性。
    • 通常与 maxRetryAttempts(int attempts) 一起使用。
  7. maxRetryAttempts(int attempts)

    • 设置最大重试次数。
    • 配合重试策略使用,帮助应对临时的网络故障。
  8. idleTimeout(long value, TimeUnit unit)

    • 设置通道的空闲超时时间。
    • 如果通道在指定时间内未使用,将被关闭以释放资源。
  9. keepAliveTime(long keepAliveTime, TimeUnit timeUnit)

    • 设置保活时间,以确保连接的活跃性。
    • 定期发送保活探测包,以防止连接被网络设备意外断开。
  10. keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit)

    • 设置保活超时时间。
    • 如果在超时时间内未收到响应,连接将被认为是失败的。
  11. maxInboundMessageSize(int bytes)

    • 设置允许接收的最大消息大小(字节)。
    • 适用于需要接收大消息的场景。
  12. intercept(ClientInterceptor... interceptors)

    • 添加一个或多个客户端拦截器。
    • 用于在请求发送和响应接收时执行自定义逻辑。
代码示例

以下是一个使用 ManagedChannelBuilder 的示例,展示如何配置不同的属性:

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.ClientInterceptor;

public class GrpcClientExample {
    public static void main(String[] args) {
        // 使用 forTarget 方法指定服务目标
        ManagedChannel channel = ManagedChannelBuilder.forTarget("dns:///example.com:8080")
                .useTransportSecurity() // 启用 TLS/SSL 加密
                .defaultLoadBalancingPolicy("round_robin") // 使用轮询负载均衡策略
                .enableRetry() // 启用重试机制
                .maxRetryAttempts(3) // 设置最大重试次数
                .idleTimeout(5, TimeUnit.MINUTES) // 设置空闲超时时间
                .keepAliveTime(1, TimeUnit.MINUTES) // 设置保活时间
                .maxInboundMessageSize(10 * 1024 * 1024) // 设置最大入站消息大小为 10 MB
                .intercept(new MyClientInterceptor()) // 添加自定义拦截器
                .build();

        // 使用存根与服务通信
        // MyServiceGrpc.MyServiceBlockingStub stub = MyServiceGrpc.newBlockingStub(channel);

        // 关闭通道
        channel.shutdown();
    }
}

// 自定义拦截器示例
class MyClientInterceptor implements ClientInterceptor {
    // 实现拦截器逻辑
}
解释
  • forTarget("dns:///example.com:8080"):指定服务目标,使用 DNS 进行名称解析和负载均衡。
  • useTransportSecurity():启用加密通信,确保数据安全。
  • defaultLoadBalancingPolicy("round_robin"):配置为轮询策略,确保请求均匀分布到所有可用节点。
  • enableRetry() 和 maxRetryAttempts(3):启用重试机制,设置最大重试次数为 3 次,以提高可靠性。
  • idleTimeout 和 keepAliveTime:配置连接的空闲超时和保活设置,以优化资源管理和连接稳定性。
  • maxInboundMessageSize:允许接收的最大消息大小为 10 MB。
  • intercept:添加自定义拦截器,用于在 gRPC 调用中插入自定义逻辑。

这些配置选项使得 ManagedChannelBuilder 能够灵活地适应各种应用需求,提供可靠的 gRPC 客户端通信。

1.2. 连接管理

ManagedChannel 的连接管理是 gRPC 框架中一个核心功能,它确保客户端与服务器之间的通信高效且可靠。连接管理涉及多个方面,包括连接的建立、维护、复用、负载均衡以及连接的关闭和清理。以下是对 ManagedChannel 连接管理机制的详细介绍:

1. 连接建立
  1. 目标地址解析

    • 当创建 ManagedChannel 时,可以通过 forAddressforTarget 方法指定目标服务器地址。
    • forAddress(String name, int port) 用于直接指定主机名和端口。
    • forTarget(String target) 支持更复杂的目标解析,包括通过 DNS 或其他服务发现机制解析服务名称。
  2. 协议支持

    • ManagedChannel 使用 HTTP/2 协议,这允许在单个 TCP 连接上进行多路复用。
    • 支持安全通信,通过 useTransportSecurity() 方法启用 TLS/SSL。
2. 连接维护与复用
  1. 多路复用

    • HTTP/2 的多路复用特性允许在一个连接上并发多个请求和响应,从而提高连接的利用率。
    • ManagedChannel 自动管理连接的复用,无需手动干预。
  2. 连接保活

    • 通过 keepAliveTime(long keepAliveTime, TimeUnit timeUnit) 配置保活探测,确保连接在空闲时不会被网络设备意外断开。
    • keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) 设置保活超时时间,确保在指定时间内未收到响应时关闭连接。
  3. 空闲连接管理

    • idleTimeout(long value, TimeUnit unit) 用于设置连接的空闲超时时间。
    • 当连接在指定时间内未使用时,ManagedChannel 会自动关闭连接以释放资源。
3. 负载均衡与服务发现
  1. 负载均衡

    • 支持多种负载均衡策略(如轮询、随机选择),可通过 defaultLoadBalancingPolicy(String policy) 配置。
    • 负载均衡器会在多个服务器节点之间分配请求,以实现负载均衡。
  2. 服务发现

    • ManagedChannel 支持动态服务发现,通过名称解析机制(如 DNS)自动选择可用的服务器节点。
    • 这种机制确保客户端可以根据集群的动态变化调整目标节点。
4. 连接关闭与清理
  1. 优雅关闭

    • shutdown() 方法用于请求通道的优雅关闭,允许正在进行的请求完成。
    • awaitTermination(long timeout, TimeUnit unit) 等待通道终止,确保资源的正确释放。
  2. 强制关闭

    • shutdownNow() 方法用于强制关闭通道,立即终止所有活动请求。
    • 强制关闭后,通道不能再用于发送请求。
5. 代码示例
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class GrpcClient {
    private final ManagedChannel channel;

    public GrpcClient(String host, int port) {
        // 创建并配置 ManagedChannel
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // 明文通信
                .enableRetry() // 启用重试机制
                .keepAliveTime(1, TimeUnit.MINUTES) // 设置保活时间
                .idleTimeout(5, TimeUnit.MINUTES) // 设置空闲超时时间
                .build();
    }

    public void shutdown() throws InterruptedException {
        // 请求优雅关闭通道
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public static void main(String[] args) throws InterruptedException {
        GrpcClient client = new GrpcClient("localhost", 50051);
        try {
            // 执行 RPC 调用
        } finally {
            client.shutdown();
        }
    }
}

1.3. 高并发调优

在高并发场景下,如果单个连接成为瓶颈,ManagedChannel 可以通过几种方法来优化性能,以更好地处理高负载请求。以下是一些优化策略:

1. 增加并发流限制
  • HTTP/2 多路复用:默认情况下,HTTP/2 支持多路复用,即在一个连接上同时发送多个请求。但服务器和客户端都有最大并发流的限制。
  • 调整最大并发流:可以通过配置来增加单个连接上允许的最大并发流数量,从而提高单个连接的利用率。
2. 使用多个连接
  • 并行连接:如果一个连接的多路复用能力仍然不足以处理所有并发请求,可以考虑同时使用多个连接到同一个节点。
  • 配置通道池:虽然 gRPC 本身没有显式的连接池配置,但可以在应用层面创建多个 ManagedChannel 实例来实现类似的效果。
3. 优化负载均衡策略
  • 自定义负载均衡策略:使用自定义负载均衡策略来动态调整请求分配,以便更好地利用可用的连接和节点资源。
  • 健康检查:确保负载均衡器能根据节点的健康状态动态调整请求分配,避免将请求发送到负载过高的节点。
4. 网络和协议优化
  • 压缩:启用 gRPC 的压缩功能,减少数据传输量,缓解网络瓶颈。
  • 连接保持活动:通过配置保持连接活动,避免因连接建立和关闭导致的开销。
5. 服务器端优化
  • 服务器扩展:在服务器端增加更多的节点或提升单个节点的处理能力,以分散和处理更多的请求。
  • 请求队列和批处理:优化服务器端的请求处理策略,例如使用请求队列和批处理来提高吞吐量。
示例代码:调整最大并发流

以下是如何在 Java 中调整 ManagedChannel 的最大并发流配置:

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class HighConcurrencyClient {
    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("server.example.com", 50051)
                .usePlaintext()
                .maxInboundMessageSize(10 * 1024 * 1024) // 设置最大入站消息大小
                .build();

        // 使用存根与服务通信
        // MyServiceGrpc.MyServiceBlockingStub stub = MyServiceGrpc.newBlockingStub(channel);
        // MyResponse response = stub.myRpcMethod(MyRequest.newBuilder().build());

        // 关闭通道
        channel.shutdown();
    }
}

注意:maxInboundMessageSize 并不是调整并发流的参数,而是一个示例。具体的并发流限制需要通过其他方式在服务器端配置。

1.4. 连接集群服务

当客户端连接的服务端是集群服务,有多个主机节点,显然不可能依然只创建一个连接,应该想办法基于每个主机节点创建一个单独的连接。这里就用到了 forTarget方法。

1.4.1. forTarget

forTarget 方法是 gRPC 中用于指定服务端地址的关键方法之一。它是 ManagedChannelBuilder 的一部分,用于配置客户端与服务器之间的通信渠道。forTarget 方法的使用涉及到地址解析、负载均衡等机制。

1. 基本用法
ManagedChannel channel = ManagedChannelBuilder.forTarget("your-service-address:port")
        .usePlaintext()  // 如果不使用 TLS
        .build();
  • target: 这是一个字符串,通常是服务的地址,可以是以下几种形式:

    • 主机名和端口: 如 "localhost:50051"
    • 负载均衡器地址: 用于连接到负载均衡器,然后由负载均衡器将请求分发到后端服务器。
    • URI 形式: 可以包含方案,如 "dns:///example.com:443",这对于使用自定义的命名解析器很有用。
2. 机制和功能
  1. 地址解析

    • gRPC 使用名称解析器将目标字符串转换为实际的服务器地址。默认情况下,gRPC 支持多种解析方式,包括 DNS 和静态 IP。
    • 例如,dns:///example.com 使用 DNS 解析 example.com 的 IP 地址。
  2. 负载均衡

    • gRPC 内置支持客户端负载均衡。通过 forTarget 提供的地址,gRPC 客户端可以使用不同的负载均衡策略,如轮询。
    • 如果目标地址解析为多个 IP 地址,gRPC 将自动分配请求到这些地址上。
  3. 自定义命名解析器

    • 可以通过 SPI(Service Provider Interface)机制来实现自定义命名解析器。这在需要与服务发现系统集成时特别有用。
    • 自定义解析器可以解析复杂的服务目标,并动态更新可用服务器列表。
  4. 连接管理

    • ManagedChannel 自动管理与服务器的连接,包括重试、连接恢复等。
    • forTarget 配置的目标是连接管理的基础,确保客户端能够正确定位到服务。
  5. 安全性

    • 如果目标地址使用安全连接(如 HTTPS),需要配置 TLS。可以通过 useTransportSecurity 方法来设置。
    • 对于开发和测试环境,可以使用 usePlaintext 来禁用 TLS。

1.4.2. 连接管理机制

在 gRPC 中,ManagedChannel 的设计是为了高效管理和复用连接,特别是在集群环境下。当你使用 forTarget 方法并通过负载均衡策略连接到集群服务时,ManagedChannel 会尽量复用现有的连接,而不是为每个节点单独创建一个新的连接。以下是一些关键点,解释了 ManagedChannel 在连接超时时间内如何管理连接:

1. 连接复用
  1. HTTP/2 多路复用

    • ManagedChannel 使用 HTTP/2 协议,该协议支持在一个连接上并发多个请求和响应。
    • 这意味着即使在连接超时时间内,ManagedChannel 也会尽量复用现有的连接,而不是为每个请求创建新的连接。
  2. 连接池

    • 虽然 ManagedChannel 本身不实现传统的连接池,但它通过内部机制管理连接的创建和复用。
    • ManagedChannel 会维护一个连接池,根据需要打开新的连接或复用现有的连接。
2. 负载均衡和连接管理
  1. 负载均衡策略

    • 通过 defaultLoadBalancingPolicy 方法配置的负载均衡策略(如 round_robinpick_first 等)决定了请求如何分配到不同的节点。
    • 负载均衡器会根据策略选择合适的节点,并尝试复用现有的连接。
  2. 连接超时和重试

    • 如果在连接超时时间内无法成功连接到某个节点,ManagedChannel 会根据配置的重试策略尝试重新连接。
    • 重试机制可以在不同节点之间切换,但仍然会尽量复用现有的连接。
3. 具体行为
  • 首次连接

    • 当第一次请求时,ManagedChannel 会根据负载均衡策略选择一个节点并建立连接。
    • 这个连接会被缓存起来,以便后续请求复用。
  • 后续请求

    • 对于后续的请求,ManagedChannel 会优先复用现有的连接。
    • 如果现有连接不可用(例如,连接超时或节点故障),ManagedChannel 会尝试选择另一个节点并建立新的连接。
  • 连接超时

    • 如果在连接超时时间内无法成功连接到任何节点,ManagedChannel 会根据配置的重试策略进行重试。
    • 重试过程中,ManagedChannel 会尝试不同的节点,但仍然会尽量复用现有的连接。
4. 代码示例

以下是一个示例,展示了如何配置 ManagedChannel 以连接到集群服务,并处理连接超时和重试:

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class ClusterClient {
    public static void main(String[] args) {
        // 使用 forTarget 方法指定服务目标
        ManagedChannel channel = ManagedChannelBuilder.forTarget("dns:///my-service-cluster")
                .useTransportSecurity() // 启用 TLS/SSL 加密
                .defaultLoadBalancingPolicy("round_robin") // 使用轮询负载均衡策略
                .enableRetry() // 启用重试机制
                .maxRetryAttempts(3) // 设置最大重试次数
                .idleTimeout(5, TimeUnit.MINUTES) // 设置空闲超时时间
                .keepAliveTime(1, TimeUnit.MINUTES) // 设置保活时间
                .build();

        // 使用存根与服务通信
        // MyServiceGrpc.MyServiceBlockingStub stub = MyServiceGrpc.newBlockingStub(channel);

        // 执行 RPC 调用
        // MyResponse response = stub.myRpcMethod(MyRequest.newBuilder().build());

        // 关闭通道
        channel.shutdown();
    }
}

2. 拦截器

其实在上一篇文章的示例代码中,就有用到服务端、客户端拦截器。

2.1. 概念

gRPC 拦截器(Interceptor)是 gRPC 框架中用于拦截和处理 RPC 调用过程中的请求和响应的一种机制。拦截器可以在客户端和服务器端使用,允许开发者在请求被发送到服务器之前、以及响应被返回到客户端之前,执行一些自定义的逻辑。

1. 拦截器的类型
  1. 客户端拦截器(Client Interceptor)

    • 用于在客户端侧拦截 RPC 调用。
    • 可以用于修改请求元数据、记录日志、执行认证和授权等。
  2. 服务器端拦截器(Server Interceptor)

    • 用于在服务器端拦截 RPC 调用。
    • 可以用于验证请求、记录日志、处理异常、修改响应等。
2. 应用场景
  1. 日志记录

    • 拦截器可以用于记录每个 RPC 调用的详细信息,包括请求和响应的元数据、执行时间等。
    • 便于监控和调试。
  2. 认证和授权

    • 可以在拦截器中实现身份验证和权限检查,确保只有经过认证的请求才能访问服务。
    • 拦截器可以从请求的元数据中提取认证信息并进行验证。
  3. 异常处理

    • 服务器端拦截器可以用于统一处理异常,将内部异常转换为标准的 gRPC 状态码和消息。
  4. 请求/响应修改

    • 可以在请求发送之前或响应返回之前对其进行修改,例如添加或移除某些元数据。
  5. 性能监控和度量

    • 拦截器可以用于收集性能数据,如请求的延迟、吞吐量等。
    • 这些数据可以用于生成性能报告和识别瓶颈。

2.2. 使用

2.2.1. 客户端拦截器

客户端拦截器用于在客户端发送请求之前和接收到响应之后执行额外的逻辑。以下是客户端拦截器的详细使用方法和示例代码。

1. 创建客户端拦截器

客户端拦截器通过实现 ClientInterceptor 接口来创建。这个接口有一个方法 interceptCall,它允许你在请求发送之前和响应接收之后执行自定义逻辑。

import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.ForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;

public class ClientLoggingInterceptor implements ClientInterceptor {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                System.out.println("Client sending request to method: " + method.getFullMethodName());
                super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(responseListener) {
                    @Override
                    public void onMessage(RespT message) {
                        System.out.println("Client received message: " + message);
                        super.onMessage(message);
                    }

                    @Override
                    public void onClose(Status status, Metadata trailers) {
                        System.out.println("Client call closed with status: " + status);
                        super.onClose(status, trailers);
                    }
                }, headers);
            }

            @Override
            public void sendMessage(ReqT message) {
                System.out.println("Client sending message: " + message);
                super.sendMessage(message);
            }
        };
    }
}
2. 使用客户端拦截器

在创建客户端通道时,通过 ClientInterceptors 工具类将拦截器附加到通道。

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.StreamObserver;

public class HelloWorldClientWithInterceptor {
    private final GreeterGrpc.GreeterBlockingStub blockingStub;

    public HelloWorldClientWithInterceptor(String host, int port) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext()
                .build();

        // 将拦截器附加到通道
        channel = ClientInterceptors.intercept(channel, new ClientLoggingInterceptor());

        blockingStub = GreeterGrpc.newBlockingStub(channel);
    }

    public void greet(String name) {
        HelloRequest request = HelloRequest.newBuilder().setName(name).build();
        HelloResponse response = blockingStub.sayHello(request);
        System.out.println("Greeting: " + response.getMessage());
    }

    public static void main(String[] args) {
        HelloWorldClientWithInterceptor client = new HelloWorldClientWithInterceptor("localhost", 50051);
        client.greet("World");
    }
}

2.2.2. 服务端拦截器

服务端拦截器用于在服务端接收到请求之前和发送响应之后执行额外的逻辑。以下是服务端拦截器的详细使用方法和示例代码。

1. 创建服务端拦截器

服务端拦截器通过实现 ServerInterceptor 接口来创建。这个接口有一个方法 interceptCall,它允许你在请求到达服务实现之前和响应发送之前执行自定义逻辑。

import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;

public class ServerLoggingInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
        System.out.println("Server received call to method: " + call.getMethodDescriptor().getFullMethodName());
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
            @Override
            public void onMessage(ReqT message) {
                System.out.println("Server received message: " + message);
                super.onMessage(message);
            }

            @Override
            public void onHalfClose() {
                System.out.println("Server stream half-closed");
                super.onHalfClose();
            }

            @Override
            public void onComplete() {
                System.out.println("Server stream completed");
                super.onComplete();
            }

            @Override
            public void onCancel() {
                System.out.println("Server stream cancelled");
                super.onCancel();
            }

            @Override
            public void onReady() {
                System.out.println("Server stream ready");
                super.onReady();
            }
        };
    }
}
2. 使用服务端拦截器

在创建服务端时,通过 ServerInterceptors 工具类将拦截器附加到服务。

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

public class HelloWorldServerWithInterceptor {
    private final Server server;

    public HelloWorldServerWithInterceptor(int port) {
        server = ServerBuilder.forPort(port)
                .addService(ServerInterceptors.intercept(new GreeterImpl(), new ServerLoggingInterceptor()))
                .build();
    }

    public void start() throws IOException {
        server.start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            HelloWorldServerWithInterceptor.this.stop();
            System.err.println("*** server shut down");
        }));
    }

    public void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    private static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {
            HelloResponse response = HelloResponse.newBuilder()
                    .setMessage("Hello " + req.getName())
                    .build();
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        HelloWorldServerWithInterceptor server = new HelloWorldServerWithInterceptor(50051);
        server.start();
        server.server.awaitTermination();
    }
}

3. 请求头

gRPC 请求头的实现基于 Metadata 类,这是一种用于在客户端和服务器之间传递附加信息的键值对结构。gRPC 请求头允许你在调用过程中传递额外的上下文信息,比如认证令牌、跟踪 ID 等。下面是对 gRPC 请求头的详细介绍,包括其结构、使用方法和常见应用场景。

3.1. 元数据(Metadata)结构

  • 键(Key)

    • 键是一个字符串,必须以小写字母开头,并且只能包含小写字母、数字和连字符(-)。
    • 对于二进制数据,键必须以 -bin 结尾。
  • 值(Value)

    • 值可以是 ASCII 字符串或字节数组。
    • gRPC 提供了两种编组器(Marshaller)用于处理这两种类型的值:

      • Metadata.ASCII_STRING_MARSHALLER 用于 ASCII 字符串。
      • Metadata.BINARY_BYTE_MARSHALLER 用于字节数组。
1. 创建 Metadata 实例

你可以创建一个 Metadata 实例来存储键值对:

Metadata metadata = new Metadata();
2. 添加和检索键值对
  • 添加键值对

    Metadata.Key<String> key = Metadata.Key.of("custom-header", Metadata.ASCII_STRING_MARSHALLER);
    metadata.put(key, "value");
  • 检索键值对

    String value = metadata.get(key);
3. 二进制数据

对于二进制数据,键需要以 -bin 结尾:

Metadata.Key<byte[]> binaryKey = Metadata.Key.of("custom-header-bin", Metadata.BINARY_BYTE_MARSHALLER);
metadata.put(binaryKey, new byte[]{0x01, 0x02});
byte[] binaryValue = metadata.get(binaryKey);

3.2. 使用请求头

在 gRPC 中,通过 ServerInterceptor 可以拦截请求并访问请求头。如果你希望在具体的服务实现类中也能够访问这些请求头,可以将请求头通过上下文传递给服务实现。以下是如何在 GreeterImpl 服务实现中访问请求头的完整示例:

  1. 定义服务和拦截器
import io.grpc.*;

public class HelloWorldServerWithHeaders {
    private final Server server;

    public HelloWorldServerWithHeaders(int port) {
        server = ServerBuilder.forPort(port)
                .addService(ServerInterceptors.intercept(new GreeterImpl(), new HeaderInterceptor()))
                .build();
    }

    public void start() throws IOException {
        server.start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            HelloWorldServerWithHeaders.this.stop();
            System.err.println("*** server shut down");
        }));
    }

    public void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    private static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
        @Override
        public void sayHello(HelloRequest req, StreamObserver<HelloResponse> responseObserver) {
            // 从当前上下文中获取请求头
            Metadata headers = ServerCallContext.getHeaders();
            Metadata.Key<String> customHeaderKey = Metadata.Key.of("custom-header", Metadata.ASCII_STRING_MARSHALLER);
            String headerValue = headers.get(customHeaderKey);
            System.out.println("Service received header: " + headerValue);

            HelloResponse response = HelloResponse.newBuilder()
                    .setMessage("Hello " + req.getName())
                    .build();
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }
    }

    private static class HeaderInterceptor implements ServerInterceptor {
        @Override
        public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
                ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
            // 将请求头存储在上下文中
            Context ctx = Context.current().withValue(ServerCallContext.HEADERS_KEY, headers);
            return Contexts.interceptCall(ctx, call, headers, next);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        HelloWorldServerWithHeaders server = new HelloWorldServerWithHeaders(50051);
        server.start();
        server.server.awaitTermination();
    }
}
  1. 上下文类定义

为了在服务实现中访问请求头,我们需要一个上下文类来存储和获取这些头信息:

import io.grpc.Context;
import io.grpc.Metadata;

public class ServerCallContext {
    // 创建一个上下文键,用于存储 Metadata
    public static final Context.Key<Metadata> HEADERS_KEY = Context.key("metadata-headers");

    public static Metadata getHeaders() {
        // 从当前上下文中获取存储的 Metadata
        return HEADERS_KEY.get();
    }
}
说明
  • HeaderInterceptor

    • 在拦截器中,使用 Context 将请求头信息存储起来。
    • 使用 Contexts.interceptCall 将上下文与调用关联。
  • GreeterImpl

    • 在服务实现中,使用自定义的 ServerCallContext 类获取请求头信息。
    • 通过 Context 机制,可以在服务实现中访问 Metadata 对象。

通过这种方式,你可以在 gRPC 服务实现中访问客户端发送的请求头信息。使用 Context 是在 gRPC 中传递请求上下文信息的一种常见方法。

3.3. 常见应用场景

  1. 认证和授权

    • 通过请求头传递认证令牌(如 JWT、OAuth)。
    • 服务器拦截器可以验证令牌的有效性并拒绝未授权的请求。
  2. 请求跟踪

    • 传递唯一的请求跟踪 ID,用于分布式追踪系统。
    • 帮助在微服务架构中追踪请求的生命周期。
  3. 版本控制和特性标志

    • 客户端可以通过请求头传递所需的 API 版本或启用的特性。
    • 服务器可以根据这些信息调整响应行为。
  4. 自定义业务逻辑

    • 传递特定业务上下文信息以影响服务端的处理逻辑。

4. 状态码

在 gRPC 中,状态码用于表示 RPC 调用的结果。它们帮助客户端了解请求是成功还是失败,以及如果失败,是什么原因导致的。gRPC 定义了一组状态码,每个状态码都有特定的用途和应用场景。以下是 gRPC 状态码的常见应用场景及其详细使用示例。

4.1. 常见状态码

  1. OK (Status.Code.OK)

    • 场景:请求成功,服务端正确处理了请求并返回了期望的结果。
    • 示例:客户端请求数据检索操作,服务端成功返回数据。
  2. CANCELLED (Status.Code.CANCELLED)

    • 场景:操作被客户端取消。
    • 示例:客户端在长时间的流式响应中取消了请求。
    • 使用

      responseObserver.onError(Status.CANCELLED.withDescription("Operation was cancelled by client").asRuntimeException());
  3. UNKNOWN (Status.Code.UNKNOWN)

    • 场景:发生未知错误,通常是由于服务器端抛出异常。
    • 示例:服务端捕获到未处理的异常。
    • 使用

      try {
          // some operation that might throw an exception
      } catch (Exception e) {
          responseObserver.onError(Status.UNKNOWN.withDescription("An unknown error occurred").withCause(e).asRuntimeException());
      }
  4. INVALID_ARGUMENT (Status.Code.INVALID_ARGUMENT)

    • 场景:客户端提供了无效的参数。
    • 示例:客户端发送了不符合格式要求的请求数据。
    • 使用

      if (request.getName() == null || request.getName().isEmpty()) {
          responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("Name cannot be empty").asRuntimeException());
      }
  5. DEADLINE_EXCEEDED (Status.Code.DEADLINE_EXCEEDED)

    • 场景:操作未在指定的时间内完成。
    • 示例:客户端设置的请求超时时间被超过。
    • 使用

      responseObserver.onError(Status.DEADLINE_EXCEEDED.withDescription("Deadline exceeded").asRuntimeException());
  6. NOT_FOUND (Status.Code.NOT_FOUND)

    • 场景:请求的资源不存在。
    • 示例:客户端请求的记录在数据库中不存在。
    • 使用

      responseObserver.onError(Status.NOT_FOUND.withDescription("Resource not found").asRuntimeException());
  7. ALREADY_EXISTS (Status.Code.ALREADY_EXISTS)

    • 场景:尝试创建已存在的资源。
    • 示例:客户端尝试创建已存在的用户账号。
    • 使用

      responseObserver.onError(Status.ALREADY_EXISTS.withDescription("Resource already exists").asRuntimeException());
  8. PERMISSION_DENIED (Status.Code.PERMISSION_DENIED)

    • 场景:客户端没有执行该操作的权限。
    • 示例:未授权的用户尝试访问受限资源。
    • 使用

      responseObserver.onError(Status.PERMISSION_DENIED.withDescription("Permission denied").asRuntimeException());
  9. RESOURCE_EXHAUSTED (Status.Code.RESOURCE_EXHAUSTED)

    • 场景:资源耗尽,通常是配额或限流问题。
    • 示例:超出 API 请求配额。
    • 使用

      responseObserver.onError(Status.RESOURCE_EXHAUSTED.withDescription("Resource exhausted, quota exceeded").asRuntimeException());
  10. FAILED_PRECONDITION (Status.Code.FAILED_PRECONDITION)

    • 场景:操作的前置条件未满足。
    • 示例:在某个特定状态下操作不被允许。
    • 使用

      responseObserver.onError(Status.FAILED_PRECONDITION.withDescription("Failed precondition").asRuntimeException());
  11. ABORTED (Status.Code.ABORTED)

    • 场景:操作中止,通常由于并发问题。
    • 示例:由于版本冲突导致事务中止。
    • 使用

      responseObserver.onError(Status.ABORTED.withDescription("Operation aborted due to conflict").asRuntimeException());
  12. OUT_OF_RANGE (Status.Code.OUT_OF_RANGE)

    • 场景:操作尝试的范围超出有效范围。
    • 示例:请求的分页索引超出有效范围。
    • 使用

      responseObserver.onError(Status.OUT_OF_RANGE.withDescription("Requested index is out of range").asRuntimeException());
  13. UNIMPLEMENTED (Status.Code.UNIMPLEMENTED)

    • 场景:未实现或不支持的操作。
    • 示例:客户端调用了服务端未实现的 API 方法。
    • 使用

      responseObserver.onError(Status.UNIMPLEMENTED.withDescription("Method not implemented").asRuntimeException());
  14. INTERNAL (Status.Code.INTERNAL)

    • 场景:内部错误,通常是服务端的问题。
    • 示例:服务端的代码逻辑错误导致请求失败。
    • 使用

      responseObserver.onError(Status.INTERNAL.withDescription("Internal server error").asRuntimeException());
  15. UNAVAILABLE (Status.Code.UNAVAILABLE)

    • 场景:服务不可用,通常是由于临时故障或过载。
    • 示例:服务停机或网络故障。
    • 使用

      responseObserver.onError(Status.UNAVAILABLE.withDescription("Service is currently unavailable").asRuntimeException());
  16. DATA_LOSS (Status.Code.DATA_LOSS)

    • 场景:数据丢失或损坏。
    • 示例:由于存储问题导致数据丢失。
    • 使用

      responseObserver.onError(Status.DATA_LOSS.withDescription("Data loss occurred").asRuntimeException());
  17. UNAUTHENTICATED (Status.Code.UNAUTHENTICATED)

    • 场景:请求未通过认证。
    • 示例:客户端未提供有效的身份验证凭据。
    • 使用

      responseObserver.onError(Status.UNAUTHENTICATED.withDescription("Request not authenticated").asRuntimeException());

4.2. 开发示例

1. 定义服务(Proto 文件)

首先,我们定义一个简单的 gRPC 服务和消息:

// user.proto

syntax = "proto3";

package example;

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  string user_id = 1;
}

message GetUserResponse {
  string user_id = 1;
  string name = 2;
  string email = 3;
}

编译这个 proto 文件以生成 Java 代码。

2. 服务端实现

在服务端实现中,我们处理请求并使用 gRPC 状态码来应对不同的情况:

// UserServiceImpl.java

import io.grpc.Status;
import io.grpc.stub.StreamObserver;

public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {

    @Override
    public void getUser(GetUserRequest request, StreamObserver<GetUserResponse> responseObserver) {
        String userId = request.getUserId();

        // 检查无效参数
        if (userId == null || userId.isEmpty()) {
            responseObserver.onError(Status.INVALID_ARGUMENT
                    .withDescription("User ID cannot be empty")
                    .asRuntimeException());
            return;
        }

        // 模拟数据库查找
        if ("123".equals(userId)) {
            GetUserResponse response = GetUserResponse.newBuilder()
                    .setUserId(userId)
                    .setName("John Doe")
                    .setEmail("john.doe@example.com")
                    .build();
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        } else {
            // 模拟资源未找到
            responseObserver.onError(Status.NOT_FOUND
                    .withDescription("User not found")
                    .asRuntimeException());
        }
    }
}
3. 启动服务器
// UserServer.java

import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.io.IOException;

public class UserServer {

    private final int port;
    private final Server server;

    public UserServer(int port) {
        this.port = port;
        this.server = ServerBuilder.forPort(port)
                .addService(new UserServiceImpl())
                .build();
    }

    public void start() throws IOException {
        server.start();
        System.out.println("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            UserServer.this.stop();
            System.err.println("*** server shut down");
        }));
    }

    public void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    public void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        UserServer server = new UserServer(50051);
        server.start();
        server.blockUntilShutdown();
    }
}
4. 客户端实现

客户端负责调用服务端的方法并处理返回的状态码:

// UserClient.java

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

public class UserClient {

    private final UserServiceGrpc.UserServiceBlockingStub blockingStub;

    public UserClient(String host, int port) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // 不使用 SSL/TLS
                .build();
        blockingStub = UserServiceGrpc.newBlockingStub(channel);
    }

    public void getUser(String userId) {
        GetUserRequest request = GetUserRequest.newBuilder().setUserId(userId).build();
        try {
            GetUserResponse response = blockingStub.getUser(request);
            System.out.println("User found: " + response.getName() + " (" + response.getEmail() + ")");
        } catch (StatusRuntimeException e) {
            System.err.println("RPC failed: " + e.getStatus());
        }
    }

    public static void main(String[] args) {
        UserClient client = new UserClient("localhost", 50051);
        client.getUser("123"); // 应该成功
        client.getUser(""); // 应该返回 INVALID_ARGUMENT
        client.getUser("999"); // 应该返回 NOT_FOUND
    }
}
5. 运行示例
  1. 编译 proto 文件:使用 protoc 编译 user.proto 以生成 Java 代码。
  2. 运行服务器:启动 UserServer
  3. 运行客户端:启动 UserClient,观察不同的请求如何返回不同的状态码。

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论