一、前言
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。