guide-rpc-framework 流程梳理

Shie1d發表於2024-08-07

開源專案 https://github.com/Snailclimb/guide-rpc-framework

在學習中記錄下來的大致流程梳理,其中一些細節沒有拉出來講,主要是篇幅太長。

NettyServerMain

透過@Component 註解 拿到nettyRpcServer

NettyRpcServer nettyRpcServer = (NettyRpcServer) applicationContext.getBean("nettyRpcServer");

手動註冊HelloService

HelloService helloService2 = new HelloServiceImpl2();

// 註冊到zookeeper
RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
                .group("test2").version("version2").service(helloService2).build();

public class RpcServiceConfig {
    /**
     * service version
     */
    private String version = "";
    /**
     * when the interface has multiple implementation classes, distinguish by group
     */
    private String group = "";
    /**
     * target service
     */
    private Object service;
    public String getRpcServiceName() {
        return this.getServiceName() + this.getGroup() + this.getVersion();
    }
    // 實現的第一個介面的全限定名
    public String getServiceName() {
        return this.service.getClass().getInterfaces()[0].getCanonicalName();
    }
}
//註冊
nettyRpcServer.registerService(rpcServiceConfig);

// 1 會將 service 放進一個map
serviceMap.put(rpcServiceName, rpcServiceConfig.getService());


// 2 放入zookeeper 會先獲取當前服務提供方的IP地址以及埠(固定9998)組成InetSocketAddress,
new InetSocketAddress(host, NettyRpcServer.PORT)    
   
// 以註冊的HelloService實現的介面名+組名+版本號 + 為 zk 的節點路徑,    
// 如 /my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
public static final String ZK_REGISTER_ROOT_PATH = "/my-rpc";
String servicePath = CuratorUtils.ZK_REGISTER_ROOT_PATH + "/" + rpcServiceName + inetSocketAddress.toString();


//拿到 servicePath  就會以此為path建立節點
zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).forPath(path);

//zk有四種節點,我們建立的忍耐節點
持久(persistent)節點:
(1) session斷開後,資料不會丟失
(2)可以建立子節點
臨時(ephemeral)節點:
(1)session斷開後,資料會丟失
(2)不可以建立子節點    
持久順序(PERSISTENT_SEQUENTIAL)節點:同持久節點
建立順序節點時會預設設定順序標識,即znode名稱後會附加一個 順序號 ,順序號是一個單調遞增的計數器,由父節點維護
臨時順序(EPHEMERAL_SEQUENTIAL)節點: —> ZK實現分散式鎖的基礎。
(1)、(2)同臨時節點,(3)同持久順序節點      臨時是為了防止一個鎖無法被刪除,順序:只要自己的時順序是最小的說明拿到了鎖。
// zk 的三種監聽
NodeCache:只是監聽某一個特定的節點
PathChildrenCache:監控一個ZNode的子節點.
TreeCache:可以監控整個樹上的所有節點,類似於PathChildrenCache和NodeCache的組合

服務端啟動

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
DefaultEventExecutorGroup serviceHandlerGroup = new DefaultEventExecutorGroup(
                RuntimeUtil.cpus() * 2,
                ThreadPoolFactoryUtil.createThreadFactory("service-handler-group", false)
        );


ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // TCP預設開啟了 Nagle 演算法,該演算法的作用是儘可能的傳送大資料快,減少網路傳輸。TCP_NODELAY 引數的作用就是控制是否啟用 Nagle 演算法。
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    // 是否開啟 TCP 底層心跳機制
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //表示系統用於臨時存放已完成三次握手的請求的佇列的最大長度,如果連線建立頻繁,伺服器處理建立新連線較慢,可以適當調大這個引數
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 當客戶端第一次進行請求的時候才會進行初始化
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // 30 秒之內沒有收到客戶端請求的話就關閉連線
                            ChannelPipeline p = ch.pipeline();
                            // 在 Netty 中,IdleStateHandler 是一個用於檢測連線空閒狀態的處理器。當通道在指定時間內沒有任何讀、寫或讀寫操作時,它會觸發相應的事件。
                            p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
                            p.addLast(new RpcMessageEncoder());
                            p.addLast(new RpcMessageDecoder());
                            p.addLast(serviceHandlerGroup, new NettyRpcServerHandler());
                        }
                    });

            // 繫結埠,同步等待繫結成功
            ChannelFuture f = b.bind(host, PORT).sync();
            // 等待服務端監聽埠關閉
            f.channel().closeFuture().sync();

