手撕RPC系列(1)—最原始的RPC通俗理解

一、前言

RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。网上太多博文五花八门,一上来就netty、grpc、thrift、Protobuf、单体架构、分布式架构…一堆听不懂的名词,可能有些博主自己都不清楚什么情况,导致读者半天也搞不清rpc的本质。这里,我们从最最基础的东西开始讲起,本系列教程会一步一步演化,抽丝剥茧的从本质讲起。

二、原理

在这里插入图片描述

同一个jvm进程中,想要调用其它类的方法我们都知道直接调用就行了,如果是不同机器之间的进程,需要相互调用对方的方法该怎么办呢?答:需要网络通信实现。网络通信的本质是什么呢?答:二进制的传输。所以,RPC的本质是二进制的网络传输。类似的原理还有http方式,都是客户端通过某些协议调用了服务端的方法。上图的1-10步,是一个标准的rpc流程图,你在网上看到的都是这个图,我们今天要讲的,是最原始版的rpc,先忽略图中红色的部分,现在不需要知道那是干什么的。

三、前置基础

socket编程:socket的编程是网络编程的基础,如果不具备此基础,不建议往下读。

我们通过socket编程,实现网络间的二进制字节数组(ByteArray)传输,实现下图中2和5的步骤。

四、例子

在这里插入图片描述

这个图是不是比上面的简单多了?先知道1-6步的具体流程,我们再来写例子。步骤少了,但它依然是个rpc,少了一堆干扰的东西,理解起来更容易。

代码结构:

common类:

Student.java #实体类,作为数据的传输和传入的对象

StudentService.java #接口类,定义一个findStudentByid接口,给客户端调用

StudentServiceImpl.java #接口实现类,实现findStudentByid接口,必须在服务端实现

rpc类:

Client.java #客户端类

Server.java #服务端类

以下是代码:

Student.java

package common;

public class Student {
    int id;
    String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

StudentService.java :客户端要通过rpc调用服务端的方法,需要调用的是接口,而不是实现,实现的活由Server端去做

package common;

public interface StudentService  {
    public Student findStudentByid(int id) throws Exception;
}

StudentServiceImpl.java :服务端要实现客户端请求的接口类,把数据查出来,再返回给客户端,这里为方便,把数据写死,实际中可能是数据库中查询

package common;

public class StudentServiceImpl implements StudentService {
    @Override
    public Student findStudentByid(int id) {
        return new Student(id,"zhangsan");
    }
}

咱们重点看下面两个类,是rpc的核心

Client.java

package rpc;

import common.Student;
import common.StudentService;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws Exception {
        // 创建Socket类,指定ip和端口
        Socket socket = new Socket("127.0.0.1", 8888);
        // ByteArrayOutputStream用于写出字节数组,上面说了,rpc的本质是二进制字节数组的传输
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        // DataOutputStream用于客户端写出数据,这里是具体值,最后会把123传给服务端
        DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

        // 接口实现的过程相当于调用Server端的方法,这里不要理解成Client的实现
        StudentService studentService = new StudentService() {
            @Override
            public Student findStudentByid(int id) throws Exception {
                dataOutputStream.writeInt(id);

                // 这里byteArrayOutputStream.toByteArray()就是把流转换成字节数组传到服务端,里面包含了123对应的字节数组
                socket.getOutputStream().write(byteArrayOutputStream.toByteArray());
                // 执行flush,才真正触发字节数组的传输
                socket.getOutputStream().flush();

                // 调用完Server端的方法后,这里获取Server端返回的流
                DataInputStream dis = new DataInputStream(socket.getInputStream());
                // 从流里面获取Server端返回的具体值
                int sid = dis.readInt();
                String name = dis.readUTF();
                Student student = new Student(sid, name);
                return student;
            }
        };

        // Client端调用接口,从Server端获取返回的数据
        Student student = studentService.findStudentByid(123);
        // 打印
        System.out.println(student);
        // 关闭流
        dataOutputStream.close();
        // 关闭socket,表示整个rpc流程结束
        socket.close();
    }
}

Server.java

package rpc;

import common.Student;
import common.StudentService;
import common.StudentServiceImpl;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws Exception {
        // 服务端监听8888端口,跟客户端统一
        ServerSocket serverSocket = new ServerSocket(8888);
        while (true){
            // 接受客户端传过来的字节码
            Socket socket = serverSocket.accept();
            // 服务端的具体处理过程
            process(socket);
            socket.close();
        }
    }
    public static void process(Socket socket) throws Exception {
        InputStream input = socket.getInputStream();
        OutputStream output = socket.getOutputStream();
        DataInputStream dataInputStream = new DataInputStream(input);
        DataOutputStream dataOutputStream = new DataOutputStream(output);

        // 这里获取客户端传过来的id,也就是123
        int id = dataInputStream.readInt();
        // 实例化客户端接口的实现类StudentServiceImpl
        StudentService service = new StudentServiceImpl();
        // 服务端执行查询数据的逻辑
        Student student = service.findStudentByid(id);
        // 最后把查询的数据写回给客户端
        dataOutputStream.writeInt(student.getId());
        dataOutputStream.writeUTF(student.getName());
        dataOutputStream.flush();
    }
}

最后先运行Server.java,再运行Client.java,输出:

Student{id=123, name='zhangsan'}

五、总结

本次示例其实就是一个socket编程,建立在能更好理解rpc的基础上,实现一个阉割版的rpc实例。实际工作中不可能会这样去做。这种最原始的方式也是槽点多多,比如要在Student加一个字段,在接口加一个方法,无论是客户端还是服务端改动都是巨大的。下一节,将引入stub的概念,进一步优化流程,也就是本文最上面的图片,也是标准版的rpc。

发布了10 篇原创文章 · 获赞 12 · 访问量 8169

猜你喜欢

转载自blog.csdn.net/u013289115/article/details/105524784
RPC