基於Netty 手寫 Dubbo 框架

基於Netty 手寫 Dubbo 框架

1、Dubbo是什麼,解決什麼樣的問題?

爲了解決模塊拆分後,彼此遠程調用的問題。

RPC -> Remote Procedure Call 遠程調用,常見的RPC框架有:

阿里的:dubbo。

噹噹的:dubbox。

谷歌的:grpc。

SpringCloud(一站式開發)等。

2、實現方案

查看官網dubbo結構圖

在這裏插入圖片描述

1、首先通過register將服務提供者的url註冊到Registry註冊中心中。

2、客戶端Consumer從註冊中心獲取被調用服務端註冊信息,如:接口名稱,URL地址等信息。

3、將獲取的url地址返回到Consumer客戶端,客戶端通過獲取的URL地址支持invoke反射機制獲取服務的實現。

3、整體項目結構信息

|-- netty-to-dubbo
	|-- netty-dubbo-api
		|-- cn.org.july.netty.dubbo.api
			|-- Iservice : 對外服務暴露接口
			|-- RpcRequest :服務請求對象Bean
	|-- netty-dubbo-common
		|-- cn.org.july.netty.dubbo.annotation
			|-- RpcAnnotation : 定義一個接口標識註解
    |-- netty-dubbo-server
    	|-- cn.org.july.netty.dubbo.registry
    		|-- IRegisterCenter :服務註冊接口
    		|-- RegisterCenterImpl:服務註冊實現類
    		|-- ZkConfig:ZK配置文件
    	|-- cn.org.july.netty.dubbo.rpc
    		|-- NettyRpcServer:基於netty實現的Rpc通訊服務
    		|-- RpcServerHandler:Rpc服務處理流程
    	|-- cn.org.july.netty.dubbo.service
    		|-- ServiceImpl:對外接口IService接口實現類
    |-- netty-dubbo-client
    	|-- cn.org.july.netty.dubbo.loadbalance
    		|-- LoadBalance :負載均衡實現接口
    		|-- RandomLoadBalance:負載均衡實現類隨機獲取服務提供者
    	|-- cn.org.july.netty.dubbo.proxy
    		|-- RpcClientProxy:netty客戶端通訊組件
    		|-- RpcProxyHandler:netty與服務端通訊消息處理組件
    	|-- cn.org.july.netty.dubbo.registry
    		|-- IServiceDiscover:從註冊中心獲取註冊的服務接口
    		|-- ServiceDiscoverImpl:接口IServiceDiscover的實現類
    		|-- ZkConfig:zk配置文件。

4、服務提供者Provider

4.1、實現Iservice接口

首先我們看下Iservice接口的內容:

package cn.org.july.netty.dubbo.api;

/**
 * @author july_whj
 */
public interface IService {
    /**
     * 計算加法
     */
    int add(int a, int b);
    /**
     * @param msg
     */
    String sayHello(String msg);
}

我們編寫ServiceImpl實現以上兩個接口類。

package cn.org.july.netty.dubbo.service;

import cn.org.july.netty.dubbo.annotation.RpcAnnotation;
import cn.org.july.netty.dubbo.api.IService;

/**
 * @author july_whj
 */
@RpcAnnotation(IService.class)
public class ServiceImpl implements IService {
    @Override
    public int add(int a, int b) {
        return a + b;
    }
    @Override
    public String sayHello(String msg) {
        System.out.println("rpc say :" + msg);
        return "rpc say: " + msg;
    }
}

該類實現比較簡單,不做多處理,下面分析服務註冊。

4.2、服務註冊到ZK

​ 首先我們定義一個接口類IRegisterCenter,裏面定義一個registry方法,該方法實現服務註冊。服務註冊需要將服務的名稱、服務的地址註冊到註冊中心中,我們定義接口如下:

package cn.org.july.netty.dubbo.registry;

/**
 * @author july_whj
 */
public interface IRegisterCenter {
    /**
     * 服務註冊
     * @param serverName 服務名稱(實現方法路徑)
     * @param serviceAddress 服務地址
     */
    void registry(String serverName,String serviceAddress);
}

