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的調用鍊
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2Lc1TPBR2b1clW6BnMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2LcRHelR3LcJzLctmch1mclRXY39jNwUDMxgDMyETNxkDM4EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
這裡我們關注的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服務。