RPC是遠程過程調用,對於java而言,就是兩個JVM通信,一個JVM a想要調用另一個JVM b中的類。b把執行結果在發送給a的過程。好,我們就是要來實現這個過程。
兩個接口:
public interface IDiff {
double diff(double a,double b);
}
public interface ISum {
public int sum(int a, int b);
}
兩個實現:
public class DiffImpl implements IDiff{
@Override
public double diff(double a, double b) {
return a - b;
}
}
public class SumImpl implements ISum {
public int sum(int a, int b) {
return a + b;
}
}
我們假設這兩個類在服務器上,客戶端沒有這兩個類,但客戶端還是想使用這兩個服務,所以我們可以通過網絡,把客戶端的想法,告訴服務器,我要使用那兩個類。而在這之前,服務器先要開啓這個服務。
服務端:
public class RPCServer {
private static final Logger LOG = LoggerFactory.getLogger(RPCServer.class);
private static final int threadSize = 10;
private final ExecutorService threadPool;
/**
*服務在一開始就是確定的
*不允許客戶端自動添加服務
*Key爲全限定接口名,Value爲接口實現類對象
*/
private final Map<String, Object> servicePool;
private final int port;
private volatile boolean stop;
public RPCServer(Map<String, Object> servicePool, int port) {
this.port = port;
this.threadPool = Executors.newFixedThreadPool(threadSize);
this.servicePool = servicePool;
}
/**
* RPC服務端處理函數 監聽指定TPC端口
* 每次有請求過來的時候調用服務,放入線程池中處理.
*/
@SuppressWarnings("resource")
public void service() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
while (!stop) {
final Socket socket = serverSocket.accept();
threadPool.execute(new Runnable() {
public void run() {
try {
process(socket);
} catch (Exception e) {
LOG.warn("Illegal calls",e);
} finally {
IOUtil.close(socket);
}
}
});
}
}
public void stop() {
stop = true;
threadPool.shutdown();
}
/**
* 調用服務 通過TCP Socket返回結果對象
* 有可能因爲客戶端的
* 類名錯誤
* 方法名錯誤
* 參數錯誤
* 而調用失敗
*/
private void process(Socket socket) throws Exception {
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Message message = (Message) in.readObject();
// 調用服務
Object result = call(message);
if(result == null){
LOG.warn("Without this service, the service interface is "+message.getInterfaceName());
IOUtil.close(socket);
return;
}
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(result);
IOUtil.close(socket);
}
/**
* 服務處理函數 通過包名+接口名
* 在servicePool中找到對應服務
* 通過調用方法參數類型數組獲取Method對象
* 通過Method.invoke(對象,參數)調用對應服務
*/
private Object call(Message message) throws Exception {
String interfaceName = message.getInterfaceName();
Object service = servicePool.get(interfaceName);
if (service == null) {
return null;
}
Class<?> serviceClass = Class.forName(interfaceName);
Method method = serviceClass.getMethod(message.getMethodName(), message.getParamsTypes());
Object result = method.invoke(service, message.getParameters());
return result;
}
}
開啓服務:
public static void main(String[] args){
Map<String,Object> servicePool = new HashMap<String, Object>();
// 先將服務確定好,才能調用,不允許客戶端自動添加服務
servicePool.put(ISum.class.getName(), new SumImpl());
servicePool.put(IDiff.class.getName(), new DiffImpl());
RPCServer server = new RPCServer(servicePool,8080);
try {
server.service();
} catch (IOException e) {
e.printStackTrace();
}
}
這是一個通用的消息格式,用於在client和server之間傳遞消息,
消息主要包括;客戶端需要調用的接口,方法名,參數類型
以及參數數組,這樣是爲了唯一確定調用的是哪個類的哪個方法,參數是什麼,以及什麼類型。就是通過反射調用的。
/**
* RPC調用條件
* 1.調用接口名稱 (包名+接口名)
* 2.調用方法名
* 3.調用參數Class類型數組
* 4.調用接口的參數數組
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
// 包名+接口名稱
private String interfaceName;
private String methodName;
private Class<?>[] paramsTypes;
private Object[] parameters;
public Message() {
}
public Message(String interfaceName, String methodName,
Class<?>[] paramsTypes, Object[] parameters) {
this.interfaceName = interfaceName;
this.methodName = methodName;
this.paramsTypes = paramsTypes;
this.parameters = parameters;
}
//setters and getters
}
上面是服務端的代碼,主要就是使用接口名作爲key,實現類作爲值;
使用反射調用方法,再把結果用java序列化的方式寫會客戶端。
好了,我們再來看看客戶端的實現。
/**
* @author root
*客戶端比較簡單,就是連接服務器
*然後用java序列化,把對象發送給服務器
*/
public class RPCClient {
// 服務端地址
private final String serverAddress;
// 服務端端口
private final int serverPort;
public RPCClient(String serverAddress, int serverPort) {
this.serverAddress = serverAddress;
this.serverPort = serverPort;
}
/**
* 同步的請求和接收結果
*/
public Object sendAndReceive(Message transportMessage) {
Object result = null;
Socket socket = null;
try {
socket = new Socket(serverAddress, serverPort);
// 反序列化 TransportMessage對象
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(transportMessage);
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
// 阻塞等待讀取結果並反序列化結果對象
result = in.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
IOUtil.close(socket);
}
return result;
}
public String getServerAddress() {
return serverAddress;
}
public int getServerPort() {
return serverPort;
}
}
客戶端請求服務器:
public class TestClient {
public static void main(String[] args) {
String serverAddress = "localhost";
int serverPort = 8080;
int count = 10;
final RPCClient client = new RPCClient(serverAddress, serverPort);
final Random random = new Random();
ExecutorService exe = Executors.newFixedThreadPool(count );
for (int i = 0; i < count; i++) {
exe.execute(new Runnable() {
public void run() {
Message transportMessage = null;
if(random.nextBoolean())
transportMessage = buildMessage();
else
transportMessage = buildMessage2();
Object result = client.sendAndReceive(transportMessage);
System.out.println(result);
}
});
}
exe.shutdown();
}
/**
* 創建一次消息調用
* @return
*/
public static Message buildMessage() {
String interfaceName = ISum.class.getName();
Class<?>[] paramsTypes = { int.class, int.class};
Object[] parameters = { 9, 3};
String methodName = "sum";
Message transportMessage = new Message(interfaceName,
methodName, paramsTypes, parameters);
return transportMessage;
}
public static Message buildMessage2() {
String interfaceName = IDiff.class.getName();
Class<?>[] paramsTypes = { double.class, double.class};
Object[] parameters = { 9.0, 3.0};
String methodName = "diff";
Message transportMessage = new Message(interfaceName,
methodName, paramsTypes, parameters);
return transportMessage;
}
}
大家可以看到客戶端其實很簡單:就是把消息發給服務器,然後接受調用結果。好了這就是一個rpc調用的過程,但是這個效率很低,低的原因主要是java序列化和阻塞的IO模型。在高併發的場景下,這種線程阻塞的方式,如果使用能適應高併發高效的NIO和更好的java序列化框架(比如protobuf),效果會更好。
先開啓服務器,在開啓客戶端,這是一次客戶端打印的結果: