RPC框架(三)客户端篇

客户端

客户端的调用逻辑:

客户端: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 可能会包括以下功能:

  1. 建立与远程服务端的连接:通过 Netty 的网络通信能力,与远程服务端建立连接,以便进行后续的通信。
  2. 封装和发送 RPC 请求:将客户端发起的 RPC 请求封装成网络消息,并通过建立的连接发送给远程服务端。
  3. 接收和处理 RPC 响应:从建立的连接中接收远程服务端的响应消息,并进行解析和处理,将结果返回给调用方。
  4. 管理连接的生命周期:包括连接的建立、维护和释放等操作,确保连接的可靠性和高效性。

代码如下:

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()));
    }
}

致谢:

代码取自:

GitHub - Snailclimb/guide-rpc-framework: A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。)

本文从个人学习感悟出发,理清从客户端端开始,一步步到RPC框架的所有脉络。希望与大家广泛交流。
 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值