一点一点实现一个RPC框架 零

dubbo的介绍大家自行百度,下面以一个demo作为dubbo学习的开始


DUBBO 入门例子 - 一个服务一个实现

dubbo版本2.5.8
注册中心使用zookeeper,版本3.4

代码结构如下


image

api是接口服务定义
consumer是服务消费者
provider是服务生产者

api代码如下

public interface DemoService {

    String sayHi(String name);
}

只是用来定义服务接口,代码很简单

provider代码如下
接口实现类

public class ProviderService implements DemoService {

    @Override
    public String sayHi(String name) {

        return "provider " + name;
    }
}

启动类

public class Provider {

    public static void main(String[] args) throws Exception {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"provider.xml"});
        context.start();
        System.in.read();
    }
}

provider.xml文件内容如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="hello-world-app"  />

    <!-- 使用zookeeper注册中心暴露服务地址 -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />

    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880" />

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" />

    <!-- 和本地bean一样实现服务 -->
    <bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />
</beans>

pom文件如下,consumer类似

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>dubboooo</artifactId>
        <groupId>tiger</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>provider</artifactId>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.5.8</version>
        </dependency>

        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.10</version>
        </dependency>
        <dependency>
            <groupId>tiger</groupId>
            <artifactId>api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

consumer代码如下

public class Consumer {

    public static void main(String[] args) {

        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"consumer.xml"});
        context.start();
        DemoService service = (DemoService) context.getBean("demoService");
        System.out.println(service.sayHi("wawa"));
    }
}

consumer.xml配置文件如下

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
    <dubbo:application name="consumer-of-helloworld-app"  />

    <!-- 使用zookeeper注册中心暴露服务地址 -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />

    <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
    <dubbo:reference id="demoService" interface="com.tiger.dubbo.api.DemoService" />
</beans>

依次启动zookeeper、provider、consumer,consumer控制台显示“provider wawa”则成功。

例子 - 一个服务多个实现

以上是dubbo最简单的demo,下面在demo的基础上做一些简答的修改。
上面的代码是provider提供接口实现,然后由consumer消费。如果同一个接口有多个实现consumer是如何区分的?

增加新的接口实现

public class SecondProviderService implements DemoService {
    @Override
    public String sayHi(String name) {

        return "second " + name;
    }
}

在provider.xml文件中暴露新的实现

<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" />

<bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />

<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="secService" />

<bean id="secService" class="com.tiger.dubbo.provider.SecondProviderService" />

现在的问题就是consumer如何区分两个实现。
dubbo是通过分组概念来区分的,在service配置文件中增加group参数,区分每个实现,修改后代码如下

<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="demoService" group="first" />

<bean id="demoService" class="com.tiger.dubbo.provider.ProviderService" />

<dubbo:service interface="com.tiger.dubbo.api.DemoService" ref="secService" group="second"/>

<bean id="secService" class="com.tiger.dubbo.provider.SecondProviderService" />

通过group参数为first和second来区分两个实现,consumer调用代码不变,只是在配置文件中需要指定消费的是哪个实现

<dubbo:reference id="demoService" interface="com.tiger.dubbo.api.DemoService" group="first" />

运行结果这里就不写了,大家可以自己试试.

参考博客

学习

在学习dubbo过程中,经常有“我要学习什么的想法”,经常有看完一部分不知道后面要干嘛的时候。总的来说感觉自己缺少“dubbo能干什么?”的明确答案,所以在学习的时候就缺乏学习路线,这也跟自己长期的学习习惯有关:我要学习A技能,上网搜索 “A技能教程”或者“从零开始学A”。这样自己的学习完全依赖他人的总结,所以导致学习的成果同样依赖博客的质量,这就是高耦合啊同学们!!!我的学习质量为什么要依赖他人的学习成果。希望从今天开始改变自己的学习方式,从dubbo开始.

RPC

dubbo是一个RPC框架,在学习这个框架之前我们需要先明白什么是RPC,为什么需要RPC框架,怎么实现RPC框架(what why how)。

RPC是Remote Procedure Call,字面意思就是“远程过程调用”,翻译为“远程程序调用”可能更符合我们程序员。它的功能就是可以在A机器上访问B机器上的方法。

那么为什么需要RPC,随着业务量扩大,单机系统拆分成集群,为了维护性和高可用性的系统服务化,都需要RPC来实现。那么现在对RPC有了大致了解,也知道为什么要用RPC了,但是RPC框架是什么鬼,它有啥用?那么这就要从如何实现RPC讲起了。

