8分钟教你写个RPC框架——初中生都能看懂的RPC教程
上一篇理论篇,《初中生都能看懂的RPC教程——3分钟让你搞明白RPC之理论篇》,用三分钟时间讲明白了RPC是干啥的,这一篇实践篇,就为大家展示怎么实现一个RPC,具体原理是啥。
上一篇讲到 计算机A 想去调用 计算机B 的dog.run()方法,具体是怎么做的呢?请看下面:
原理
一个完整的RPC流程,可以用下面这张图来描述:
左边的Client,对应的就是前面的计算机A,而右边的Server,对应的则是计算机 B。``
- Client
- 在 计算机A 的Application代码中,调用了接口Dog的一个实现类的run方法,希望执行一个小狗跑的动作;
- 这个Dog实现类,内部并不是直接实现小狗跑的动作,而是通过远程调用 计算机B 的RPC接口,来获取运算结果,称之为Stub;
- Stub如何与 计算机B 建立远程通讯呢?这时候就要用到远程通讯工具了,也就是图中的Run-time Library,我们可以用Java的Socket,也可以用基于Http协议的HttpClient,或者其他通讯工具类;
- Stub通过调用Socket和计算机B建立起了通讯,然后将请求数据(方法名run、参数100)发送给计算机B。注意,底层通信用的是二进制,我们需要把数据序列化成二进制。
- Server
- 计算机B 收到A发过来的二进制数据,反序列化,也就是计算机B的Run-time Library;
- 计算机B 将数据Stub处理,其实Stub并没有做什么,只是解析数据,发现A想调用自己的dog.run(),,参数是100,那么就调用自己的dog.run(100)方法;
- 计算机B 执行完dog.run(100)方法,将结果发送给 计算机A,怎么发?同样的流程,只不过现在B变成了Client,A变成了Server。
实践,代码
- Client端,即计算机A
Client在应用层发起RPC请求,CilentApp:
public class CilentApp {
public static void main(String[] args) {
Dog dog = new DogRemoteImpl();
//让小狗跑100米,像调用本地方法一样,实际上底层用了RPC调用了另一台计算机上的run()方法
String result = dog.run(100);
System.out.println(result);//将收到的结果打印出来
}
}
DogRemoteImpl是接口Dog的实现类,我们把RPC实现逻辑封装进去了,这样在Client端感知不到RPC的存在,感觉好像是直接调用dog.run();
再来看看DogRemoteImpl类,它实现了接口Dog,
public class DogRemoteImpl implements Dog {
public String run(int length) {
String ipAddress = "192.168.1.167";//服务器的ip地址
int port = 55566;//服务器计算机上,对应的服务器程序的端口
try {
Socket socket = new Socket(ipAddress, port);
// 将请求序列化
DogRpcRequest dogRpcRequest = new DogRpcRequest("run",length);
OutputStream out = socket.getOutputStream();//socket输出管道
//序列化后的内容会传送到 socket输出管道上
ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
// 将dogRpcRequest对象序列化,并将序列化后的内容传到socket输出管道上,通过网络发送出去
objectOutputStream.writeObject(dogRpcRequest);
// 等待服务器发来结果,并将结果反序列化
InputStream in = socket.getInputStream();//socket输入管道,接受传进来的网络信息
//收到的内容会通过in传到objectInputStream上
ObjectInputStream objectInputStream = new ObjectInputStream(in);
Object response = objectInputStream.readObject();//对收到的内容反序列化
if (response instanceof String) {
return (String) response;
} else {
throw new InternalError();
}
} catch (Exception e) {
throw new InternalError();
}
}
}
用Socket进行远程通信,用ObjectOutputStream来序列化对象,用ObjectInputStream实现反序列化,以便接受识别服务器发来的结果,注释写的都很清楚;DogRpcRequest 里面封装了Client端想要请求的方法名和参数值;
再来看看Dog接口和DogRpcRequest 类:
public interface Dog {//接口
public String run(int length);
}
//DogRpcRequest实现了Serializable接口,只有这样该对象才能序列化
public class DogRpcRequest implements java.io.Serializable{
public String method;
public int length;
public int getLength() {
return length;
}
public String getMethod() {
return method;
}
public DogRpcRequest(String m,int length){
this.length = length;
method = m;
}
}
- Server端,即计算机B
来看看server端怎么实现的,和Cilent很类似,ServerApp :
public class ServerApp {
private Dog dog = new DogImpl();//真正的run()方法在这里面
public static void main(String[] args) throws IOException {
new ProviderApp().run();
}
private void run() throws IOException {
ServerSocket listener = new ServerSocket(55566);
try {
while (true) {
System.out.println("我在监听了...");
Socket socket = listener.accept();//监听
try {
//服务器端收到内容了,并反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object object = objectInputStream.readObject();
//判断客户端想调用哪个函数
String result = null;
if (object instanceof DogRpcRequest) {
DogRpcRequest dogRpcRequest = (DogRpcRequest) object;
if ("run".equals(dogRpcRequest.getMethod())) {//确定想调用run()函数
result = dog.run(dogRpcRequest.getLength());//调用真正的run
} else {
throw new UnsupportedOperationException();
}
}
//将结果序列化,再传回客户端
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(result);
} catch (Exception e) {
System.out.println("fail " + e);
} finally {
socket.close();
}
}
} finally {
listener.close();
}
}
}
Server端先用ServerSocket.accept()监听端口,收到请求后,马上反序列化请求->执行->序列化执行结果,最后将二进制格式的执行结果返回给Client;
其中,DogImpl类也是接口Dog的实现类,这里面的run()方法就是Cilent梦寐以求的那个run(),Server帮Cilent调用完了,然后把结果返回给Cilent;
DogImpl类:
public class DogImpl implements Dog{
//计算机B上的run()方法,真正的run()方法,A梦寐以求的东西
public String run(int length) {
return "我跑到终点了,跑了" + length + "米";
}
}
这样,我们就实现了一个很简单的RPC实例。
最后的思考
我们的这个RPC太简陋了,主要有以下问题:
-
Server端是单线程
万一A正在请求调用B上的run,突然又来了个C也想调用B上的run,这时Server就处理不过来了,是否可以考虑在Server端使用多线程来实现,线程池? -
通用性太差
小狗跑,我们给接口Dog实现了DogRemoteImpl,难道以后有小猫游、小熊飞,我们还要重新写吗?有没有通用的模式呢?值得思考,提示:Dubbo通过和Spring的集成,用到了@Reference注解; -
长连接与短连接、负载均衡…
其实问题有很多,想要实现一个高效稳定的RPC框架,可不是我们随随便便就能写出来的,要考虑的细节和问题有很多,有兴趣的同学可以研究下开源RPC框架,比如:Dubbo、Motan、gRPC等。