以上方法實在try語句中,最終要呼叫shutdownGracefully用於優雅地關閉 Netty 的執行緒池。

流水線

通訊協議:

image-20240806202508062

Encoder

public class RpcMessageEncoder extends MessageToByteEncoder<RpcMessage> {
    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);

    @Override
    protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) {
        try {
            // 魔法數 用於識別該訊息是否是該接受的
            // public static final byte[] MAGIC_NUMBER = {(byte) 'g', (byte) 'r', (byte) 'p', (byte) 'c'};
            out.writeBytes(RpcConstants.MAGIC_NUMBER);
            // 協議版本
            out.writeByte(RpcConstants.VERSION);
            // 留出4位元組的訊息長度欄位
            out.writerIndex(out.writerIndex() + 4);
            // 訊息型別 請求/心跳/
            byte messageType = rpcMessage.getMessageType();
            out.writeByte(messageType);
            // 序列化型別
            out.writeByte(rpcMessage.getCodec());
            // 壓縮型別
            out.writeByte(CompressTypeEnum.GZIP.getCode());
            // requestId 用了原子自增
            out.writeInt(ATOMIC_INTEGER.getAndIncrement());
            // build full length
            // 構造訊息體
            byte[] bodyBytes = null;
            // 常量16位元組
            int fullLength = RpcConstants.HEAD_LENGTH;
            // if messageType is not heartbeat message,fullLength = head length + body length
            // 如果不是心跳檢測
            if (messageType != RpcConstants.HEARTBEAT_REQUEST_TYPE
                    && messageType != RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
                // serialize the object 序列化
                String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
                log.info("codec name: [{}] ", codecName);
                // 載入序列化器
                Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
                        .getExtension(codecName);
                bodyBytes = serializer.serialize(rpcMessage.getData());
                // compress the bytes 壓縮
                String compressName = CompressTypeEnum.getName(rpcMessage.getCompress());
                Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
                        .getExtension(compressName);
                bodyBytes = compress.compress(bodyBytes);
                // 總長度 = 頭+體
                fullLength += bodyBytes.length;
            }

            if (bodyBytes != null) {
                // 寫入訊息
                out.writeBytes(bodyBytes);
            }
            int writeIndex = out.writerIndex();
            // 定位到訊息協議長度欄位 寫入長度
            out.writerIndex(writeIndex - fullLength + RpcConstants.MAGIC_NUMBER.length + 1);
            out.writeInt(fullLength);
            // 還原index
            out.writerIndex(writeIndex);
        } catch (Exception e) {
            log.error("Encode request error!", e);
        }
    }
}

Decoder

LengthFieldBasedFrameDecoder 是 Netty 提供的一個解碼器,用於處理基於長度欄位的幀。它透過在訊息中指定長度欄位,來確定每個訊息的邊界,以解決 TCP 粘包和拆包問題。

