RPC(簡單實現)




筆者之前僅看過RPC這個單詞,完全沒有了解過,不想終於還是碰上了。起因:這邊想提高並發量而去看kafka(最后折中使用了redis),其中kafka需要安裝ZooKeeper,而ZooKeeper又與分布式相關,再繼續就發現分布式的基礎是RPC,於是寫下了這篇博文


1. RPC

RPC(Remote Procedure Call)遠程過程調用,即通過網絡通信來調用遠程計算機程序上的服務,而這個調用過程就像調用本地方法一樣簡單透明,並且不需要了解底層的網絡技術協議。RPC采用C/S架構,發出請求的程序是Client,提供服務的則是Server,類似於Http請求與響應。簡單總結就是:調用的方法實際在遠程,而要像調用本地方法一樣簡單。


1)對於客戶端的我:調用本地的一個方法(存根)就能獲得服務。 這個存根是遠程服務的一個代理,其底層如何實現,對於我來說是透明的。

2)對於遠程服務器:監聽是否有連接過來,來了就調用對應的方法並返回(服務器端較易理解)


其結構圖如下:

  1. 用戶調用一個 “本地” 函數,該函數調用客戶句柄(遠程服務在本地的存根)
  2. 客戶句柄調用網絡通信來訪問遠程程序
  3. 遠程程序收到網絡通信及相關信息就調用服務句柄
  4. 服務句柄就調用服務函數,函數結束逆序返回結果完成一次遠程調用




2. 為什么需要RPC

當我們的業務量越來越龐大,垂直增加服務器的數量對提高性能的作用愈加微乎,此時難免會采用分布式的架構以便更好地提高性能。分布式架構的每個服務都是獨立的部分,當需要完成某項業務且依賴不同的服務式時,這些服務就需要互相調用,此時服務之間的調用就需要一種高效的應用程序之間的通訊手段了,這就是PRC出現的原因





3. RPC實現要求


3.1 服務提供方

提供服務:實現所提供的服務

服務暴漏:僅僅實現了服務是不夠的,還需要將提供的服務暴漏給外界,讓外界知道有何,如何使用服務


3.2 服務調用方

遠程代理對象:在調用本地方法時實際調用的是遠程的方法,那么勢必本地需要一個遠程代理對象


總結:為了實現RPC需要有:通信模型(BIO、NIO),服務定位(IP、PORT),遠程代理對象(遠程服務的本地代理),序列化(網絡傳輸轉換成二進制)





4. 簡單實現

其主要的對象有:服務端接口、服務端接口實現、服務暴漏、客戶端接口(與服務端共享同個接口)、服務的引用


4.1 服務端接口

public interface Service {

	// 提供兩個服務,說hello和整數相加
	public String hello();
	public int sum(int a, int b);
	
}

4.2 服務端接口實現

public class ServiceImpl implements Service {

	@Override
	public String hello() {
		return "Hello World";
	}

	@Override
	public int sum(int a, int b) {
		return  a + b;
	}
}

4.3 服務暴漏

public static void export(Object service, int port) {
	
	if (service == null || port <= 0 || port > 65535) {
        throw new RuntimeException("Arguments error");
    }
	
	System.out.println(service.getClass().getName() + ": " + port + "服務暴露");
	
	new Thread( () -> {
		
		try (ServerSocket server = new ServerSocket(port);) {
			
			while(true){
				try (
						Socket socket = server.accept();
						ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
						ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
					) {
					
					// 讀取方法名
					String methodName = in.readUTF();
					
					// 讀取參數類型
					Class<?>[] parameterTypes = (Class<?>[])in.readObject();
					
					// 讀取參數值
					Object[] arguments = (Object[])in.readObject();
					
					// 獲取方法
					Method method = service.getClass().getMethod(methodName, parameterTypes);
					
					// 處理結果
					Object result = method.invoke(service, arguments);
					
					// 寫入結果
					out.writeObject(result);
					
				} catch (Exception e) {
					e.printStackTrace();
				} 
			}
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}).start();
}

這個暴露的邏輯是服務端監聽特定端口,等客戶端發起請求后連接,然后通過Java的IO流獲取方法名,參數等相關信息,最后通過反射實現方法的調用並將結果響應給客戶端


4.4 客戶端接口

public interface ClientService {

	// 提供兩個服務,說hello和整數相加
	public String hello();
	public int sum(int a, int b);
		
}

4.5 服務引用

public static <T>T refer(Class<T> interfaceClass, String host, int port){
	
	if(interfaceClass == null || !interfaceClass.isInterface() || host == null || port <= 0 || port > 65535){
		throw new RuntimeException("Arguments error");
	}
	
	System.out.println("正在調用遠程服務");
	
	@SuppressWarnings("unchecked")
	T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() {
				
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			
			Object result = null;
			try (
					Socket socket = new Socket(host, port);
					ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
					ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
				) {
				
				out.writeUTF(method.getName());
				
				out.writeObject(method.getParameterTypes());
				
				out.writeObject(args);
				
				result = in.readObject();
				
			} catch (Exception e) {
				e.printStackTrace();
			}
			return result;
		}
	});
	return proxy;
}

而引用服務的邏輯是:創建Socket套接字連接,序列化相關請求信息發送給服務端,然后等待響應結果。其中透明調用是使用了動態代理


4.6 測試

public class Test {
	
	public static void main(String[] args) {
		
		// 暴露服務
		ServiceImpl service = new ServiceImpl();
		RPCFramework.export(service, 8080);
		
		// 調用服務
		Client client = RPCFramework.refer(Client.class, "127.0.0.1", 8080);
		int sum = client.sum(1, 2);
		String rs = client.hello();
		System.out.println("遠程響應:" + sum);
		System.out.println("遠程響應:" + rs);
	}
}
RPC.ServiceImpl:8080----- 服務暴露

正在調用遠程服務
遠程響應:3
遠程響應:Hello World






5. 思考


5.1 為什么不用Http

RPC與具體協議無關,可基於Http、TCP,但因為TCP性能相對較好。Http屬於應用層協議,TCP屬於傳輸層協議,相對在底層少了一層封裝,而且為了可靠傳輸而選擇TCP不選擇UDP


5.2 常用的RPC框架

Dubbo(阿里巴巴)、SpringCloud、RMI(JDK內置)


5.3 為什么要使用動態代理

因為要像本地調用一樣,對於使用者來說是透明的。

Object result = XXX(String method, String host, int port)

上面這樣其實也行,但並不能感覺到是調用本地方法一樣,而且如果一個接口有多個方法的話,每調用一次方法就需要發送一次host / port

// 動態代理可以這樣使用
ProxyObject.方法1
ProxyObject.方法2

// 沒有使用動態代理則不人性化
XXX(String method1, String host, int port)
XXX(String method2, String host, int port)

5.4 為什么參數與參數類型需要分開傳

為了方便分辨方法的重載,下面獲取方法需要方法名和參數類型

service.getClass().getMethod(methodName, parameterTypes)






6. 優化


6.1 網絡通信

上面事例中采用BIO形式,阻塞訪問而導致並發量不高,可以用NIO代替


6.2 序列化

這里用了JDK原生方法只能序列化實現了Serializable接口的類,可以使用第三方的類庫來提高性能


6.3 服務負載

服務的自動發現,客戶端能動態感知服務端的變化,從實現熱部署,可用定時輪詢的方法,eg:ZooKeeper


6.4 Cluster

集群化,這樣便可以提供負載均衡


6.5 請求與響應

請求與響應可以進行編碼封裝,而不是這樣單獨一個一個發送




參考Yanyan.He



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM