基于netty手写RPC框架

代码目录结构

在这里插入图片描述
  • rpc-common存放公共类
  • rpc-interface为rpc调用方需要调用的接口
  • rpc-register提供服务的注册与发现
  • rpc-client为rpc调用方底层实现
  • rpc-server为rpc被调用方底层实现
  • rpc-sample-client就是使用自实现的rpc框架调用rpc-sample-server
  • rpc-sample-server就是rpc框架的被调用方

技术要点

1. 使用zookeeper作注册中心,把被调用方的信息注册上去

在这里插入图片描述

在这里插入图片描述

服务的注册

public void register(String data) {
        if (data != null) {
            byte[] bytes = data.getBytes();
            try {
                if (zk.exists(dataPath, null) == null) {
                    zk.create(dataPath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }
                zk.create(dataPath + "/data", bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            } catch (KeeperException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

服务的发现

public String discover() {
        String data = null;
        int size = dataList.size();
        // 存在新节点,使用即可
        if (size > 0) {
            if (size == 1) {
                data = dataList.get(0);
            } else {
                data = dataList.get(ThreadLocalRandom.current().nextInt(size));
            }
        }
        return data;
    }

2、自定义注解

注解RpcService标记被调用方的实现类,RpcClientService标记调用方的类需要生成代理类

@Target({ ElementType.TYPE })//注解用在接口上
@Retention(RetentionPolicy.RUNTIME)//VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息
@Component
public @interface RpcClientService {
}
@Target({ ElementType.TYPE })//注解用在接口上
@Retention(RetentionPolicy.RUNTIME)//VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息
@Component
public @interface RpcService {
    Class<?> value();
}

3、调用方代理类的注入

扫描包下的RpcClientService注解,并生成代理类

/**
 * 用于Spring动态注入自定义接口
 *
 * @author shuangyueliao
 */
@Component
public class ServiceBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        Set<Class<?>> typesAnnotatedWith = new Reflections("com.shuangyueliao.rpc.myinterface").getTypesAnnotatedWith(RpcClientService.class);
        for (Class beanClazz : typesAnnotatedWith) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz);
            GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();

            //在这里,我们可以给该对象的属性注入对应的实例。
            //比如mybatis,就在这里注入了dataSource和sqlSessionFactory,
            // 注意,如果采用definition.getPropertyValues()方式的话,
            // 类似definition.getPropertyValues().add("interfaceType", beanClazz);
            // 则要求在FactoryBean(本应用中即ServiceFactory)提供setter方法,否则会注入失败
            // 如果采用definition.getConstructorArgumentValues(),
            // 则FactoryBean中需要提供包含该属性的构造方法,否则会注入失败
            Properties properties = new Properties();
            InputStream is=this.getClass().getResourceAsStream("/application.properties");
            try {
                properties.load(is);
            } catch (IOException e) {
                e.printStackTrace();
            }
            String registerAddress = properties.getProperty("zookeeper.url");
            String dataPath = properties.getProperty("zookeeper.register.path.prefix");
            ServiceDiscovery serviceDiscovery = new ServiceDiscovery(registerAddress, dataPath);
            definition.getPropertyValues().addPropertyValue("serviceDiscovery", serviceDiscovery);
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClazz);

            //注意,这里的BeanClass是生成Bean实例的工厂,不是Bean本身。
            // FactoryBean是一种特殊的Bean,其返回的对象不是指定类的一个实例,
            // 其返回的是该工厂Bean的getObject方法所返回的对象。
            definition.setBeanClass(RpcProxy.class);

            //这里采用的是byType方式注入,类似的还有byName等
            definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
            registry.registerBeanDefinition(beanClazz.getSimpleName(), definition);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }

}

获取代理类

public class RpcProxy<T> implements FactoryBean<T> {

    private String serverAddress;

    private Class<T> interfaceType;

    private ServiceDiscovery serviceDiscovery;

    public RpcProxy(Class<T> interfaceType) {
        this.interfaceType = interfaceType;
    }

    public ServiceDiscovery getServiceDiscovery() {
        return serviceDiscovery;
    }

    public void setServiceDiscovery(ServiceDiscovery serviceDiscovery) {
        this.serviceDiscovery = serviceDiscovery;
    }

    private RpcClient rpcClient;

    @Override
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(interfaceType.getClassLoader(),
                new Class<?>[] { interfaceType }, (proxy, method, args) -> {
                    //创建RpcRequest,封装被代理类的属性
                    RpcRequest request = new RpcRequest();
                    request.setRequestId(UUID.randomUUID().toString());
                    //拿到声明这个方法的业务接口名称
                    request.setClassName(method.getDeclaringClass()
                            .getName());
                    request.setMethodName(method.getName());
                    request.setParameterTypes(method.getParameterTypes());
                    request.setParameters(args);
                    synchronized (this) {
                        if (rpcClient == null) {
                            //查找服务
                            if (serviceDiscovery != null) {
                                serverAddress = serviceDiscovery.discover();
                            }
                            //随机获取服务的地址
                            String[] array = serverAddress.split(":");
                            String host = array[0];
                            int port = Integer.parseInt(array[1]);
                            //创建Netty实现的RpcClient,链接服务端
                            rpcClient = new RpcClient(host, port);
                        }
                    }
                    //通过netty向服务端发送请求
                    RpcResponse response = rpcClient.send(request);
                    //返回信息
                    if (response.isError()) {
                        throw response.getError();
                    } else {
                        return response.getResult();
                    }
                });
    }

    @Override
    public Class<?> getObjectType() {
        return interfaceType;
    }

}