​ 第二,我們使用zookeerper作爲服務註冊中心,在netty-dubbo-server模塊中引入zk的客戶端操作類,pom文件如下:

<dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-recipes</artifactId>
     <version>2.5.0</version>
</dependency>
<dependency>
     <groupId>org.apache.curator</groupId>
     <artifactId>curator-framework</artifactId>
     <version>2.5.0</version>
</dependency>

注意:這裏版本使用的2.5.0,我使用的zk版,

​ 第三,實現該接口編寫接口實現類RegisterCenterImpl

package cn.org.july.netty.dubbo.registry;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

/**
 * @author july_whj
 */
public class RegisterCenterImpl implements IRegisterCenter {

    private CuratorFramework curatorFramework;
    {
        curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(ZkConfig.addr).sessionTimeoutMs(4000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 10)).build();
        curatorFramework.start();
    }
    @Override
    public void registry(String serverName, String serviceAddress) {
        String serverPath = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serverName);
        try {
            if (curatorFramework.checkExists().forPath(serverPath) == null) {
                curatorFramework.create().creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT).forPath(serverPath, "0".getBytes());
            }
            String addr = serverPath.concat("/").concat(serviceAddress);
            String rsNode = curatorFramework.create().withMode(CreateMode.EPHEMERAL)
                    .forPath(addr, "0".getBytes());
            System.out.println("服務註冊成功," + rsNode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

我們分析下以上代碼:

​ 定義一個CuratorFramework對象,通過代碼塊來實例化該對象,並通過curatorFramework.start();來連接ZKConfig中配置好的地址連接ZK。

​ 使用zk作爲註冊中心,我們瞭解下ZK的存儲結構。zookeeper的命名空間的結構和文件系統很像。一個名字和文件一樣使用/的路徑表現,zookeeper的每個節點都是被路徑唯一標識的。

在這裏插入圖片描述
​ 分析一下registry方法,首先從ZkConfig中獲取要註冊數據的根節點信息,並將該信息和服務名稱進行拼接,判斷該路徑是否存在,如果不存在使用PERSISTENT方式創建該服務名稱路徑信息。PERSISTENT方式爲持久方式,我們使用這種方式創建因爲服務名稱不是動態變化的,不用每次去監聽它的變化。而我們服務的地址是有可能存在多個,並且有可能發生變化,我們使用EPHEMERAL方式來創建服務的實現地址。

​ 我們將ServiceImpl服務註冊到zk上,我們首先獲取這個服務的服務名稱,和服務實現的地址,將該服務的服務名稱和服務地址註冊到zk上,下面看下我們的註冊服務的測試類RegTest

import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import cn.org.july.netty.dubbo.registry.RegisterCenterImpl;

import java.io.IOException;

public class RegTest {
    public static void main(String[] args) throws IOException {
        IRegisterCenter registerCenter = new RegisterCenterImpl();
        registerCenter.registry("cn.org.july.test", "127.0.0.1:9090");
        System.in.read();
    }
}

​ 我們將cn.org.july.test服務,和服務實現的地址127.0.0.1:9090註冊到zk中。

看下服務執行效果:

在這裏插入圖片描述
服務端顯示註冊成功,我們看以下zk服務中有沒有該數據,
在這裏插入圖片描述

最後,我們可以看到數據註冊成功。

4.3、實現NettyRpcServer

​ 我們要將ServiceImpl服務發佈到zk上,並通過netty監聽某個端口信息。

​ 我們先看下

package cn.org.july.netty.dubbo.rpc;

import cn.org.july.netty.dubbo.annotation.RpcAnnotation;
import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.util.HashMap;
import java.util.Map;

/**
 * @author july_whj
 */
public class NettyRpcServer {

    private IRegisterCenter registerCenter;
    private String serviceAddress;
    private Map<String, Object> handlerMap = new HashMap<>(16);

    public NettyRpcServer(IRegisterCenter registerCenter, String serviceAddress) {
        this.registerCenter = registerCenter;
        this.serviceAddress = serviceAddress;
    }

    /**
     * 發佈服務
     */
    public void publisher() {
        for (String serviceName : handlerMap.keySet()) {
            registerCenter.registry(serviceName, serviceAddress);
        }
        try {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            //啓動netty服務
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    ChannelPipeline channelPipeline = channel.pipeline();
                    channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
                    channelPipeline.addLast(new ObjectEncoder());
                    channelPipeline.addLast(new RpcServerHandler(handlerMap));
                }
            }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
            String[] addr = serviceAddress.split(":");
            String ip = addr[0];
            int port = Integer.valueOf(addr[1]);
            ChannelFuture future = bootstrap.bind(ip, port).sync();
            System.out.println("服務啓動,成功。");
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 子對象的實現
     *
     * @param services 對象實現類
     */
    public void bind(Object... services) {
        //將實現類通過註解獲取實現類的名稱、實現類的實現放入map集合中。
        for (Object service : services) {
            RpcAnnotation annotation = service.getClass().getAnnotation(RpcAnnotation.class);
            String serviceName = annotation.value().getName();
            handlerMap.put(serviceName, service);
        }
    }
}

分析下以上代碼:

​ 通過bind方法,將服務提供者通過RpcAnnotation註解獲取服務名稱,並將服務名稱,服務實現類放入handlerMap 中。

​ 通過publisher方法,獲取handlerMap 中的服務實現,將這些服務實現通過registerCenter.registry(serviceName, serviceAddress)將這些服務註冊到zk註冊中心中,完成服務的註冊。下面代碼是netty的基礎代碼,創建兩個工作線程池,啓動netty服務,通過channelPipeline定義序列化對象和RpcServerHandler實現。這裏不做過多解析。

​ 我們看下RpcServerHandler的代碼實現。

package cn.org.july.netty.dubbo.rpc;

import cn.org.july.netty.dubbo.api.RpcRequest;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.Buffer;
import java.util.HashMap;
import java.util.Map;

public class RpcServerHandler extends ChannelInboundHandlerAdapter {
    private Map<String, Object> handlerMap = new HashMap<>();

    public RpcServerHandler(Map<String, Object> handlerMap) {
        this.handlerMap = handlerMap;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws UnsupportedEncodingException {
        System.out.println("channelActive:" + ctx.channel().remoteAddress());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服務端接收到消息:" + msg);
        RpcRequest rpcRequest = (RpcRequest) msg;
        Object result = new Object();
        if (handlerMap.containsKey(rpcRequest.getClassName())) {
            Object clazz = handlerMap.get(rpcRequest.getClassName());
            Method method = clazz.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getTypes());
            result = method.invoke(clazz, rpcRequest.getParams());
        }
        ctx.write(result);
        ctx.flush();
        ctx.close();
    }
}

​ 這裏複寫了channelRead方法,接收客戶端傳遞的RpcRequest對象信息。下面判斷handlerMap中是否存在客戶端調用的實現類,如果存在通過反射機制獲取服務端實現類,通過invoke方法調用方法實現,並將執行結果result對象通過ctx.write(result);將執行結果返回客戶端。

4.4、編寫服務啓動類ServerTest

import cn.org.july.netty.dubbo.api.IService;
import cn.org.july.netty.dubbo.registry.IRegisterCenter;
import cn.org.july.netty.dubbo.registry.RegisterCenterImpl;
import cn.org.july.netty.dubbo.rpc.NettyRpcServer;
import cn.org.july.netty.dubbo.service.ServiceImpl;

/**
 * Created with IntelliJ IDEA.
 * User:  wanghongjie
 * Date:  2019/5/3 - 23:03
 * <p>
 * Description:
 */
public class ServerTest {
    public static void main(String[] args) {
        IService service = new ServiceImpl();
        IRegisterCenter registerCenter = new RegisterCenterImpl();
        NettyRpcServer rpcServer = new NettyRpcServer(registerCenter, "127.0.0.1:8080");
        rpcServer.bind(service);
        rpcServer.publisher();
    }
}

啓動netty服務,將服務實現類service通過bind方法綁定到handlerMap中,通過publisher方法,將service、服務實現地址發佈到zk,並啓動netty服務,監聽8080端口。

5、實現服務消費者

​ 做爲服務消費者,我們首先要連接zk註冊中心,獲取服務實現的地址,並實時監聽獲取最新的地址信息。通過遠程調用實現該服務。如果服務實現是多個我們需實現客戶端負載,選取我們的服務地址。

5.1、負載均衡實現

​ 定義loadbalance接口.

package cn.org.july.netty.dubbo.loadbalance;

import java.util.List;

public interface LoadBalance {
    String select(List<String> repos);
}

​ 定義select選擇方法。

通過RandomLoadBalance 實現loadbalance接口,從實現名稱可以看到Random隨機獲取。

package cn.org.july.netty.dubbo.loadbalance;

import java.util.List;
import java.util.Random;

public class RandomLoadBalance implements LoadBalance {
    @Override
    public String select(List<String> repos) {
        int len = repos.size();
        if (len == 0)
            throw new RuntimeException("未發現註冊的服務。");
        Random random = new Random();
        return repos.get(random.nextInt(len));
    }
}

5.2、獲取註冊中心服務註冊信息

​ 定義IServiceDiscover接口,定義discover方法,進行服務發現。

package cn.org.july.netty.dubbo.registry;

public interface IServiceDiscover {
    String discover(String serviceName);
}

通過ServiceDiscoverImpl實現IServiceDiscover接口。

package cn.org.july.netty.dubbo.registry;

import cn.org.july.netty.dubbo.loadbalance.LoadBalance;
import cn.org.july.netty.dubbo.loadbalance.RandomLoadBalance;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;

import java.util.ArrayList;
import java.util.List;

/**
 * @author july_whj
 */
public class ServiceDiscoverImpl implements IServiceDiscover {

    List<String> repos = new ArrayList<String>();
    private CuratorFramework curatorFramework;

    public ServiceDiscoverImpl() {
        curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(ZkConfig.addr).sessionTimeoutMs(4000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 10))
                .build();
        curatorFramework.start();
    }

    @Override
    public String discover(String serviceName) {
        String path = ZkConfig.ZK_REGISTER_PATH.concat("/").concat(serviceName);
        try {
            repos = curatorFramework.getChildren().forPath(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
        registerWatch(path);
        LoadBalance loadBalance = new RandomLoadBalance();
        return loadBalance.select(repos);
    }

    /**
     * 監聽ZK節點內容刷新
     *
     * @param path 路徑
     */
    private void registerWatch(final String path) {
        PathChildrenCache childrenCache = new PathChildrenCache(curatorFramework, path, true);
        PathChildrenCacheListener childrenCacheListener = new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                repos = curatorFramework.getChildren().forPath(path);
            }
        };
        childrenCache.getListenable().addListener(childrenCacheListener);
        try {
            childrenCache.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}

​ 和服務註冊同樣定義CuratorFramework對象,並通過curatorFramework.start();連接ZK。

連接成功後通過zk註冊的根節點加服務名稱,獲取該服務的服務地址。

​ 獲取的服務地址有可能不是最新的服務地址,我們需要監聽zk節點的內容刷新,通過調用registerWatch方法,監聽該節點的數據變化。

​ 最後,將獲取到的地址集合,通過LoadBalance隨機選出一個地址,實現該服務。

5.3、客戶端netty實現RPC遠程調用

定義客戶端實現類RpcClientProxy.

package cn.org.july.netty.dubbo.proxy;

import cn.org.july.netty.dubbo.api.RpcRequest;
import cn.org.july.netty.dubbo.registry.IServiceDiscover;
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.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Created with IntelliJ IDEA.
 * User:  wanghongjie
 * Date:  2019/5/3 - 23:08
 * <p>
 * Description:
 */
public class RpcClientProxy {
    private IServiceDiscover serviceDiscover;

    public RpcClientProxy(IServiceDiscover serviceDiscover) {
        this.serviceDiscover = serviceDiscover;
    }

    public <T> T create(final Class<T> interfaceClass) {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass}, new InvocationHandler() {
                    //封裝RpcRequest請求對象,然後通過netty發送給服務等
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        RpcRequest rpcRequest = new RpcRequest();
                        rpcRequest.setClassName(method.getDeclaringClass().getName());
                        rpcRequest.setMethodName(method.getName());
                        rpcRequest.setTypes(method.getParameterTypes());
                        rpcRequest.setParams(args);
                        //服務發現,zk進行通訊
                        String serviceName = interfaceClass.getName();
                        //獲取服務實現url地址
                        String serviceAddress = serviceDiscover.discover(serviceName);
                        //解析ip和port
                        System.out.println("服務端實現地址:" + serviceAddress);
                        String[] arrs = serviceAddress.split(":");
                        String host = arrs[0];
                        int port = Integer.parseInt(arrs[1]);
                        System.out.println("服務實現ip:" + host);
                        System.out.println("服務實現port:" + port);
                        final RpcProxyHandler rpcProxyHandler = new RpcProxyHandler();
                        //通過netty方式進行連接發送數據
                        EventLoopGroup group = new NioEventLoopGroup();
                        try {
                            Bootstrap bootstrap = new Bootstrap();
                            bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)
                                    .handler(new ChannelInitializer<SocketChannel>() {
                                        @Override
                                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                                            ChannelPipeline channelPipeline = socketChannel.pipeline();
                                            channelPipeline.addLast(new ObjectDecoder(1024 * 1024, ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())));
                                            channelPipeline.addLast(new ObjectEncoder());
                                            //netty實現代碼
                                            channelPipeline.addLast(rpcProxyHandler);
                                        }
                                    });
                            ChannelFuture future = bootstrap.connect(host, port).sync();
                            //將封裝好的對象寫入
                            future.channel().writeAndFlush(rpcRequest);
                            future.channel().closeFuture().sync();
                        } catch (Exception e) {

                        } finally {
                            group.shutdownGracefully();
                        }
                        return rpcProxyHandler.getResponse();
                    }
                });
    }
}

