手撸一个RPC框架

原文来自公众号 三不猴子

如何调用他人的远程服务?

由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:


image.png
  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这些步骤都封装起来,让用户对这些细节透明。

1. 怎么做到透明化远程服务调用?

怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时还是选择动态代理方式。我们这个最简易版的自然也是采用动态代理的方式。

2. 怎么对消息进行编码和解码?

2.1 确定消息数据结构

  1. 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;
  2. 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;
  3. 参数类型&参数值参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class)以及相应的参数值;超时时间

2.2 序列化

从RPC的角度上看,主要看三点:1)通用性,比如是否能支持Map等复杂的数据结构;2)性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;3)可扩展性,对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。我们的是最简易版所以就采用了jdk序列化的方式来处理。

开始撸代码

  1. 初始化工程

    首先创建2个项目分别是server和client;server项目下两个模块分别是rpc-server-api和rpc-server-provider。

    为什么server项目要创建两个模块?

client在调用服务端的服务时需要知道服务端的一些信息,client可以依赖于这个模块。我们的项目中SDK和契约包就是提供了这个功能。而真正的实现是放在rpc-server-provider中。

  1. rpc-server-api
public interface IHelloService {
    String sayHello(String content);

    String saveUser(User user);
}

请求参数类

private String className;
private String methodName;
private Object[] parameters;
  1. rpc-server-provider

    首先rpc-server-provider是依赖rpc-server-api的。我们写一个实现类,来实现api中定义的接口。

    public class HelloServiceImpl implements IHelloService{
       
        @Override
        public String sayHello(String content) {
            System.out.println("request in sayHello:"+content);
            return "Say Hello:"+content;
        }
    }
    

    我这么写好了实现远程要怎么才能调用的到呢?我们还需把服务暴露出去,那就需要一个服务暴露的方法。这里就是不断去接受请求,每一个socket交给一个processorHandler来处理。

public class RpcProxyServer {
    ExecutorService executorService = Executors.newCachedThreadPool();
    public void publisher(Object service, int port) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
            while (true) {//不断接受请求
                Socket socket = serverSocket.accept();//BIO
                //每一个socket 交给一个processorHandler来处理
                executorService.execute(new ProcessorHandler(socket, service));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

具体processorHandler的代码,从socket中获取请求对象,并是使用请求对象调用本服务方法,同时返回方法执行结果,将返回结果写入socket中。

public class ProcessorHandler implements Runnable {
    private Socket socket;
    private Object service;
    
    public ProcessorHandler(Socket socket, Object service) {
        this.socket = socket;
        this.service = service;
    }

    @Override
    public void run() {
        try (InputStream inputStream = socket.getInputStream();
             ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
            //输入流中应该有什么东西?
            //请求哪个类,方法名称、参数
            RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
            Object result = invoke(rpcRequest); //反射调用本地服务
            objectOutputStream.writeObject(result);
            objectOutputStream.flush();
        } catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private Object invoke(RpcRequest request) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //反射调用
        Object[] args = request.getParameters(); //拿到客户端请求的参数
        Class<?>[] types = new Class[args.length]; //获得每个参数的类型
        for (int i = 0; i < args.length; i++) {
            types[i] = args[i].getClass();
        }
        Class clazz = Class.forName(request.getClassName()); //跟去请求的类进行加载
        Method method = clazz.getMethod(request.getMethodName(), types); //sayHello, saveUser找到这个类中的方法
        return method.invoke(service, args);
    }
}

大功告成,把服务发布出去。

/**
 * Hello world!
 *
 */
public class App {
    public static void main( String[] args ){
       IHelloService helloService=new HelloServiceImpl();
       RpcProxyServer proxyServer=new RpcProxyServer();
       // 发布到8080端口
       proxyServer.publisher(helloService,8080);
    }
}
  1. 客户端代码开撸。我们现在在客户端依赖了服务端的api(SDK、契约包)如何才能实现调用远程方法呢?类似于服务端代理类。

    public class RpcProxyClient {
        public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
            return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(),
                    new Class<?>[]{interfaceCls},new RemoteInvocationHandler(host,port));
        }
    }
    
    public class RemoteInvocationHandler implements InvocationHandler {
    
        private String host;
        private int port;
    
        public RemoteInvocationHandler(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        @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.setParameters(args);
            //远程通信
            RpcNetTransport netTransport=new RpcNetTransport(host,port);
            Object result=netTransport.send(rpcRequest);
    
            return result;
        }
    }
    

    处理网络传输的类

    public class RpcNetTransport {
    
        private String host;
        private int port;
    
        public RpcNetTransport(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        public Object send(RpcRequest request) {
            Object result = null;
            try (//建立连接
                 Socket socket = new Socket(host, port);
                 //网络socket
                 ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                 ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
                
                outputStream.writeObject(request); //序列化()
                outputStream.flush();
                result = inputStream.readObject();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return result;
        }
    

    最后使用这个远程调用:

    public class App {
        public static void main(String[] args) {
            RpcProxyClient rpcProxyClient = new RpcProxyClient();
    
            IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost",8080);
         }
    }
    

最后画一张来总结一下整个流程

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

推荐阅读更多精彩内容