RPC框架结构

我们先来想想如何实现一次远程调用,既然是远程调用,首先肯定要处理网络问题,我们可以使用现有协议比如http,或者我们自定义协议;网络问题解决后就要传输数据了,客户端调用服务端方法,需要知道对方地址(ip),调用的方法地址(接口名称,方法名称),参数等。传输这些数据之前就需要对数据进行序列化操作,相对的接收数据后需要反序列化。盗用大佬的一副图片

image

以及大佬说的:
1)服务消费方(client)调用以本地调用方式调用服务;

2)client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

3)client stub找到服务地址,并将消息发送到服务端;

4)server stub收到消息后进行解码;

5)server stub根据解码结果调用本地的服务;

6)本地服务执行并将结果返回给server stub;

7)server stub将返回结果打包成消息并发送至消费方;

8)client stub接收到消息,并进行解码;

9)服务消费方得到最终结果。
RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

后面我会按照大佬的逻辑实现一个简单rpc框架。

简单通信

首先是两个项目通过socket的通信,相信大家之前都写过类似的轮子,这里就不贴代码;

特定服务调用

既然两个服务可以通过网络互相通信,那么我们完全可以在客户端发送需要调用的服务类信息、方法信息、参数等。
调用的服务

public interface DemoService {

    String sayHi(String name);
}

客户端调用代码如下

public class ClientApp {

    public static void main(String[] args) {

        try {
            RPCClient.start();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        DemoService service = ServiceFactory.createService("demo");
        System.out.println(service.sayHi("haha"));
    }
}

start是启动了网络访问;后面我们需要假装给接口一个实现类,然后由这个实现类执行目标方法。
根据我们前面的理论,我们需要一个代理类来假装实现类,完成访问远端方法的责任,所以这里我们使用一个静态代理实现(为了简单)。

public class DemoServiceProxy implements DemoService {

    public String sayHi(String name){

        String msg = "com.tiger.dubbo.api.DemoService-sayHi-java.lang.String-" + name;
        try {
            return (String) RPCClient.sendMsg(msg);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

代理类非常简单;msg中的信息是传给服务端的,服务端会解析信息中的内容做相应的处理,这里我们简单处理,定义参数间用“-”隔开,第一个为服务类名称,第二个为方法名,第三个为参数类,第四个为参数值(对,只支持一个参数),这就相当于一个简单的协议。

服务端这边也很简单,根据协议解析信息,并做处理代码如下:

public static Object handleMsg(String msg)
            throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
            InstantiationException, InvocationTargetException {

        String[] msgArr = msg.split("-");
        String klassName = msgArr[0];
        if ("com.tiger.dubbo.api.DemoService".equals(klassName))
            klassName = "com.togo.service.DemoServiceImpl";
        Class klass = Class.forName(klassName);
        Class param = Class.forName(msgArr[2]);
        Method method = klass.getMethod(msgArr[1], param);

        return method.invoke(klass.newInstance(), msgArr[3]);
    }

看到我的实现类获取方式有没有某个部位突然发紧。Orz

根据我们的协议,一些信息,并执行对应方法,将信息返回。

至此有翔以来最吊的rpc完成了~~撒花~~

好吧~继续完善,并对照dubbo等框架学习~作者还是很认真的 ~.~

信息

在上面我们已经实现了一个基本完美的rpc框架,但是我们不能骄傲,需要不断完善。

首先我们来处理下消息传递的问题,之前是以‘-’分割字符串,只能传递一个参数。现在我们把它修改为json格式传递。

这里json选择使用阿里的fastjson,啊,真是强强联和~~

maven:

<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>fastjson</artifactId>
 <version>1.2.62</version>
</dependency>

定义传输类Message

public class Message {

    /**
     * 服务类名称
     */
    private String klassName;
    /**
     * 服务别名
     */
    private String alias;
    /**
     * 方法名称
     */
    private String methodName;
    /**
     * 方法参数类型
     */
    private Class<?>[] parameterKlassNameArrays;
    /**
     * 方法参数值
     */
    private Object[] parameterArrays;
    
}

客户端设置好参数后格式化为json格式传输

JSONObject.toJSONString(message)

然后服务端接收到后解析

Message message = JSONObject.parseObject(msg, Message.class);

然后按照对应字段继续执行。

啊~破费

等等,服务端匹配服务还是写死名称的,嗯,有点low,不是,有点不合适~~

服务实现自动加载

我们可以使用注解标注需要加载的实现类,代码如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {

    String name() default "";
}

@Service
public class DemoServiceImpl implements DemoService {

    public String sayHi(String name) {
        return "hi " + name;
    }
}

那现在就需要扫描到这个类,并把这个类保存下来。

从根路径开始扫描

String root = URLDecoder.decode(RPCServer.class.getResource("/").getPath(), String.valueOf(Charset.defaultCharset()));
private static void scan(String root) {

        System.out.println("start scan");
        File file = new File(root);
        allFiles(file, root);
        loadImpl();
    }
    
private static void allFiles(File file, String root) {

        if (file.isDirectory()) {

            File[] files = file.listFiles();
            if (files == null)
                return;

            for (File f : files) {

                if (f.isDirectory())
                    allFiles(f, root);
                else {
                    String path = f.getAbsolutePath();
                    Context.INSTANCE.addFile(handlePathToClass(path, root));
                }
            }
        }
    }

private static void allFiles(File file, String root) {

        if (file.isDirectory()) {

            File[] files = file.listFiles();
            if (files == null)
                return;

            for (File f : files) {

                if (f.isDirectory())
                    allFiles(f, root);
                else {
                    String path = f.getAbsolutePath();

                    Context.INSTANCE.addFile(handlePathToClass(path, root));
                }
            }
        }
    }

整体思路就是从根路径开始,如果碰到目录则搜索目录下文件,然后把所有的文件路径记录下来。
handlePathToClass是用来处理路径为类全限定名

private static String handlePathToClass(String path, String root) {

        path = path.substring(root.length());
        path = path.replace('/', '.');
        return path.substring(0, path.length() - ".class".length());
    }

这里的Context是我们创建的上下文,用于记录扫描信息(代码在git上看吧,不贴了)。
扫描完所有类之后,我们对扫描结果进行处理,将其中标注了servic注解的类放到Context中。

到这里服务端的东西就差不多了。再来看看客户端

动态代理

目前客户端使用一个静态代理来代理接口,静态的缺点很明显,就是。。。不高端,作为一个高大上的框架,怎么能用静态的,必须动态的。
使用Java自带的动态代理实现目标接口的代理类,在代理类中实现远程调用服务端服务逻辑,然后神不知鬼不觉的把结果返回给客户端。
主要代码如下

public class RemoteProxy<T> implements InvocationHandler {

    private Class<T> klass;
    private String alias;

    public RemoteProxy(Class<T> klass, String alias) {

        this.klass = klass;
        this.alias = alias;
    }


    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Message message = new Message();
        message.setKlassName(klass.getName());
        message.setAlias(alias);
        message.setMethodName(method.getName());
        message.setParameterKlassNameArrays(method.getParameterTypes());
        message.setParameterArrays(args);

        return RPCClient.sendMsg(JSONObject.toJSONString(message));
    }
}

public class ServiceFactory {

    public static <T> T createService(Class<T> klass, String alias) {

        if (klass == null)
            return null;

        RemoteProxy<T> rp = new RemoteProxy<T>(klass, alias);
        Object subject = Proxy.newProxyInstance(rp.getClass().getClassLoader(), new Class[]{klass}, rp);

        return (T) subject;
    }
}

然后客户端就可以非常简单的远程调用了

DemoService service = ServiceFactory.createService(DemoService.class, "multi");
System.out.println(service.sayHi("haha"));

一个服务多个实现

这种需求还是比较常见的,客户端指定需要使用的服务别名就可以使用不同的服务。在我们的Service注解中有name字段,该字段就标注了该服务的别名。

@Service(name = "multi")
public class MultiDemoServiceImpl implements DemoService {

    @Override
    public String sayHi(String name) {
        return "multi " + name;
    }
}

然后在扫描类之后的加载服务阶段会根据name字段作为查找类的key的一部分。

Key key = new Key(c.getName());
Service service = (Service) klass.getDeclaredAnnotation(Service.class);
if (StringUtil.isNotEmpty(service.name())) {

    key.setAlias(service.name());
}
Context.INSTANCE.addServiceImpl(key, path);

然后客户端只需要在调用的时候指定alias就好了。

鼓掌~.~

源码

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

推荐阅读更多精彩内容