调用方底层基于netty的发送请求和接收响应

public RpcClient(String host, int port) {
        this.host = host;
        this.port = port;
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel channel) {
                            // 向pipeline中添加编码、解码、业务处理的handler
                            channel.pipeline()
                                    .addLast(new RpcEncoder(RpcRequest.class))  //OUT - 1
                                    .addLast(new RpcDecoder(RpcResponse.class)) //IN - 1
                                    .addLast(RpcClient.this);                   //IN - 2
                        }
                    }).option(ChannelOption.SO_KEEPALIVE, true);
            // 链接服务器
            future = bootstrap.connect(host, port).sync();
        } catch (Exception e) {
            e.printStackTrace();
            try {
                future.channel().closeFuture().sync();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
            group.shutdownGracefully();
        }
    }

    /**
     * 链接服务端,发送消息
     *
     * @param request
     * @return
     * @throws Exception
     */
    public RpcResponse send(RpcRequest request) throws Exception {
        //将request对象写入outbundle处理后发出(即RpcEncoder编码器)
        // 用线程等待的方式决定是否关闭连接
        // 其意义是:先在此阻塞,等待获取到服务端的返回后,被唤醒,从而关闭网络连接
        Object o = new Object();
        locks.put(request.getRequestId(), o);
        synchronized (o) {
            future.channel().writeAndFlush(request);
            o.wait(10000);
        }
        return response;
    }

    /**
     * 读取服务端的返回结果
     */
    @Override
    public void channelRead0(ChannelHandlerContext ctx, RpcResponse response)
            throws Exception {
        this.response = response;
        Object o = locks.remove(response.getRequestId());
        synchronized (o) {
            o.notify();
        }
    }

4、被调用方

获取接口与实现类的对应关系

public void setApplicationContext(ApplicationContext ctx)
            throws BeansException {
        Map<String, Object> serviceBeanMap = ctx
                .getBeansWithAnnotation(RpcService.class);
        if (MapUtils.isNotEmpty(serviceBeanMap)) {
            for (Object serviceBean : serviceBeanMap.values()) {
                //从业务实现类上的自定义注解中获取到value,从来获取到业务接口的全名
                String interfaceName = serviceBean.getClass()
                        .getAnnotation(RpcService.class).value().getName();
                handlerMap.put(interfaceName, serviceBean);
            }
        }
    }

读取调用方传递过来的接口名和参数,利用反射调用相应类并返回结果给前端

public void channelRead0(final ChannelHandlerContext ctx, RpcRequest request)
            throws Exception {
        RpcResponse response = new RpcResponse();
        response.setRequestId(request.getRequestId());
        try {
            //根据request来处理具体的业务调用
            Object result = handle(request);
            response.setResult(result);
        } catch (Throwable t) {
            response.setError(t);
        }
        //写入 outbundle(即RpcEncoder)进行下一步处理(即编码)后发送到channel中给客户端
        ctx.writeAndFlush(response);
    }

    /**
     * 根据request来处理具体的业务调用
     * 调用是通过反射的方式来完成
     * 
     * @param request
     * @return
     * @throws Throwable
     */
    private Object handle(RpcRequest request) throws Throwable {
        String className = request.getClassName();
        
        //拿到实现类对象
        Object serviceBean = handlerMap.get(className);
        
        //拿到要调用的方法名、参数类型、参数值
        String methodName = request.getMethodName();
        Class<?>[] parameterTypes = request.getParameterTypes();
        Object[] parameters = request.getParameters();
        
        //拿到接口类
        Class<?> forName = Class.forName(className);
        
        //调用实现类对象的指定方法并返回结果
        Method method = forName.getMethod(methodName, parameterTypes);
        return method.invoke(serviceBean, parameters);
    }

5、自定义rpc框架的使用

1、被调用方maven依赖

<dependency>
            <groupId>com.shuangyueliao</groupId>
            <artifactId>rpc-server</artifactId>
            <version>1.0-SNAPSHOT</version>
</dependency>

2、调用方maven依赖

<dependency>
            <groupId>com.shuangyueliao</groupId>
            <artifactId>rpc-client</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
</dependency>

3、被调用方实现类加上注解RpcService,里面的值是被调用的接口

@RpcService(PayService.class)
public class PayServiceImpl implements PayService {
    @Override
    public int calculate(int a, int b) {
        int result = a + b;
        return result;
    }
}

4、调用方建立包名com.shuangyueliao.rpc.myinterface,新建要调用的接口,并加上注解RpcClientService

@RpcClientService
public interface PayService {
    int calculate(int a, int b);
}

功能演示

1、启动zookeeper,如需要修改zookeeper连接地址,请修改rpc-sample-server和rpc-sample-client的配置文件application.properties中的配置项zookeeper.url
2、运行rpc-sample-server(被调用方)RpcBootstrap的main方法启动被调用方
3、运行rpc-sample-client(调用方)的StartApp的main方法启动调用方
4、浏览器输入http://localhost:8090/order请求rpc-sample-client,rpc-sample-client会RPC调用rpc-sample-server

在这里插入图片描述

在这里插入图片描述

项目地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容