public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder {
    public RpcMessageDecoder() {
        // lengthFieldOffset: magic code is 4B, and version is 1B, and then full length. so value is 5
        // lengthFieldLength: full length is 4B. so value is 4
        // lengthAdjustment: full length include all data and read 9 bytes before, so the left length is (fullLength-9). so values is -9
        // initialBytesToStrip: we will check magic code and version manually, so do not strip any bytes. so values is 0
        this(RpcConstants.MAX_FRAME_LENGTH, 5, 4, -9, 0);
        
        /**
     * @param maxFrameLength      最大長度,如果超過則丟棄
     * @param lengthFieldOffset      長度欄位在那個位置
     * @param lengthFieldLength     長度欄位的長度
     * @param lengthAdjustment    以長度欄位為基準,還有幾個位元組是訊息內容,由於我們要自己處理魔法數等等,魔法書+版本+長度是9位元組 所以要減去9位元組回到開頭
     * @param initialBytesToStrip Number of bytes skipped.
     *                            If you need to receive all of the header+body data, this value is 0
     *                            if you only want to receive the body data, then you need to skip the number of bytes consumed by the header.
     */
    }

然後根據協議定義的每個欄位幾個位元組,一個一個欄位的讀取。 然後解壓縮,反序列化。

private Object decodeFrame(ByteBuf in) {
        // note: must read ByteBuf in order
        checkMagicNumber(in);
        checkVersion(in);
        int fullLength = in.readInt();
        // build RpcMessage object
        byte messageType = in.readByte();
        byte codecType = in.readByte();
        byte compressType = in.readByte();
        int requestId = in.readInt();
        RpcMessage rpcMessage = RpcMessage.builder()
                .codec(codecType)
                .requestId(requestId)
                .messageType(messageType).build();
        if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
            rpcMessage.setData(RpcConstants.PING);
            return rpcMessage;
        }
        if (messageType == RpcConstants.HEARTBEAT_RESPONSE_TYPE) {
            rpcMessage.setData(RpcConstants.PONG);
            return rpcMessage;
        }
        int bodyLength = fullLength - RpcConstants.HEAD_LENGTH;
        if (bodyLength > 0) {
            byte[] bs = new byte[bodyLength];
            in.readBytes(bs);
            // decompress the bytes
            String compressName = CompressTypeEnum.getName(compressType);
            Compress compress = ExtensionLoader.getExtensionLoader(Compress.class)
                    .getExtension(compressName);
            bs = compress.decompress(bs);
            // deserialize the object
            String codecName = SerializationTypeEnum.getName(rpcMessage.getCodec());
            log.info("codec name: [{}] ", codecName);
            Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class)
                    .getExtension(codecName);
            if (messageType == RpcConstants.REQUEST_TYPE) {
                RpcRequest tmpValue = serializer.deserialize(bs, RpcRequest.class);
                rpcMessage.setData(tmpValue);
            } else {
                RpcResponse tmpValue = serializer.deserialize(bs, RpcResponse.class);
                rpcMessage.setData(tmpValue);
            }
        }
        return rpcMessage;

    }

NettyRpcServerHandler

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    try {
        if (msg instanceof RpcMessage) {
            log.info("server receive msg: [{}] ", msg);
            byte messageType = ((RpcMessage) msg).getMessageType();
            RpcMessage rpcMessage = new RpcMessage();
            rpcMessage.setCodec(SerializationTypeEnum.HESSIAN.getCode());
            rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode());
            // 心跳檢測
            if (messageType == RpcConstants.HEARTBEAT_REQUEST_TYPE) {
                rpcMessage.setMessageType(RpcConstants.HEARTBEAT_RESPONSE_TYPE);
                // 返回一個pong
                rpcMessage.setData(RpcConstants.PONG);
            } else {
                // 轉換類
                RpcRequest rpcRequest = (RpcRequest) ((RpcMessage) msg).getData();
                //  rpcRequest.getRpcServiceName() 透過serviceName 去map裡拿到對應的類,再根據方法名和引數型別確定方法反射執行
                Object result = rpcRequestHandler.handle(rpcRequest);
                log.info(String.format("server get result: %s", result.toString()));
                //  型別為  返回
                rpcMessage.setMessageType(RpcConstants.RESPONSE_TYPE);
                if (ctx.channel().isActive() && ctx.channel().isWritable()) {
                    RpcResponse<Object> rpcResponse = RpcResponse.success(result, rpcRequest.getRequestId());
                    rpcMessage.setData(rpcResponse);
                } else {
                    RpcResponse<Object> rpcResponse = RpcResponse.fail(RpcResponseCodeEnum.FAIL);
                    rpcMessage.setData(rpcResponse);
                    log.error("not writable now, message dropped");
                }
            }
            // 如果在寫入資料的過程中出現錯誤,Netty 會關閉連線
            ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
        }
    } finally {
        //Ensure that ByteBuf is released, otherwise there may be memory leaks
        ReferenceCountUtil.release(msg);
    }
}