​ 我們看下create方法,通過動態代理newProxyInstance方法,傳入待調用的接口對象,獲取getClassLoader後,實現invoke方法。定義RpcRequest對象,封裝請求參數。通過interfaceClass對象獲取服務實現名稱,調用discover方法獲取服務提供者的地址信息,netty通過該信息連接服務,並將RpcRequest對象發送到服務端,服務端解析對象,獲取接口請求參數等信息,執行方法,並將結果返回到客戶端RpcProxyHandler對象接收返回結果。RpcProxyHandler代碼實現:

package cn.org.july.netty.dubbo.proxy;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Created with IntelliJ IDEA.
 * User:  wanghongjie
 * Date:  2019/5/3 - 23:21
 * <p>
 * Description:
 */
public class RpcProxyHandler extends ChannelInboundHandlerAdapter {
    private Object response;

    public Object getResponse() {
        return response;
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //將服務端返回的內容返回
        response = msg;
    }
}

​ 我們複寫channelRead方法,獲取服務端返回的結果信息msg,並將msg賦值給response,通過getResponse獲取返回信息。

5.4、客戶單調用測試

import cn.org.july.netty.dubbo.api.IService;
import cn.org.july.netty.dubbo.proxy.RpcClientProxy;
import cn.org.july.netty.dubbo.registry.IServiceDiscover;
import cn.org.july.netty.dubbo.registry.ServiceDiscoverImpl;

/**
 * Created with IntelliJ IDEA.
 * User:  wanghongjie
 * Date:  2019/5/3 - 23:06
 * <p>
 * Description:
 */
public class ClientTest {
    public static void main(String[] args) {
        IServiceDiscover serviceDiscover = new ServiceDiscoverImpl();
        RpcClientProxy rpcClientProxy = new RpcClientProxy(serviceDiscover);
        IService iService = rpcClientProxy.create(IService.class);
        System.out.println(iService.sayHello("netty-to-dubbo"));
        System.out.println(iService.sayHello("你好"));
        System.out.println(iService.sayHello("成功咯,很高興"));
        System.out.println(iService.add(10, 4));
    }
}

我們看下執行效果。​

服務端啓動:

在這裏插入圖片描述

客戶單調用:
在這裏插入圖片描述
遠程調用完成。
源碼地址:傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章