RPC-Reomto proceducer call 远程过程调用 基于 java
一下主要基于自己的理解、这里我将http调用也视为一种rpc调用。
远程 这里远程应该相对于进程而言,甚至可能是线程。
过程调用 简单地说就是执行代码(服务、业务逻辑)。
远程协议(姑且这么叫) 既然要远程,势必设计两端之间的通讯,在最通用化的假设下,该协议一般就是网络协议。所以相关的远程协议一般设计TCP/IP(Socket通讯),http通讯(主流的rest Api就可以看成是一种远程调用),以及基于TCP/IP协议族的自定义协议。
调用的核心 从调用的时间流来看,调用的核心主要就是告诉目标服务要请求的服务,要传递的参数、以及客户端取得最终的执行结果。
RPC的实现主要的两种类型 一种以http为主,纯粹的http请求,返回结果,还有一种以TCP/IP为主,类似于实现一个服务代理,屏蔽网络请求,就像纯粹的本地调用一样(当然也可能是基于http协议封装的框架、如spring cloud的Fetch技术)。
RPC的实现 面对单一语言的RPC是相对简单的,对于要跨语言的最主要的是要保持调用接口的同意。
RPC、RMI、SOAP、REST RPC强调的的是端之间的调用和结果返回;RMI可以说是RPC的一种实现,是一种用面向对象技术实现的RPC技术;SOAP 简单对线访问协议,强调协议性和服务;REST(Representational State Transfer)一种特殊的RPC,既可以是RPC,也可以不是,一种网络应用的架构设计风格,可以认为是一种轻量级的SOAP,弱化了协议的要求、使用一种类似于约定的协议模式。
微服务与远程调用的关系 微服务的一个核心技术就是服务调用链,微服务中存在着大量的服务调用,服务间调用根据距离度量可以使用Eventbus、RPC、http等调用,Eventbus主要的适合的场景还是应用内调用,当然也有把远程调用封装成Eventbus形式的,RPC和http(rest)基本是同级的,甚至在有些概念下是有部分重合的。http相对于RPC协议的优点是目前为止大部分的服务都是以http形式出现的,http协议拥有最为出色的跨平台跨协议和历史性,开发部署十分简单。而RPC的主要优点就是效率和调用,大部分的RPC框架都基于TCP、UDP协议来实现,因此相比http,RPC框架往往拥有更高的效率,其次是调用问题,大部分RPC框架都是通过类似代理的技术手段,屏蔽了远程调用,使远程调用看起来像本地调用一样。
分布式与微服务的关系 分布式服务服务其实也是一种微服务,只不过微服务是一种架构理念、更强调的是服务的粒度化、服务的编排,以一个一个服务为设计开发的单位,由于概念更抽象,其事务性质可能很难保障,而分布式系统主要是一个系统,往往是分布式系统中所有的微服务都是为一个核心产品的实现服务,其服务间调用可能更要强调事务性。
主流远程框架 Thrift、谷歌的基于ProtoBuf的gRPC、Dubbo、Hessian、java RMI。
主流框架简介
对于大部分远程框架、技术,其实现理念总是实现一个服务接口,然后在背调服务上实现服务的实现,通过一些技术手段,创建一个接口的实现类,通过该类进行远程调用,对于更通用的远程服务,可能还会实现一个类似于注册中心的东西,用于注册和发现服务,与CS中的服务器和客户端不同,在RPC中客户端和服务端的地位是相同的,他们可以相互提供服务,其实有时候还是应该不同,至少从表现上来看是这样子的。
java RMI
RMI使用java的远程消息交换协议(JRMP-Java Remote Message Protocol)实现远程协议,只能在java语言开发的系统间进行RPC。所有的java参数、返回值都必须是可序列化的(这里把基本数据类型也视为可序列化)。首先看一下RMI的调用链
这里我们关注的rmi核心是Sub,Sub主要的工作是暴露服务端提供的接口,然后在调用的时候帮你屏蔽网络细节,让你可以像本地调用一样调用远程服务。Sub的消息通过向下传输、网络传输后最终到达Skeleton,然后Skeleton调用Server上的真正实现。
要实现一个RMI调用,基础步骤如下:
1、创建你的服务接口、实现你的服务。
2、编写服务端、客户端程序。
3、注册服务、远程调用。
首先来实现一个Echo服务
// service
public interface EchoService extends Remote {
String echo(String name) throws RemoteException;
}
// service impl
public class EchoServiceImpl extends UnicastRemoteObject implements EchoService {
public EchoServiceImpl() throws RemoteException {
super();
}
public String echo(String name) throws RemoteException {
return String.format("hello %s", name);
}
}
服务实现的唯一不同就是要实现一个远程接口 Remote代表一个远程服务注册到Naming中心,其实现要继承UnicastRemoteObject这样可以保证客户端可以获取到这个对象的存根
// server
public class Server {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/echo", new EchoServiceImpl());
} catch (RemoteException | MalformedURLException | AlreadyBoundException e) {
e.printStackTrace();
}
}
}
// client
public class Client {
public static void main(String[] args) {
try {
EchoService echoService = (EchoService) Naming.lookup("rmi://localhost:1099/echo");
System.out.println(echoService.echo("jsen"));
} catch (RemoteException | NotBoundException | MalformedURLException e) {
e.printStackTrace();
}
}
}
这里没有看到Stub和Skelton,因为在jdk1.4后rmi使用动态代理来实现RPC技术。
自己使用Sub、Skelton方法实现(RMI)远程调用
// EchoServiceSub
public class EchoServiceSub implements EchoService {
private Socket socket;
public EchoServiceSub(Socket socket) {
this.socket = socket;
}
@Override
public String echo(String name) {
try(Socket s = socket) {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(s.getOutputStream());
objectOutputStream.writeObject("echo##" + name);
objectOutputStream.flush();
return new ObjectInputStream(s.getInputStream()).readObject().toString();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
// EchoServiceSkeleton
public class EchoServiceSkeleton extends Thread {
private EchoService echoService;
private EchoServiceSkeleton(EchoService echoService) {
this.echoService = echoService;
}
@Override
public void run() {
try {
var serverSocket = new ServerSocket(8080);
var socket = serverSocket.accept();
while (socket != null) {
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
String parameter = objectInputStream.readObject().toString();
String[] parameters = parameter.split("##");
String method = parameters[0];
String parm = parameters[1];
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
if ("echo".equals(method)) {
objectOutputStream.writeObject(echoService.echo(parm));
} else {
objectOutputStream.writeObject("no method found");
}
objectOutputStream.flush();
socket = serverSocket.accept();
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws RemoteException {
new EchoServiceSkeleton(new EchoServiceImpl()).start();
}
}
// ClientSub
public class ClientSub {
public static void main(String[] args) {
try {
EchoService echoService = new EchoServiceSub(new Socket("localhost", 8080));
System.out.println(echoService.echo("jack"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用动态代理实现(RMI)
自己实现代理就是直接通过客户端参数在服务器上动态构造实现类,执行远程命令,客户端可以使用动态代理来屏蔽网络调用的细节,我们还可以利用spring的BeanFactory来实现服务端实现类的管理。
这里我们首先定义一个Protocol类来封装请求:
// Protocol
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@AllArgsConstructor
public class Protocol implements Serializable {
private Class<?> serviceImplName;
private String methodName;
private Object[] params;
Class<?>[] paramTypes;
}
服务端核心:创建实现类,执行方法,返回结果
// Handler
public class Handler extends Thread {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(Socket s = socket) {
ObjectInputStream objectInputStream = new ObjectInputStream(s.getInputStream());
Protocol protocol = (Protocol)objectInputStream.readObject();
Object result = protocol.getServiceImplName().getDeclaredMethod(protocol.getMethodName(),
protocol.getParamTypes()).invoke(protocol.getServiceImplName().getConstructor().newInstance(),
protocol.getParams());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(s.getOutputStream());
objectOutputStream.writeObject(result);
} catch (IOException | ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
e.printStackTrace();
}
}
}
客户端核心:动态代理屏蔽网络细节,这里没有创建一个实现类,只是实现了一个EchoService的Proxy,调用EchoService的每个方法(API),会自动封装参数,请求网络。
// Proxy
public class Proxy<T> {
public T createProxy(Class<?> serviceImplName, InetSocketAddress inetSocketAddress) {
return (T) java.lang.reflect.Proxy.newProxyInstance(serviceImplName.getClassLoader(), new Class<?>[]{serviceImplName.getInterfaces()[0]}, (proxy, method, args) -> {
Socket socket = new Socket();
socket.connect(inetSocketAddress);
try(Socket s = socket) {
Protocol protocol = new Protocol(serviceImplName, method.getName(), args, method.getParameterTypes());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(s.getOutputStream());
objectOutputStream.writeObject(protocol);
ObjectInputStream objectInputStream = new ObjectInputStream(s.getInputStream());
return objectInputStream.readObject();
}
});
}
}
服务端:直接简单粗暴的socket while true
// ServerProxy
public class ServerProxy extends Thread {
private int port;
public ServerProxy(int port) {
this.port = port;
}
@Override
public void run() {
try {
var serverSocket = new ServerSocket(port);
while (true) {
new Handler(serverSocket.accept()).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ServerProxy(8080).start();
}
}
客户端:
// ClientProxy
public class ClientProxy {
public static void main(String[] args) throws RemoteException {
EchoService echoService = new Proxy<EchoService>().createProxy(EchoServiceImpl.class, new InetSocketAddress("localhost", 8080));
System.out.println(echoService.echo("proxy"));
}
}
Thrift、gRpc
thrift最早由facebook于2007年开发,是一个跨语言的RPC框架,其主要支持的语言有http://thrift.apache.org/docs/Languages,由于是一个跨语言的,因此就必须要定义一个特殊的格式(protocol)来起到连接各种语言消除差异的媒介,因此,包括gRPC也是这样,都先要定义IDL语言,由IDL来生成各种语言下的适配实现。
1、生成一个最简单的echo.thrift IDL
namespace java com.jsen.test.thrift.service
// 定义服务
service EchoService {
string echo(1:string name)
}
2、编译IDL文件 thrift -r -gen java echo.thrift 调用该命令会生成一个gen-java的包,该包下会有一个与服务同名的EchoService.java,这个文件是thrift的核心,该类下有三个子类Client,Processor,IFace机器异步实现Async类,其中IFace类是服务接口类,Client和Processor分别是客户端类和服务端类,我们要把实现逻辑写在IFace的实现类里面,因此接下来写一个Echo实现类。
public class EchoServiceImpl implements EchoService.Iface {
public String echo(String name) throws TException {
return String.format("hello %s", name);
}
}
public class Server {
public static void main(String[] args) {
try {
TProcessor processor = new EchoService.Processor<EchoService.Iface>(new EchoServiceImpl());
TServerSocket serverSocket = new TServerSocket(8080);
TServer.Args tArgs = new TServer.Args(serverSocket);
tArgs.processor(processor);
tArgs.protocolFactory(new TBinaryProtocol.Factory());
TServer tServer = new TSimpleServer(tArgs);
tServer.serve();
} catch (TTransportException e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
try(TSocket tSocket = new TSocket("localhost", 8080)) {
TProtocol tProtocol = new TBinaryProtocol(tSocket);
EchoService.Client client = new EchoService.Client(tProtocol);
tSocket.open();
System.out.println(client.echo("thrift"));
} catch (TException e) {
e.printStackTrace();
}
}
}
3、上面代码中有两个重要概念TTransport和TProtocol,TTransport是定义传输的,TProtocol是协议层提供了各种默认的传输协议,也可以自定义传输协议,常见的有TBinaryProtocol、TJSONProtocol、TDebugProtocol等协议。因此thrift支持多种传输方式和传输协议。
4、还有一个概念叫TServer,定义的是服务类型,这里的TSimpleServer是一个单线程的服务,一般用于测试,TThreadPoolServer和TNonblockingServer分别是多线程下的阻塞和非阻塞IO服务。