RPC其实就是远程程序调用,即Remote Procedure Call,相对于远程程序调用,其实最常见的调用就是本地函数调用,比如main函数中调用两数相加的函数,实现两数相加,这种形式在单体应用中,当然是很常见的,但是对于在分布式的场景下,就无法满足了,比如主机A总是想要主机B上的某些服务,就可以利用远程调用的方式。
但是很容易想到的是,我们可以直接HTTP请求啊,但是利用httpclient需要对于参数这些的设定,无法将模块抽象出来,而RPC则就是要解决远程调用像在本地调用一样简单。
所以,RPC要解决的问题就是:
1、解决分布式环境下,服务之间调用的问题
2、远程调用用,像本地调用一样方便。
RPC其实就相当于一个远程代理
以上就是一个RPC远程调用的示意图,client中的Application通过Client Stub实现远程调用,client Run-Time Library是实现远程调用的工具包,可以是java的socket,也可以是httpclient,因为传输的数据是二进制的,因此需要进行序列化以及反序列化。
AddService:
public interface AddService {
String add(double a,double b);
}
AddServiceImpl:
public class AddServiceImpl implements AddService {
@Override
public String add(double a, double b) {
return String.valueOf(a+b);
}
}
Server:
public interface Server {
public void stop();
public void start() throws IOException;
public void register(Class serviceInterface,Class impl);
public boolean isRunning();
public int getPort();
}
ServiceCenter:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServiceCenter implements Server {
//声明线程池,一个可重用固定个数的线程池
private static ExecutorService executor= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static final HashMap<String,Class> serviceRegistry=new HashMap<String,Class>();
private static boolean isRunning=false;
private static int port;
public ServiceCenter(int port){
this.port=port;
}
//客户端的请求服务通过socket发送过来远程调用服务端接口并执行得结果
private static class ServiceTask implements Runnable{
Socket client=null;
public ServiceTask(Socket client){
this.client=client;
}
public void run(){
ObjectInputStream input=null;
ObjectOutputStream output=null;
try{
//将客户端发送的码流反序列化成对象,反射调用服务实现者,获得调用服务实现者,获得执行结果
input=new ObjectInputStream(client.getInputStream());
//服务名称
String serviceName=input.readUTF();
//方法名称
String methodName=input.readUTF();
//获得各种参数类型
Class<?>[] parameterTypes=(Class<?>[]) input.readObject();
//获得各种参数的值
Object[] arguments=(Object[])input.readObject();
//用服务名称寻得服务
Class serviceClass=serviceRegistry.get(serviceName);
if(serviceClass==null){
throw new ClassNotFoundException(serviceName+"not found");
}
//利用方法名和参数类型拿到该服务中的该方法
Method method=serviceClass.getMethod(methodName,parameterTypes);
//远程执行方法,实例化类,利用参数值获得执行结果
Object result=method.invoke(serviceClass.newInstance(),arguments);
//将执行结果反序列化,通过socket发送给客户端
output=new ObjectOutputStream(client.getOutputStream());
output.writeObject(result);
}catch (Exception e){
e.printStackTrace();
}finally {
if(output!=null){
try{
output.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(input!=null){
try{
input.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(client!=null){
try{
client.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
}
@Override
public void stop() {
isRunning=false;
executor.shutdown();
}
@Override
public void start() throws IOException {
//建立服务端的socket
ServerSocket server=new ServerSocket();
//绑定端口
server.bind(new InetSocketAddress(port));
System.out.println("start service");
try{
while(true){
executor.execute(new ServiceTask(server.accept()));
}
}finally {
server.close();
}
}
@Override
public void register(Class serviceInterface, Class impl) {
serviceRegistry.put(serviceInterface.getName(),impl);
}
@Override
public boolean isRunning() {
return isRunning;
}
@Override
public int getPort() {
return port;
}
}
RPCClient:
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.Socket;
public class RPCClient {
//第一个表示泛型,第二个表示返回的是T类型的数据
public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr) {
// 1.将本地的接口调用转换成JDK的动态代理,在动态代理中实现接口的远程调用
return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class<?>[]{serviceInterface},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
try {
// 2.创建Socket客户端,根据指定地址连接远程服务提供者
socket = new Socket();
socket.connect(addr);
// 3.将远程服务调用所需的接口类、方法名、参数列表等编码后发送给服务提供者
output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(serviceInterface.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
// 4.同步阻塞等待服务器返回应答,获取应答后返回
input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
} finally {
if (socket != null) socket.close();
if (output != null) output.close();
if (input != null) input.close();
}
}
});
}
}
@SpringBootApplication
public class RpcApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(RpcApplication.class, args);
new Thread(new Runnable() {
@Override
public void run() {
try{
//服务端开通一个端口提供对外服务
Server serviceServer=new ServiceCenter(8088);
//将add服务的相应的服务的名称以及服务类注入(接口类名以及实现类)
serviceServer.register(AddService.class, AddServiceImpl.class);
//开启对外服务
serviceServer.start();
}catch(IOException e){
e.printStackTrace();
}
}
}).start();
//建立客户端代理
AddService service= RPCClient.getRemoteProxyObj(AddService.class,new InetSocketAddress("localhost",8088));
//得到结果
System.out.println(service.add(3,4));
}
}
问:当存在多个不同的服务时是什么情况呢?
答:多个不同类型的服务,自然就是封装成不同的服务,对于服务端,将该服务再次注入到开启到对外服务的端口上,再次开启对外服务即可;而对于客户端,就需要制定一个新的动态代理实现该服务的远程调用。对于相同类型的服务,在服务端可以放在一个服务类中,用不同的方法名体现,这样在客户端就还是之前的动态代理,只主要用不同的方法调用即可。说了这么多,就是一个动态代理实则对应一个服务端的服务(一类方法)
演示如下:
AddService服务中存在add1以及add2服务,还存在不同类型的服务SayHiService
@SpringBootApplication
public class RpcApplication {
public static void main(String[] args) throws IOException {
SpringApplication.run(RpcApplication.class, args);
new Thread(new Runnable() {
@Override
public void run() {
try{
//服务端开通一个对外端口提供对外服务
Server serviceServer=new ServiceCenter(8088);
//将add服务的相应的服务的名称以及服务类注入(接口类名以及实现类)
serviceServer.register(AddService.class, AddServiceImpl.class);
//将sayHi服务注入
serviceServer.register(SayHi.class, SayHiImpl.class);
//开启对外服务
serviceServer.start();
}catch(IOException e){
e.printStackTrace();
}
}
}).start();
//建立客户端代理实现远程调用,指定调用远程服务的名称
AddService service= RPCClient.getRemoteProxyObj(AddService.class,new InetSocketAddress("localhost",8088));
SayHi sayHiService=RPCClient.getRemoteProxyObj(SayHi.class,new InetSocketAddress("localhost",8088));
//得到结果
System.out.println("8088调用两数相加服务:"+service.add1(3,4));//同一类服务的不同方法
System.out.println("8088调用两数相加服务:"+service.add2(3,4,5));
System.out.println("8088调用sayHi服务:"+sayHiService.sayHi());//不同类的服务
}
}
总归来说,就是如上图所示,在分布式环境下serviceB想要调用serviceA上的服务的时候,就需要建立一个那个远程服务的动态代理,就相当于本地调用,动态代理其实就将你的请求发给ServiceA的IP以及其开放的对外端口,实现服务的远程调用。一个动态代理对应一个服务,动态代理将想要调用的接口类、方法名以及参数列表进行编码后在调用的时候发送给服务提供者,并通过同步阻塞等待获得应答。而服务端需要把客户端发过来的进行反序列化,明确客户端调用哪个服务中的哪个函数以及相应的参数列表,在获得这些之后执行方法,将结果写入,返回给客户端。(具体的通信方式可以用socket也可以用httpcliient的方式)
rpc是分布式微服务架构中常见的通信方式,但是以上仅仅实现了一个简单的demo,具体怎么通信还是要进行设计,因为还有很多想不到的问题,比如:
1、假设serviceA又存在了一个新的服务,需要发布出去,就是让别人知道我有这个功能了,你们可以调用了。这个过程其实就是服务注册,可以用Zookeeper进行实现,在Dubbo中就是用Zookeeper做注册中心,实现服务的统一注册,可以获得服务的实例列表。
2、假设存在一个serviceB也有AddService服务,那对于serviceB来说,我只想要这个服务,具体是谁给我的并不重要,但是具体调用谁确实负载需要考虑的,也就是说需要存在一个中间件去平衡这个负载,但是对于serviceB是屏蔽的。
3、serviceB需要每次去注册中心查询实例列表吗,是不是可以考虑缓存呢…
4、异步调用,不是阻塞调用?版本控制?服务端关闭?
其实以上问题,对于微服务架构springcloud以及Dubbo都是需要考虑的,我希望我会学习到。。。
了一个新的服务,需要发布出去,就是让别人知道我有这个功能了,你们可以调用了。这个过程其实就是服务注册,可以用Zookeeper进行实现,在Dubbo中就是用Zookeeper做注册中心,实现服务的统一注册,可以获得服务的实例列表。
2、假设存在一个serviceB也有AddService服务,那对于serviceB来说,我只想要这个服务,具体是谁给我的并不重要,但是具体调用谁确实负载需要考虑的,也就是说需要存在一个中间件去平衡这个负载,但是对于serviceB是屏蔽的。
3、serviceB需要每次去注册中心查询实例列表吗,是不是可以考虑缓存呢…
4、异步调用,不是阻塞调用?版本控制?服务端关闭?
其实以上问题,对于微服务架构springcloud以及Dubbo都是需要考虑的,我希望我会学习到。。。
小小菜鸟,一堆问题…