天天看點

RPC 整理RPC-Reomto proceducer call 遠端過程調用 基于 java

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的調用鍊

RPC 整理RPC-Reomto proceducer call 遠端過程調用 基于 java

這裡我們關注的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服務。