客户端
客户端的调用逻辑:
客户端:NettyClientMain.java ---> 客户端:HelloController.java ---> RPC框架:RpcReference --->RPC框架:SpringBeanPostProcessor.java ---> RPC框架:RpcClientProxy.java ---> RPC框架:NettyRpcClient.java ---> RPC框架:NettyRpcClientHandler.java、ServiceDiscovery.java --->RPC框架:RandomLoadBalance.java
客户端的整体脉络比较简单,所以就全部写出来了。可以看到与服务端相比,它的嵌套来嵌套去少了很多。比较偏向于线性调用,一直往下调用。
client:
先来看看客户端的目录结构:
NettyClientMain:
package com.cjd;
import com.cjd.anno.RpcScan;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@RpcScan(basePackage = {"com.cjd"})
public class NettyClientMain {
public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(NettyClientMain.class);
// 通过注解获取到bean的集合
HelloController helloController = (HelloController) applicationContext.getBean("helloController");
helloController.test();
}
}
这个代码中,出现了和服务端一样的结构@RpcScan,它主要用于标识要扫描的包路径,以便在使用 RPC 框架时自动扫描并注册相应的服务接口和实现类。
HelloController:
接着到了HelloController。
package com.cjd;
import com.cjd.anno.RpcReference;
import org.springframework.stereotype.Component;
@Component
public class HelloController {
@RpcReference(version = "version1", group = "test1")
private HelloService helloService;
public void test() throws InterruptedException {
String hello = this.helloService.hello(new Hello("111", "222"));
//如需使用 assert 断言,需要在 VM options 添加参数:-ea
assert "Hello description is 222".equals(hello);
Thread.sleep(12000);
for (int i = 0; i < 10; i++) {
System.out.println(helloService.hello(new Hello("111", "222")));
}
}
}
可以看到,在客户端他直接通过本地调用了HelloService的方法。我们需要在porn.xml导入依赖
<dependency> <groupId>com.cjd</groupId> <artifactId>helloService-api</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency>
RPC框架:
先来看看整体目录结构:
与客户端有关的主要是anno、proxy、registry、remoting、spring部分。
anno目录:
RpcReference:
先来看看anno部分。是RpcReference.java的代码。
@RpcReference
注解通常用于标识一个字段或者属性作为 RPC 客户端的引用。当一个字段或属性被标记为 @RpcReference
时,RPC 框架会自动注入对应的远程服务的代理对象,使得客户端可以通过代理对象调用远程服务。
代码如下:
package com.cjd.anno;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时可以获取上
@Target({ElementType.FIELD}) //用于类上
@Inherited
public @interface RpcReference {
String version() default "";
String group() default "";
}
只有两个参数,分别是想要调用的版本号和组数。
spring目录
SpringBeanPostProcessor:
在服务端篇的时候,SpringBeanPostProcessor有一部分代码没有写,这部代码就是客户端需要的部分,在这个地方展示。
代码如下:
package com.cjd.spring;
import com.cjd.anno.RpcReference;
import com.cjd.anno.RpcService;
import com.cjd.config.RpcServiceConfig;
import com.cjd.enums.RpcRequestTransportEnum;
import com.cjd.extension.ExtensionLoader;
import com.cjd.factory.SingletonFactory;
import com.cjd.provider.ServiceProvider;
import com.cjd.provider.impl.ZkServiceProviderImpl;
import com.cjd.proxy.RpcClientProxy;
import com.cjd.remoting.transport.RpcRequestTransport;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
@Component
@Slf4j
public class SpringBeanPostProcessor implements BeanPostProcessor {
private final ServiceProvider serviceProvider;
private final RpcRequestTransport rpcClient;
public SpringBeanPostProcessor() {
this.serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);
this.rpcClient = ExtensionLoader.getExtensionLoader(RpcRequestTransport.class).getExtension(RpcRequestTransportEnum.NETTY.getName());
}
/**
* 这个方法在目标 Bean 初始化之前被调用。在这个方法中,首先判断目标 Bean 是否被 @RpcService 注解标记,
* 如果是,则获取该注解信息,构建 RpcServiceConfig,并通过 ServiceProvider 发布该服务。
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@SneakyThrows
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 服务端的部分已经展示过了
}
/**
* 这个方法在目标 Bean 初始化之后被调用。
* 在这个方法中,首先获取目标 Bean 的类信息和声明的字段,然后遍历所有字段,检查是否带有 @RpcReference 注解。
* 如果某个字段带有 @RpcReference 注解,则构建 RpcServiceConfig,并创建 RpcClientProxy,
* 通过 RpcClientProxy 获取代理对象,最后将代理对象设置到字段中。
* @param bean
* @param beanName
* @return
* @throws BeansException
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 获取目标 Bean 的类信息。
Class<?> targetClass = bean.getClass();
// 获取目标 Bean 类中声明的所有字段。
Field[] declaredFields = targetClass.getDeclaredFields();
// 遍历所有字段,检查是否带有 @RpcReference 注解。
for (Field declaredField : declaredFields) {
// 如果某个字段带有 @RpcReference 注解,就创建一个 RpcServiceConfig 对象,设置引用的服务组和版本信息。
RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class);
if (rpcReference != null) {
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
.group(rpcReference.group())
.version(rpcReference.version()).build();
// 创建 RpcClientProxy 对象,使用该对象获取代理对象。
RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceConfig);
Object clientProxy = rpcClientProxy.getProxy(declaredField.getType());
declaredField.setAccessible(true);
try {
// 将获取的代理对象设置到目标 Bean 的对应字段中。
declaredField.set(bean, clientProxy);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return bean;
}
}
里面出现了一个新的变量名,那就是RpcClientProxy,客户端的代理类。
proxy目录:
RpcClientProxy:
它使用了 Java 的动态代理机制来生成代理对象,以便在客户端调用远程服务时进行封装和处理。
代码如下:
package com.cjd.proxy;
import com.cjd.config.RpcServiceConfig;
import com.cjd.enums.RpcErrorMessageEnum;
import com.cjd.enums.RpcResponseCodeEnum;
import com.cjd.exception.RpcException;
import com.cjd.remoting.dto.RpcRequest;
import com.cjd.remoting.dto.RpcResponse;
import com.cjd.remoting.transport.RpcRequestTransport;
import com.cjd.remoting.transport.client.NettyRpcClient;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
// 实现了 InvocationHandler 接口,这是 Java 动态代理的核心接口,它定义了代理对象的方法调用处理逻辑
public class RpcClientProxy implements InvocationHandler {
private static final String INTERFACE_NAME = "interfaceName";
private final RpcRequestTransport rpcRequestTransport;
private final RpcServiceConfig rpcServiceConfig;
public RpcClientProxy(RpcRequestTransport rpcRequestTransport, RpcServiceConfig rpcServiceConfig) {
this.rpcRequestTransport = rpcRequestTransport;
this.rpcServiceConfig = rpcServiceConfig;
}
public RpcClientProxy(RpcRequestTransport rpcRequestTransport) {
this.rpcRequestTransport = rpcRequestTransport;
this.rpcServiceConfig = new RpcServiceConfig();
}
public <T> T getProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, this);
}
/**
* RpcClientProxy 类可以用于拦截方法调用、将方法名、参数以及其他必要信息封装成一个 RPC 请求、
* 将该请求发送到远程服务器、接收响应,并在返回结果给调用方之前处理该响应。
* @param proxy
* @param method
* @param args
* @return
*/
@SneakyThrows
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
log.info("invoked method: [{}]", method.getName());
RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName())
.parameters(args)
.interfaceName(method.getDeclaringClass().getName())
.paramTypes(method.getParameterTypes())
.requestId(UUID.randomUUID().toString())
.group(rpcServiceConfig.getGroup())
.version(rpcServiceConfig.getVersion())
.build();
RpcResponse<Object> rpcResponse = null;
// 判断是不是采用 netty连接
if (rpcRequestTransport instanceof NettyRpcClient) {
CompletableFuture<RpcResponse<Object>> completableFuture = (CompletableFuture<RpcResponse<Object>>) rpcRequestTransport.sendRpcRequest(rpcRequest);
rpcResponse = completableFuture.get();
}
// 判断是不是采用 socket连接
// if (rpcRequestTransport instanceof SocketRpcClient) {
// rpcResponse = (RpcResponse<Object>) rpcRequestTransport.sendRpcRequest(rpcRequest);
// }
this.check(rpcResponse, rpcRequest);
return rpcResponse.getData();
}
private void check(RpcResponse<Object> rpcResponse, RpcRequest rpcRequest) {
// 首先,它检查 rpcResponse 是否为 null,如果是,则抛出一个 RpcException 异常,表示服务调用失败
if (rpcResponse == null) {
throw new RpcException(RpcErrorMessageEnum.SERVICE_INVOCATION_FAILURE, INTERFACE_NAME + ":" + rpcRequest.getInterfaceName());
}
// 接下来,它比较请求的 requestId 和响应的 requestId 是否相等,如果不相等,则抛出一个 RpcException 异常,表示请求和响应不匹配。
if (!rpcRequest.getRequestId().equals(rpcResponse.getRequestId())) {
throw new RpcException(RpcErrorMessageEnum.REQUEST_NOT_MATCH_RESPONSE, INTERFACE_NAME + ":" + rpcRequest.getInterfaceName());
}
// 最后,它检查 rpcResponse 中的 code 是否为成功的代码,如果不是,则同样抛出一个 RpcException 异常,表示服务调用失败。
if (rpcResponse.getCode() == null || !rpcResponse.getCode().equals(RpcResponseCodeEnum.SUCCESS.getCode())) {
throw new RpcException(RpcErrorMessageEnum.SERVICE_INVOCATION_FAILURE, INTERFACE_NAME + ":" + rpcRequest.getInterfaceName());
}
}
}
里面出现了一个新的变量,那就是NettyRpcClient。
remoting目录:
目录结构如下:
在transport目录里client的部分出现了NettyRpcClient的代码。
NettyRpcClient:
主要作用是实现客户端与服务端之间的通信,包括建立连接、发送请求、接收响应等功能。
具体来说,NettyRpcClient
可能会包括以下功能:
- 建立与远程服务端的连接:通过 Netty 的网络通信能力,与远程服务端建立连接,以便进行后续的通信。
- 封装和发送 RPC 请求:将客户端发起的 RPC 请求封装成网络消息,并通过建立的连接发送给远程服务端。
- 接收和处理 RPC 响应:从建立的连接中接收远程服务端的响应消息,并进行解析和处理,将结果返回给调用方。
- 管理连接的生命周期:包括连接的建立、维护和释放等操作,确保连接的可靠性和高效性。
代码如下:
package com.cjd.remoting.transport.client;
import com.cjd.enums.CompressTypeEnum;
import com.cjd.enums.SerializationTypeEnum;
import com.cjd.enums.ServiceDiscoveryEnum;
import com.cjd.extension.ExtensionLoader;
import com.cjd.factory.SingletonFactory;
import com.cjd.registry.ServiceDiscovery;
import com.cjd.remoting.constants.RpcConstants;
import com.cjd.remoting.dto.RpcMessage;
import com.cjd.remoting.dto.RpcRequest;
import com.cjd.remoting.dto.RpcResponse;
import com.cjd.remoting.transport.RpcRequestTransport;
import com.cjd.remoting.transport.codec.RpcMessageDecoder;
import com.cjd.remoting.transport.codec.RpcMessageEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@Slf4j
public class NettyRpcClient implements RpcRequestTransport {
private final ServiceDiscovery serviceDiscovery;
private final UnprocessedRequests unprocessedRequests;
private final ChannelProvider channelProvider;
private final Bootstrap bootstrap;
private final EventLoopGroup eventLoopGroup;
public NettyRpcClient() {
// 1、创建线程组
eventLoopGroup = new NioEventLoopGroup();
// 2、创建客户端启动助手
bootstrap = new Bootstrap();
// 3、设置线程组
bootstrap.group(eventLoopGroup)
// 4、设置客户端通道实现NIO
.channel(NioSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
// The timeout period of the connection.
// If this time is exceeded or the connection cannot be established, the connection fails.
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
// 5、创建一个通道初始化对象
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// If no data is sent to the server within 15 seconds, a heartbeat request is sent
//心跳机制
p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
// 添加两个边解码器
p.addLast(new RpcMessageEncoder());
p.addLast(new RpcMessageDecoder());
// 客户端自定义处理类
p.addLast(new NettyRpcClientHandler());
}
});
this.serviceDiscovery = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class).getExtension(ServiceDiscoveryEnum.ZK.getName());
this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class);
this.channelProvider = SingletonFactory.getInstance(ChannelProvider.class);
}
/**
* connect server and get the channel ,so that you can send rpc message to server
*
* @param inetSocketAddress server address
* @return the channel
*/
@SneakyThrows
public Channel doConnect(InetSocketAddress inetSocketAddress) {
// 创建了一个 CompletableFuture<Channel> 对象,用于表示异步操作的结果,这里期望的结果是一个 Channel 对象,代表了客户端与服务端的连接通道。
CompletableFuture<Channel> completableFuture = new CompletableFuture<>();
// 7、启动客户端,等待连接服务端
// 注册了一个监听器 (ChannelFutureListener),在连接操作完成时进行处理。
// 如果连接成功 (future.isSuccess()),则通过 completableFuture.complete(future.channel()) 将异步操作标记为已完成,
// 并设置其结果为 future.channel(),即连接成功后的通道对象。
bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
log.info("The client has connected [{}] successful!", inetSocketAddress.toString());
// CompletableFuture 对象所代表的异步操作已经完成,而且完成时的结果就是 future.channel() 所表示的内容。
completableFuture.complete(future.channel());
} else {
throw new IllegalStateException();
}
});
// 通过 completableFuture.get() 获取异步操作的结果。
// 由于 completableFuture.complete(future.channel()) 已经将异步操作标记为完成,
// 所以 completableFuture.get() 可以立即获取到连接成功后的 Channel 对象,从而返回给调用方。
return completableFuture.get();
}
// 用于在客户端发送 RPC 请求到服务端。
@Override
public Object sendRpcRequest(RpcRequest rpcRequest) {
// 首先创建了一个 CompletableFuture<RpcResponse<Object>> 对象 resultFuture,用于表示异步操作的结果,即表示 RPC 调用的返回结果。
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
//通过 serviceDiscovery.lookupService(rpcRequest) 获取服务端的地址 inetSocketAddress。
InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcRequest);
// 通过 getChannel(inetSocketAddress) 获取与服务端地址相关的通道 channel
Channel channel = getChannel(inetSocketAddress);
if (channel.isActive()) {
// 将未处理的请求放入 unprocessedRequests 中,以便后续处理。
unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture);
// 构建了 RpcMessage 对象,包括 RPC 请求的数据、编解码方式、压缩方式等信息。
RpcMessage rpcMessage = RpcMessage.builder().data(rpcRequest)
.codec(SerializationTypeEnum.KRYO.getCode())
.compress(CompressTypeEnum.GZIP.getCode())
.messageType(RpcConstants.REQUEST_TYPE).build();
// 通过 channel.writeAndFlush(rpcMessage) 将 RPC 消息写入通道并发送到服务端。同时注册了一个监听器 (ChannelFutureListener),在发送完成后进行处理。
channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
// 如果成功,则记录日志,表示消息发送成功
log.info("client send message: [{}]", rpcMessage);
} else {
// 发送操作失败,通过 future.channel().close() 关闭通道,表示发送失败后进行清理操作。
future.channel().close();
// 然后通过 resultFuture.completeExceptionally(future.cause()) 将发送失败的异常传播到 resultFuture 中,表示发送失败。
resultFuture.completeExceptionally(future.cause());
log.error("Send failed:", future.cause());
}
});
} else {
throw new IllegalStateException();
}
return resultFuture;
}
public Channel getChannel(InetSocketAddress inetSocketAddress) {
Channel channel = channelProvider.get(inetSocketAddress);
if (channel == null) {
channel = doConnect(inetSocketAddress);
channelProvider.set(inetSocketAddress, channel);
}
return channel;
}
// 8、关闭线程组
public void close() {
eventLoopGroup.shutdownGracefully();
}
}
在这里面就出现了四个新的变量名,分别是ServiceDiscovery、UnprocessedRequests、ChannelProvider和NettyRpcClientHandler。
先讲讲同一目录下的三个文件,UnprocessedRequests、ChannelProvider和NettyRpcClientHandler。
UnprocessedRequests:
这个代码主要的作用就是用来存储客户端发送的暂时还没处理的RPC请求,以便在接收到服务端的响应时能够找到对应的请求并进行后续处理。在NettyRpcClient的部分,把接收到的Rpc请求都先放入到UnprocessedRequests中,给后续NettyRpcClientHandler来进行处理。
package com.cjd.remoting.transport.client;
import com.cjd.remoting.dto.RpcResponse;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class UnprocessedRequests {
// 用于存储未处理的RPC请求及其对应的 CompletableFuture 对象。String 类型的键代表请求的唯一标识,
// CompletableFuture<RpcResponse<Object>> 类型的值代表请求对应的响应的 CompletableFuture 对象。
private static final Map<String, CompletableFuture<RpcResponse<Object>>> UNPROCESSED_RESPONSE_FUTURES = new ConcurrentHashMap<>();
public void put(String requestId, CompletableFuture<RpcResponse<Object>> future) {
UNPROCESSED_RESPONSE_FUTURES.put(requestId, future);
}
/**
* 这个方法用于处理RPC请求的响应。根据响应中的请求唯一标识,从存储中获取对应的 CompletableFuture 对象,
* 并将响应设置到 CompletableFuture 中,完成异步操作。
* 如果找不到对应的 CompletableFuture 对象,会抛出 IllegalStateException 异常。
* @param rpcResponse
*/
public void complete(RpcResponse<Object> rpcResponse) {
// 通过 UNPROCESSED_RESPONSE_FUTURES.remove(rpcResponse.getRequestId())
// 从 UNPROCESSED_RESPONSE_FUTURES 中移除对应 requestId 的 CompletableFuture 对象,
// 并返回被移除的对象。
CompletableFuture<RpcResponse<Object>> future = UNPROCESSED_RESPONSE_FUTURES.remove(rpcResponse.getRequestId());
if (null != future) {
// 如果返回的 CompletableFuture 不为 null,即找到了对应的未处理请求,
// 那么调用 future.complete(rpcResponse) 完成该未处理请求,
// 并将收到的 rpcResponse 作为其结果。
future.complete(rpcResponse);
} else {
throw new IllegalStateException();
}
}
}
ChannelProvider:
ChannelProvider
类的作用是管理与远程地址相关联的Channel
对象,包括获取、存储和移除Channel
对象,并提供了线程安全的ConcurrentHashMap
来存储这些映射关系。
package com.cjd.remoting.transport.client;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ChannelProvider {
// channelMap 是一个 ConcurrentHashMap,用于存储远程地址与Channel对象之间的映射关系。
private final Map<String, Channel> channelMap;
public ChannelProvider() {
channelMap = new ConcurrentHashMap<>();
}
// get 方法用于根据给定的远程地址获取对应的Channel对象。
public Channel get(InetSocketAddress inetSocketAddress) {
// 它首先将远程地址转换为字符串作为 key,然后检查channelMap中是否存在对应的Channel。
String key = inetSocketAddress.toString();
if (channelMap.containsKey(key)) {
Channel channel = channelMap.get(key);
// 如果存在,则检查该Channel是否可用(active),如果可用则直接返回该Channel,
if (channel != null && channel.isActive()) {
return channel;
} else {
// 否则从channelMap中移除并返回null。
channelMap.remove(key);
}
}
return null;
}
// set 方法用于将给定的远程地址和Channel对象建立映射关系,并存储到channelMap中。
public void set(InetSocketAddress inetSocketAddress, Channel channel) {
String key = inetSocketAddress.toString();
channelMap.put(key, channel);
}
// remove 方法用于移除给定远程地址相关的Channel映射,并打印日志记录channelMap的大小。
public void remove(InetSocketAddress inetSocketAddress) {
String key = inetSocketAddress.toString();
channelMap.remove(key);
log.info("Channel map size :[{}]", channelMap.size());
}
}
NettyRpcClientHandler:
作为客户端的通道处理器,负责处理客户端与服务端之间的通信,包括处理收到的消息、发送心跳消息以维持连接以及处理异常情况。
package com.cjd.remoting.transport.client;
import com.cjd.enums.CompressTypeEnum;
import com.cjd.enums.SerializationTypeEnum;
import com.cjd.factory.SingletonFactory;
import com.cjd.remoting.constants.RpcConstants;
import com.cjd.remoting.dto.RpcMessage;
import com.cjd.remoting.dto.RpcResponse;
import io.netty.channel.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.EventExecutorGroup;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
// 它是一个 ChannelInboundHandlerAdapter 的子类,用于处理客户端与服务端之间的通信。
@Slf4j
public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter {
private final UnprocessedRequests unprocessedRequests;
private final NettyRpcClient nettyRpcClient;
public NettyRpcClientHandler() {
this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class);
this.nettyRpcClient = SingletonFactory.getInstance(NettyRpcClient.class);
}
/**
* 通道读取事件--读取服务端发送的消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// 客户端的消息
log.info("client receive msg: [{}]", msg);
// 首先记录收到的消息,然后根据消息类型进行处理。
if (msg instanceof RpcMessage) {
RpcMessage tmp = (RpcMessage) msg;
byte messageType = tmp.getMessageType();
// 如果收到的消息是心跳响应类型,则记录心跳消息
if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
log.info("heart [{}]", tmp.getData());
// 如果收到的消息是响应类型,则将其转换为 RpcResponse<Object> 对象,
// 并通过 unprocessedRequests.complete(rpcResponse) 完成对应的未处理请求。
//
} else if (messageType == RpcConstants.RESPONSE_TYPE) {
RpcResponse<Object> rpcResponse = (RpcResponse<Object>) tmp.getData();
// 当客户端收到服务端的响应时,会调用 unprocessedRequests.complete(rpcResponse),
// 这将会处理这个响应,并将其传播到对应的 CompletableFuture 中,从而完成对应的未处理请求。
unprocessedRequests.complete(rpcResponse);
}
}
} finally {
ReferenceCountUtil.release(msg);
}
}
/**
* 通道连接就绪事件--与服务器建立连接
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleState state = ((IdleStateEvent) evt).state();
if (state == IdleState.WRITER_IDLE) {
// 如果是写空闲状态事件,表示在一定的时间内没有向对方发送消息,这时向对方发送一个心跳消息以维持连接。
log.info("write idle happen [{}]", ctx.channel().remoteAddress());
Channel channel = nettyRpcClient.getChannel((InetSocketAddress) ctx.channel().remoteAddress());
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setCodec(SerializationTypeEnum.KRYO.getCode());
rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());
rpcMessage.setMessageType(RpcConstants.HEARTBEAT_REQUEST_TYPE);
rpcMessage.setData(RpcConstants.PING);
// 发送心跳消息,并在发送失败时关闭通道。
channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
} else {
// 如果不是写空闲状态事件,则委托给父类的默认实现来处理该事件
super.userEventTriggered(ctx, evt);
}
}
/**
* Called when an exception occurs in processing a client message
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("client catch exception:", cause);
cause.printStackTrace();
ctx.close();
}
}
registry目录:
讲完了上面四个代码,在NettyRpcClient文件下还剩下ServiceDiscovery变量没有讲。
首先看看这个目录的结构。
ServiceDiscovery:
这个是一个接口,提供了一个根据请求查找服务返回地址的方法。
package com.cjd.registry;
import com.cjd.extension.SPI;
import com.cjd.remoting.dto.RpcRequest;
import java.net.InetSocketAddress;
@SPI
public interface ServiceDiscovery {
InetSocketAddress lookupService(RpcRequest rpcRequest);
}
ZkServiceDiscoveryImpl:
基于 ZooKeeper 的服务发现实现,它通过负载均衡算法选择远程服务的地址,并返回 InetSocketAddress
对象,从而实现了服务发现的功能。
(ZkServiceDiscoveryImpl
使用了 @SPI
注解,表明它是 ServiceDiscovery
接口的一个可扩展实现,可以通过配置或其他方式来选择不同的实现类。这意味着系统可以有多个不同的 ServiceDiscovery
实现类,而使用 @SPI
注解可以让系统在需要时动态地选择合适的实现类。
而ZkServiceRegistryImpl
没有使用 @SPI
注解,可能是因为在该系统中,服务注册的实现类只有一个,不需要动态切换或者扩展。因此,它没有被设计为可扩展的部分,而是一个固定的实现类。)
package com.cjd.registry.zk.impl;
import com.cjd.enums.RpcErrorMessageEnum;
import com.cjd.exception.RpcException;
import com.cjd.extension.SPI;
import com.cjd.loadbalance.LoadBalance;
import com.cjd.loadbalance.loadbalancer.RandomLoadBalance;
import com.cjd.registry.ServiceDiscovery;
import com.cjd.registry.zk.util.CuratorUtils;
import com.cjd.remoting.dto.RpcRequest;
import com.cjd.utils.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import java.net.InetSocketAddress;
import java.util.List;
@SPI
@Slf4j
public class ZkServiceDiscoveryImpl implements ServiceDiscovery {
private final LoadBalance loadBalance;
public ZkServiceDiscoveryImpl() {
this.loadBalance = new RandomLoadBalance();
}
@Override
public InetSocketAddress lookupService(RpcRequest rpcRequest) {
// 首先通过 rpcRequest 获取需要调用的远程服务名称 rpcServiceName,
String rpcServiceName = rpcRequest.getRpcServiceName();
// 然后使用 CuratorUtils 获取 ZooKeeper 客户端
CuratorFramework zkClient = CuratorUtils.getZkClient();
// 获取指定服务名称下的所有服务地址列表 serviceUrlList。
List<String> serviceUrlList = CuratorUtils.getChildrenNodes(zkClient, rpcServiceName);
if (CollectionUtil.isEmpty(serviceUrlList)) {
throw new RpcException(RpcErrorMessageEnum.SERVICE_CAN_NOT_BE_FOUND, rpcServiceName);
}
// 进行负载均衡,通过 loadBalance.selectServiceAddress(serviceUrlList, rpcRequest) 方法选择一个具体的服务地址
String targetServiceUrl = loadBalance.selectServiceAddress(serviceUrlList, rpcRequest);
System.out.println("Successfully found the service address:[{}]" + targetServiceUrl);
log.info("Successfully found the service address:[{}]", targetServiceUrl);
// 将选定的服务地址解析为 InetSocketAddress 对象
String[] socketAddressArray = targetServiceUrl.split(":");
String host = socketAddressArray[0];
int port = Integer.parseInt(socketAddressArray[1]);
return new InetSocketAddress(host, port);
}
}
在服务端已经讲过CuratorUtils工具类了,在这就不做篇幅继续讲解了。
loadbalance目录:
在ZkServiceDiscoveryImpl文件下出现了一个LoadBalance,这个是为了做负载均衡使用的。
它的目录结构如下:
LoadBalance:
LoadBalance
接口使用了 @SPI
注解,表明它是一个可扩展的接口,允许系统有多个不同的 LoadBalance
实现类,并且可以动态选择合适的实现类。
package com.cjd.loadbalance;
import com.cjd.extension.SPI;
import com.cjd.remoting.dto.RpcRequest;
import java.util.List;
@SPI
public interface LoadBalance {
String selectServiceAddress(List<String> serviceAddresses, RpcRequest rpcRequest);
}
AbstractLoadBalance:
AbstractLoadBalance
类提供了一个通用的服务地址选择逻辑,并要求具体的负载均衡算法实现类来实现 doSelect
方法,从而实现具体的负载均衡策略。这样,其他负载均衡算法的实现类可以继承 AbstractLoadBalance
类,并通过实现 doSelect
方法来实现自己的负载均衡逻辑。
代码如下:
package com.cjd.loadbalance;
import com.cjd.remoting.dto.RpcRequest;
import com.cjd.utils.CollectionUtil;
import java.util.List;
public abstract class AbstractLoadBalance implements LoadBalance {
@Override
public String selectServiceAddress(List<String> serviceAddresses, RpcRequest rpcRequest) {
// serviceAddresses 是否为空,如果为空则返回 null
if (CollectionUtil.isEmpty(serviceAddresses)) {
return null;
}
// 如果只有一个服务地址,则直接返回该服务地址
if (serviceAddresses.size() == 1) {
return serviceAddresses.get(0);
}
// 否则调用抽象方法 doSelect 进行具体的选择逻辑。
return doSelect(serviceAddresses, rpcRequest);
}
// AbstractLoadBalance 类中的 doSelect 方法是一个抽象方法,它需要子类来实现具体的服务地址选择逻辑。
// 由于这个方法是抽象的,子类必须提供具体的实现。
protected abstract String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest);
}
RandomLoadBalance:
这个代码的逻辑相对简单,就是当服务地址不为1时,随机取一个服务地址。
代码如下:
package com.cjd.loadbalance.loadbalancer;
import com.cjd.loadbalance.AbstractLoadBalance;
import com.cjd.remoting.dto.RpcRequest;
import java.util.List;
import java.util.Random;
public class RandomLoadBalance extends AbstractLoadBalance {
@Override
protected String doSelect(List<String> serviceAddresses, RpcRequest rpcRequest) {
Random random = new Random();
return serviceAddresses.get(random.nextInt(serviceAddresses.size()));
}
}
致谢:
代码取自:
本文从个人学习感悟出发,理清从客户端端开始,一步步到RPC框架的所有脉络。希望与大家广泛交流。