NettyClientMain

客戶端呼叫HelloController 的test方法

然後呼叫了helloService.hello 方法,而HelloService由與服務端提供

@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")));
        }
    }
}

在這個bean載入的時候,就將helloService 替換為了代理物件,那麼執行hello方法,是由代理物件來完成。

@Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass = bean.getClass();
        Field[] declaredFields = targetClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            RpcReference rpcReference = declaredField.getAnnotation(RpcReference.class);
            if (rpcReference != null) {
                RpcServiceConfig rpcServiceConfig = RpcServiceConfig.builder()
                        .group(rpcReference.group())
                        .version(rpcReference.version()).build();
                // rpcClient = NettyRpcClient     rpcServiceConfig = 註解指定的service  結構在上文已經提過
                RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceConfig);
                Object clientProxy = rpcClientProxy.getProxy(declaredField.getType());
                declaredField.setAccessible(true);
                try {
                    // 將
                    // @RpcReference(version = "version1", group = "test1")
                    //    private HelloService helloService;
                    // 替換為代理物件
                    declaredField.set(bean, clientProxy);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }

        }
        return bean;
    }

代理物件

代理物件的invoke 方法

@Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        log.info("invoked method: [{}]", method.getName());
        // 構造RpcRequest  類結構在下一個程式碼塊
        RpcRequest rpcRequest = RpcRequest.builder().methodName(method.getName())
                .parameters(args)
                .interfaceName(method.getDeclaringClass().getName()) // 類名 github.javaguide.HelloService
                .paramTypes(method.getParameterTypes())
                .requestId(UUID.randomUUID().toString())
                // rpcServiceConfig 在上一塊程式碼中 初始化代理物件時傳入
                .group(rpcServiceConfig.getGroup())
                .version(rpcServiceConfig.getVersion())
                .build();
        RpcResponse<Object> rpcResponse = null;
        // 基於netty
        if (rpcRequestTransport instanceof NettyRpcClient) {
            // 傳送request請求
            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();
    }
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    private String interfaceName;
    private String methodName;
    private Object[] parameters;
    private Class<?>[] paramTypes;
    private String version;
    private String group;

    public String getRpcServiceName() {
        return this.getInterfaceName() + this.getGroup() + this.getVersion();
    }
}

sendRpcRequest

@Override
    public Object sendRpcRequest(RpcRequest rpcRequest) {
        // build return value
        CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
        // 發現服務
        /**  
        	1. lookupService 中使用到了負載均衡演算法,我們首先需要再次回顧一下服務註冊時的zk節點結構
        	   是以註冊的HelloService實現的介面名+組名+版本號 + ip port為zk的路徑,如/my-rpc/github.javaguide.HelloServicetest1version1/20.20.20.231:9998
        	2. 透過 rpcRequest.getRpcServiceName 得到介面名+組名+版本號 然後拿到這個節點的所有子節點 如上舉例的就是20.20.20.231:9998,如果註冊了更多,就會拿到多個ip + port,也就對應了多個服務提供方,這樣的話就由負載均衡演算法選擇一個來提供服務,負載均衡演算法這裡就不說了。
        	
        **/
        InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcRequest);
        // 拿到對應服務方的channel 如果沒有的話 就會執行初始的連線操作。
        Channel channel = getChannel(inetSocketAddress);
        if (channel.isActive()) {
            // put unprocessed request
            unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture);
            RpcMessage rpcMessage = RpcMessage.builder().data(rpcRequest)
                    .codec(SerializationTypeEnum.HESSIAN.getCode())
                    .compress(CompressTypeEnum.GZIP.getCode())
                    .messageType(RpcConstants.REQUEST_TYPE).build();
            // 將 rpcMessage 寫入到與服務端的連線中,並觸發重新整理操作,確保資料立即傳送到服務端
            channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
                if (future.isSuccess()) {
                    log.info("client send message: [{}]", rpcMessage);
                } else {
                    future.channel().close();
                    resultFuture.completeExceptionally(future.cause());
                    log.error("Send failed:", future.cause());
                }
            });
        } else {
            throw new IllegalStateException();
        }

        return resultFuture;
    }

