gRPC是一个现代化的开源高性能远程过程调用 (RPC) 框架可在任何环境下运行。它能够高效地连接数据中心内部及跨数据中心的服务并提供可插拔的负载均衡、追踪、健康检查和身份验证支持。此外它也适用于分布式计算的“最后一公里”用于连接设备、移动应用和浏览器与后端服务。gRPC优劣势特性 gRPC Dubbo OpenFeign协议 HTTP/2 Protobuf二进制 自定义协议Dubbo、Triple 等 HTTP/1.1 JSON性能 高低延迟、带宽小 高RPC协议 较低基于HTTP JSON序列化 Protobuf高效 Hessian / JSON / Protobuf JSON可读但慢跨语言性 ✅ 强多语言官方支持 ⚠️ 一般Dubbo3之前以Java为主 ✅ 强HTTP天然支持服务治理 ⚠️ 依赖外部组件Envoy / Istio ✅ 内置注册、发现、限流、熔断 ⚠️ 依赖 Spring Cloud Netflix / Alibaba使用复杂度 中等需 .proto 代码生成 中等需注册中心、配置 简单基于HTTP注解流式通信 ✅ 支持双向流 ⚠️ 取决于协议 ❌ 不支持调试可读性 ❌ 较差Binary ❌ 较差Binary ✅ 优秀HTTPJSON生态/集成 CNCF、K8s、Envoy等 阿里生态、Spring生态 Spring Cloud 原生典型场景 高性能微服务、跨语言调用、AI/IoT Java企业级微服务系统 Web服务间调用、BFF层、API聚合Protobuf学习之前我们要了解什么是ProtobufProtobuf介绍Protobuf官方文档地址Protocol Buffers 是一种语言无关、平台无关的可扩展机制用于序列化结构化数据。它类似于 JSON但体积更小、速度更快并且能够生成原生语言绑定。您只需定义一次数据结构即可使用专门生成的源代码轻松地从各种数据流中读取和写入结构化数据并支持多种语言。协议缓冲区是由定义语言在 .proto文件中创建、proto 编译器生成的用于与数据交互的代码、特定于语言的运行时库、写入文件或通过网络连接发送的数据的序列化格式以及序列化数据组合而成的。Protobuf举例hello.protosyntax “proto3”; // 声明使用 proto3 语法option java_package “org.laokou.proto”; // 生成Java代码的包名option java_outer_classname “HelloWorldProto”; // 生成Java代码的类名service Simple { // 服务名rpc SayHello (HelloRequest) returns (HelloReply) { // 方法名(请求参数) 返回参数}}message HelloRequest { // 请求参数string name 1; // 字段类型 字段名 唯一编号}message HelloReply { // 返回参数string message 1; // 字段类型 字段名 唯一编号}更多的写法参考Protobuf官方文档即可这里不做过多赘述开源项目推荐grpc-java 是gRPC官方基于Java实现的gRPC协议无需了解gRPC底层的具体实现细节可以直接通过API创建gRPC服务端和gRPC客户端spring-grpc 项目旨在简化gRPC应用程序的开发项目实践Spring-Cloud负载均衡代码贡献实现思路众所周知微服务项目的服务节点会出现节点宕机或节点扩容服务节点是动态变化我们需要基于Spring Cloud的负载均衡来进行设计还需要考虑到网络因素导致服务不可用或调用时间过长导致服务雪崩因此需要进行重试以及服务熔断降级。通过 discovery://service-name 标识 Spring Cloud gRPC 客户端代码实现依赖org.springframework.bootspring-boot-starter-grpc-client具体代码DiscoveryGrpcChannelFactory// 创建spring cloud grpc channel factoryfinal class DiscoveryGrpcChannelFactory extends NettyGrpcChannelFactory {/** * 构建通道工厂实例. * param globalCustomizers 要应用于所有已创建频道的全局自定义设置 * param interceptorsConfigurer 配置已创建通道上的客户端拦截器 */ public DiscoveryGrpcChannelFactory( ListGrpcChannelBuilderCustomizerNonNull NettyChannelBuilder globalCustomizers, ClientInterceptorsConfigurer interceptorsConfigurer) { super(globalCustomizers, interceptorsConfigurer); setVirtualTargets((p) - p.substring(12)); } Override public boolean supports(String target) { return target.startsWith(discovery:); } NonNull Override protected NettyChannelBuilder newChannelBuilder(NonNull String target, NonNull ChannelCredentials credentials) { return NettyChannelBuilder.forTarget(String.format(discovery://%s, target), credentials); }}DiscoveryNameResolverProviderNameResolverProvider 是 gRPC 中 负责服务名解析Name Resolution 的扩展点Slf4jfinal class DiscoveryNameResolverProvider extends NameResolverProvider {private final DiscoveryClient discoveryClient; private final SetDiscoveryNameResolver discoveryNameResolvers; public DiscoveryNameResolverProvider(DiscoveryClient discoveryClient) { this.discoveryClient discoveryClient; this.discoveryNameResolvers new HashSet(512); } Override protected boolean isAvailable() { return true; } Override protected int priority() { return 6; } Override public NameResolver newNameResolver(URI uri, NameResolver.Args args) { DiscoveryNameResolver discoveryNameResolver new DiscoveryNameResolver(uri.getHost(), discoveryClient); discoveryNameResolvers.add(discoveryNameResolver); return discoveryNameResolver; } Override public String getDefaultScheme() { return discovery; }// 监听nacos心跳详情见如下配置// 开启心跳检测// spring.cloud.nacos.discovery.heart-beat.enabledtrue// 心跳间隔毫秒// spring.cloud.nacos.discovery.heart-beat-interval30000EventListener(HeartbeatEvent.class)public void onHeartbeatEvent(HeartbeatEvent event) {log.debug(“Received HeartbeatEvent, refreshing DiscoveryNameResolvers, event: {}”, event.getValue());for (DiscoveryNameResolver discoveryNameResolver : discoveryNameResolvers) {discoveryNameResolver.refreshFromExternal();}}}DiscoveryNameResolvergRPC Server 通过注册 grpc_port 元数据到 Nacos 因此gRPC Client可以获取具体的gRPC Portfinal class DiscoveryNameResolver extends NameResolver {private final String serviceId; private final DiscoveryClient discoveryClient; private final ListServiceInstance serviceInstances; private final AtomicBoolean resolving; private Listener2 listener; public DiscoveryNameResolver(String serviceId, DiscoveryClient discoveryClient) { this.serviceId serviceId; this.discoveryClient discoveryClient; this.serviceInstances new CopyOnWriteArrayList(); this.resolving new AtomicBoolean(false); } Override public String getServiceAuthority() { return serviceId; } Override public void shutdown() { this.serviceInstances.clear(); } Override public void start(Listener2 listener) { this.listener listener; resolve(); } Override public void refresh() { resolve(); } public void refreshFromExternal() { Thread.startVirtualThread(() - { if (ObjectUtils.isNotNull(listener)) { resolve(); } }); } private void resolve() { if (this.resolving.compareAndSet(false, true)) { Thread.startVirtualThread(() - { this.serviceInstances.addAll(resolveInternal()); this.resolving.set(false); }); } } private ListServiceInstance resolveInternal() { ListServiceInstance newServiceInstanceList this.discoveryClient.getInstances(this.serviceId); if (CollectionExtUtils.isEmpty(newServiceInstanceList)) { listener.onError(Status.UNAVAILABLE.withDescription(No servers found for serviceId)); return Collections.emptyList(); } if (!isUpdateServiceInstance(serviceInstances, newServiceInstanceList)) { return serviceInstances; } this.listener.onResult(ResolutionResult.newBuilder() .setAddressesOrError(StatusOr.fromValue(toAddresses(newServiceInstanceList))) .build()); return newServiceInstanceList; } private boolean isUpdateServiceInstance(ListServiceInstance serviceInstances, ListServiceInstance newServiceInstanceList) { if (serviceInstances.size() ! newServiceInstanceList.size()) { return true; } SetString oldSet serviceInstances.stream().map(this::getAddressStr).collect(Collectors.toSet()); SetString newSet newServiceInstanceList.stream().map(this::getAddressStr).collect(Collectors.toSet()); return !ObjectUtils.equals(oldSet, newSet); } private ListEquivalentAddressGroup toAddresses(ListServiceInstance newServiceInstanceList) { ListEquivalentAddressGroup addresses new ArrayList(newServiceInstanceList.size()); for (ServiceInstance serviceInstance : newServiceInstanceList) { addresses.add(toAddress(serviceInstance)); } return addresses; } private EquivalentAddressGroup toAddress(ServiceInstance serviceInstance) { String host serviceInstance.getHost(); int port getGrpcPost(serviceInstance); return new EquivalentAddressGroup(new InetSocketAddress(host, port), getAttributes(serviceInstance)); } private Attributes getAttributes(ServiceInstance serviceInstance) { return Attributes.newBuilder() .set(Attributes.Key.create(serviceId), serviceId) .set(Attributes.Key.create(instanceId), serviceInstance.getInstanceId()) .build(); } private String getAddressStr(ServiceInstance serviceInstance) { return serviceInstance.getHost() : getGrpcPost(serviceInstance); } private int getGrpcPost(ServiceInstance serviceInstance) { MapString, String metadata serviceInstance.getMetadata(); if (ObjectUtils.isNotNull(metadata) !metadata.isEmpty()) { return Integer.parseInt(metadata.getOrDefault(grpc_port, 9090)); } return 9090; }}DiscoveryNameResolverRegister注册服务名称解析record DiscoveryNameResolverRegister(DiscoveryNameResolverProvider discoveryNameResolverProvider) implements InitializingBean, DisposableBean {Override public void afterPropertiesSet() { NameResolverRegistry.getDefaultRegistry().register(discoveryNameResolverProvider); } Override public void destroy() { NameResolverRegistry.getDefaultRegistry().deregister(discoveryNameResolverProvider); }}gRPC注解InheritedDocumentedRetention(RetentionPolicy.RUNTIME)Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })public interface GrpcClient {String serviceId();}GrpcClientBeanPostProcessor它实现了类似 Spring Autowired 的功能不过注入的不是普通 Bean而是 gRPC Client Stub代理对象public record GrpcClientBeanPostProcessor(GrpcClientFactory grpcClientFactory) implements BeanPostProcessor {Override public Object postProcessBeforeInitialization(NonNull Object bean, NonNull String beanName) throws BeansException { Class? clazz bean.getClass(); do { setFields(bean, clazz); setMethods(bean, clazz); clazz clazz.getSuperclass(); } while (ObjectUtils.isNotNull(clazz)); return bean; } private void setFields(Object bean, Class? clazz) { for (Field field : clazz.getDeclaredFields()) { GrpcClient grpcClient AnnotationUtils.findAnnotation(field, GrpcClient.class); if (ObjectUtils.isNotNull(grpcClient)) { ReflectionUtils.makeAccessible(field); ReflectionUtils.setField(field, bean, getClient(field.getType(), grpcClient)); } } } private void setMethods(Object bean, Class? clazz) { for (Method method : clazz.getDeclaredMethods()) { GrpcClient grpcClient AnnotationUtils.findAnnotation(method, GrpcClient.class); if (ObjectUtils.isNotNull(grpcClient)) { ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, bean, getClient(method.getParameterTypes()[0], grpcClient)); } } } private T T getClient(ClassT type, NonNull GrpcClient grpcClient) { String target String.format(discovery://%s, grpcClient.serviceId()); return grpcClientFactory.getClient(target, type, null); }}GrpcClientConfigImportGrpcClientsConfiguration(proxyBeanMethods false)ConditionalOnClass(DiscoveryClient.class)final class GrpcClientConfig {Bean DiscoveryNameResolverProvider discoveryNameResolverProvider(DiscoveryClient discoveryClient) { return new DiscoveryNameResolverProvider(discoveryClient); } Bean DiscoveryNameResolverRegister discoveryNameResolverRegister( DiscoveryNameResolverProvider discoveryNameResolverProvider) { return new DiscoveryNameResolverRegister(discoveryNameResolverProvider); } Bean GrpcClientBeanPostProcessor grpcClientBeanPostProcessor(GrpcClientFactory grpcClientFactory) { return new GrpcClientBeanPostProcessor(grpcClientFactory); } Bean DiscoveryGrpcChannelFactory discoveryGrpcChannelFactory( ListGrpcChannelBuilderCustomizerNonNull NettyChannelBuilder globalCustomizers, ClientInterceptorsConfigurer interceptorsConfigurer) { return new DiscoveryGrpcChannelFactory(globalCustomizers, interceptorsConfigurer); }}测试 gRPC依赖org.springframework.bootspring-boot-starter-grpc-servertesttrueorg.springframework.bootspring-boot-grpc-testtesttrueorg.springframework.cloudspring-cloud-starter-consul-discoverytesttrueorg.testcontainerstestcontainers-consultesttrueorg.springframework.bootspring-boot-starter-tomcattesttrue具体代码hello.protosyntax “proto3”;option java_package “org.laokou.grpc.proto”;option java_outer_classname “HelloWorldProto”;service Simple {rpc SayHello (HelloRequest) returns (HelloReply) {}}message HelloRequest {string name 1;}message HelloReply {string message 1;}GrpcClientTestTestcontainersImport(GrpcClientTest.MockOAuth2Config.class)SpringBootTest(webEnvironment SpringBootTest.WebEnvironment.RANDOM_PORT, classes GrpcClientTest.GrpcTest.class)class GrpcClientTest {Container static ConsulContainer consul new ConsulContainer(DockerImageNames.consul()); DynamicPropertySource static void consulProperties(DynamicPropertyRegistry registry) { registry.add(spring.cloud.consul.host, consul::getHost); registry.add(spring.cloud.consul.port, consul::getFirstMappedPort); registry.add(spring.cloud.consul.discovery.service-name, () - laokou-grpc); registry.add(spring.cloud.consul.discovery.instance-id, () - laokou-grpc-1); registry.add(spring.cloud.consul.discovery.metadata.grpc_port, () - 9097); registry.add(spring.cloud.consul.discovery.prefer-ip-address, () - true); } GrpcClient(serviceId laokou-grpc) private SimpleGrpc.SimpleBlockingV2Stub simpleBlockingV2Stub; Test void test() throws StatusException { HelloWorldProto.HelloRequest request HelloWorldProto.HelloRequest.newBuilder().setName(test).build(); HelloWorldProto.HelloReply helloReply simpleBlockingV2Stub.sayHello(request); Assertions.assertThat(helloReply).isNotNull(); Assertions.assertThat(helloReply.getMessage()).isEqualTo(Hello test); } EnableDiscoveryClient SpringBootApplication(scanBasePackages { org.laokou }) static class GrpcTest { } TestConfiguration static class MockOAuth2Config {}}application.yamlserver:port: 8979spring:application:name: laokou-grpcgrpc:server:servlet:enabled: falseport: 9097enabled: trueclient:channel:laokou-grpc:target: discovery://laokou-grpckeepalive:time: 10stimeout: 30sidle:timeout: 30s