tips:

該專案還有Extension去載入一些類,大部分在類初始化的時候會觸發。舉個例子。

在服務註冊的時候呼叫了nettyRpcServer.registerService(rpcServiceConfig);

public class NettyRpcServer {

    public static final int PORT = 9998;

    // 單例獲取例項 透過反射呼叫 ZkServiceProviderImpl 的構造器 初始化例項
    private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ZkServiceProviderImpl.class);

    public void registerService(RpcServiceConfig rpcServiceConfig) {
        serviceProvider.publishService(rpcServiceConfig);
    }
}

registerService 呼叫了 serviceProviderpublishService方法,至於serviceProvider 用到雙重校驗鎖來實現單例 這裡就不說了。

關鍵在於ZkServiceProviderImpl在構造器 初始化例項時載入了服務註冊類 serviceRegistry

public class ZkServiceProviderImpl implements ServiceProvider {

    /**
     * key: rpc service name(interface name + version + group)
     * value: service object
     */

    // 實現類 ZkServiceRegistryImpl
    private final ServiceRegistry serviceRegistry;

    public ZkServiceProviderImpl() {
        serviceRegistry = ExtensionLoader.getExtensionLoader(ServiceRegistry.class).getExtension(ServiceRegistryEnum.ZK.getName());
        // 實際就是 serviceRegistry = ZkServiceRegistryImpl
    }

getExtensionLoader 會返回一個 ExtensionLoader 類

public final class ExtensionLoader<T> {
	private final Class<?> type;
}

主要是指定type

其中的 type屬性就是 傳入的引數 ServiceRegistry.class 也就是 github.javaguide.registry.ServiceRegistry

需要注意這個在 src/main/resources/META-INF/extensions 下有同名的檔案

然後 呼叫這個返回的ExtensionLoader.getExtension 方法,引數為常量 "zk"

你去看下那個同名檔案就知道為什麼了~

// name = zk  netty  socket
public T getExtension(String name) {
    if (StringUtil.isBlank(name)) {
        throw new IllegalArgumentException("Extension name should not be null or empty.");
    }
    // firstly get from cache, if not hit, create one
    // 例項快取               Map<String, Holder<Object>> cachedInstances
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<>());
        holder = cachedInstances.get(name);// 一個空的Holder 類,holder 只有一個屬性 value 用於存放例項物件
    }
    // create a singleton if no instance exists
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

private T createExtension(String name) {
    // load all extension classes of type T from file and get specific one by name
    //  getExtensionClasses() 中返回了 Map  "zk" -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl"  這也是上文中提到了resource檔案中的類,那個檔案指定了要載入哪些類    然後存在 cachedClasses
    Class<?> clazz = getExtensionClasses().get(name);
    // clazz = github.javaguide.registry.zk.ZkServiceRegistryImpl
    if (clazz == null) {
        throw new RuntimeException("No such extension of name " + name);
    }
    T instance = (T) EXTENSION_INSTANCES.get(clazz);
    if (instance == null) {
        try {
            //  反射例項化
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            // github.javaguide.registry.zk.ZkServiceRegistryImpl -> 例項
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
    return instance;
}
private Map<String, Class<?>> getExtensionClasses() {
    // get the loaded extension class from the cache
    // zk -> "class github.javaguide.registry.zk.ZkServiceRegistryImpl"
    Map<String, Class<?>> classes = cachedClasses.get();
    // double check
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = new HashMap<>();
                // load all extensions from our extensions directory
                loadDirectory(classes);
                // class 快取
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

相關文章