前言
寫這篇博客的目的主要回顧一下學過的知識,避免遺忘,同時希望夥伴一起來指正一些不足,共同學習。文章是基於Socket來實現一個基本的RPC通信框架,並且實現版本控制。功能不會太複雜,主要是爲了疏通思路脈路。
背景環境
在分佈式中,我們經常會用到dubbo+zookeeper的框架來實現,由於本篇博客並沒有對zookeeper的實現,所以我們將由RPC-Server來統一對API進行管理,還請見諒!
一、RPC-Server創建
1.1 rpc-server-api
1.1.1基本創建
實體類的創建,並實現get&set方法。
//實體類
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
測試接口:
這裏隨便定義兩個方法,就不解釋方法了,看名字也能猜出來。
public interface IHelloService {
String sayHello(String content);
/**
* 保存用戶
* @param user
* @return
*/
String saveUser(User user);
}
RpcRequest:
關於Request對象的解釋:
Request對象,又稱爲請求對象,該對象派生自HTTPResponse類,是ASP中重要的服務器內置對象,它連接着Web服務器和Web客戶端程序。該對象用來獲取客戶端在請求一個頁面或者傳送一個Form時提供的所有信息,包括能夠標識瀏覽器和用戶的HTTP變量、存儲在客戶端Cookie信息以及附在URL後面的值、查詢字符串或頁面中Form段HTML控件內的值、Cookie、客戶端證書、查詢字符串等 。如瀏覽器和用戶的變量,客戶端表單中的數據、變量或者客戶端的cookie信息等,Request對象對應的類是System、Web、HttpRequest類。
我們這裏也簡單存儲一下請求的基本信息:
public class RpcRequest implements Serializable {
//類名
private String className;
//方法名
private String methodName;
//參數
private Object[] parameters;
//版本號
private String version;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}
1.1.2 打jar包
到此處,api模塊的編寫就簡單的完成了。我們將此模塊進行打包,然後添加到prc-server-provider中。
1.2 rpc-server-provider
在這裏我們需要對接口的註冊和接收服務端的請求處理,
1.2.1 依賴
<!-- rpc-server-api 的maven地址-->
<dependency>
<groupId>com.ccc</groupId>
<artifactId>rpc-server-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
1.2.2 定義RpcService註解
@Target(ElementType.TYPE) //修飾範圍 //類或接口
@Retention(RetentionPolicy.RUNTIME)
@Component //被spring進行掃描
public @interface RpcService {
Class<?> value(); //拿到服務的接口
/**
* 版本號
*/
String version() default "";
}
1.2.3 對api接口實現
由於我們對版本進行了控制,所以此處簡單寫兩個實現類並標註上述聲明的註解
@RpcService(value = IHelloService.class,version = "v1.0")
public class HelloServiceImpl implements IHelloService{
@Override
public String sayHello(String content) {
System.out.println("[v1.0] request in :"+content);
return "[v1.0]say Hello:"+content;
}
@Override
public String saveUser(User user) {
System.out.println("request in saveUser :" +user);
return "[v1.0]SUCCESS";
}
}
@RpcService(value = IHelloService.class,version = "v2.0")
public class HelloServiceImpl2 implements IHelloService{
@Override
public String sayHello(String content) {
System.out.println("[v2.0] request in :"+content);
return "[v2.0]say Hello:"+content;
}
@Override
public String saveUser(User user) {
System.out.println("request in saveUser :" +user);
return "[v2.0]SUCCESS";
}
}
1.2.4 PrcServer編寫
創建MyRpcServer類,實現ApplicationContextAware,InitializingBean這兩個接口,當然這並不一定是必須需要實現這兩個接口,主要能實現功能就行。
ApplicationContextAware:加載Spring配置文件時,如果Spring配置文件中所定義或者註解自動注入的Bean類實現了ApplicationContextAware 接口,那麼在加載Spring配置文件時,會自動調用ApplicationContextAware 接口中的setApplicationContext方法。
InitializingBean:InitializingBean接口爲bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是繼承該接口的類,在初始化bean的時候都會執行該方法。
另外實現的通信是基於Socket套接字來實現的,我們這裏使用一個緩存線程池來處理每一次接收到的請求,另外請求的業務邏輯我們也交由ProcessorHandler線程來處理。
public class MyRpcServer implements ApplicationContextAware, InitializingBean {
//緩存線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//存放註解的容器
private Map<String,Object> handlerMap = new HashMap<>();
//端口號
private int port;
public MyRpcServer(int port){
this.port = port;
}
/**
* InitializingBean接口方法:
* 初始化客戶端。
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
ServerSocket serverSocket = null;
try {
//創建對象並設置端口
serverSocket = new ServerSocket(port);
while (true){
Socket socket = serverSocket.accept(); //此處會阻塞
//每一個socket交給一個processorHandler處理
executorService.execute(new ProcessorHandler(handlerMap,socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket !=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* ApplicationContextAware接口方法
* 將API進行初始化並存放於容器中
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//獲取指定註解
Map<String,Object> serviceMap = applicationContext.getBeansWithAnnotation(RpcService.class);
if(! serviceMap.isEmpty()){
//遍歷註解的value
for(Object serviceBean : serviceMap.values()){
//拿到註解
RpcService rpcService = serviceBean.getClass().getAnnotation((RpcService.class));
//獲取name
String serviceName = rpcService.value().getName();
//獲取版本號
String version = rpcService.version();
//添加版本號
if(!StringUtils.isEmpty(version)){
/**
* 將serviceName拼接版本號,在此控制了版本號後,
* 如果在註冊API時候添加了版本號,那麼客戶端調用接口的時候,就必須傳遞版本號信息
* 否則無法進行調用。
*/
serviceName += "-"+version;
}
//將name作爲key class對象作爲value存放
handlerMap.put(serviceName,serviceBean);
}
}
}
}
1.2.5 ProcessorHandler
此類實現了Runnable接口,接收的每個請求都單獨用一個線程來處理,該類中主要是從socket中讀取客戶端發送過來的信息處理然後調用指定的方法。
public class ProcessorHandler implements Runnable {
private Socket socket;
private Map<String, Object> handlerMap;
/**
* 構造器
* @param handlerMap 獲取註解信息
* @param socket 獲取請求信息
*/
public ProcessorHandler(Map<String, Object> handlerMap, Socket socket) {
this.socket = socket;
this.handlerMap = handlerMap;
}
@Override
public void run() {
ObjectInputStream objectInputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
// ------------------- InputStream ------------------
//拿到客戶端信息 輸入流
objectInputStream = new ObjectInputStream(socket.getInputStream());
/**
* 進行反序列化,將客戶端發送的Request信息讀取出來。
* 包括請求哪個類,方法名稱,參數
*/
RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
//將請求進行處理
Object result = invoke(rpcRequest);
//-------------------- OutputStream --------------------
//可以用於發送廣播消息。目前沒有使用
objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(result);
objectOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private Object invoke(RpcRequest rpcRequest) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//得到類名
String serviceName = rpcRequest.getClassName();
//獲取版本號
String version = rpcRequest.getVersion();
//如果版本號不爲空,那麼將請求數據按照規定拼接
if (!StringUtils.isEmpty(version)) {
serviceName += "-" + version;
}
System.out.println(serviceName);
System.out.println("map:"+handlerMap);
//反射調用
//根據key獲取類對象
Object service = handlerMap.get(serviceName);
//根據RpcRequest請求中的serviceName 如果沒有找到 拋出異常。
if (service == null) {
throw new RuntimeException("service not found:" + serviceName);
}
//獲取請求參數數組
Object[] args = rpcRequest.getParameters(); //獲得請求參數
Method method = null;
if (args != null) { //如果有參數 對參數進行處理,如果沒有進行調用加載方法。
//遍歷獲得每個參數的類型
Class<?>[] types = new Class[args.length];
for (int i = 0; i < args.length; i++) {
//得到參數類型
types[i] = args[i].getClass();
}
//根據請求的類去加載 //HelloServiceImpl
Class clazz = Class.forName(rpcRequest.getClassName());
method = clazz.getMethod(rpcRequest.getMethodName(), types);// sayHello saveUser 找到類中的方法
}else {
//根據請求的類去加載 //HelloServiceImpl
Class clazz = Class.forName(rpcRequest.getClassName());
// sayHello saveUser 找到類中的方法
method = clazz.getMethod(rpcRequest.getMethodName());
}
//反射調用
return method.invoke(service, args);
}
}
1.2.6 注入
將代碼交由Spring來管理。
@Configuration
@ComponentScan(basePackages = "com.ccc")
public class SpringConfig {
@Bean(name = "MyRpcServer")
public MyRpcServer MyRpcServer(){
return new MyRpcServer(9527);
}
}
1.2.7 啓動類
public class App {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
((AnnotationConfigApplicationContext) context).start();
}
}
1.2.8 打包
將rpc-server打jar包,方法和上圖一樣。 到這裏簡單的rpcserver也就完成了。下面寫個prc-client
二、RPC-Client 創建
創建個maven項目就行
2.1導入依賴
將RPC-Server的依賴導入進來
<dependency>
<groupId>com.ccc</groupId>
<artifactId>rpc-server-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2.2 與Server進行連接
public class RpcNetTransport {
private String host;
private int prot;
public RpcNetTransport(String host, int prot) {
this.host = host;
this.prot = prot;
}
public Object send(RpcRequest request) {
Socket socket = null;
Object result = null;
ObjectOutputStream objectOutputStream = null;
ObjectInputStream inputStream = null;
try {
socket = new Socket(host, prot); //建立連接
/**
* 將客戶的端的request信息進行輸出到Server端
*/
objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(request); //序列化
objectOutputStream.flush();
// ----------------- InputStream ---------------
//接受服務端發來的廣播消息,暫時沒用上。
inputStream = new ObjectInputStream(socket.getInputStream());
result = inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
}
2.3 java動態代理來進行server之間交互。
2.3.1 RemoteInvocationHandler
在這裏我使用java的動態代理來實現創建一個RemoteInvocationHandler實現InvocationHandler,並實現invoke方法
InvocationHandler:是proxy代理實例的調用處理程序實現的一個接口,每一個proxy代理實例都有一個關聯的調用處理程序;在代理實例調用方法時,方法調用被編碼分派到調用處理程序的invoke方法,該方法有三個參數:
proxy:代理類代理的真實代理對象com.sun.proxy.$Proxy0
method:我們所要調用某個對象真實的方法的Method對象
args:指代代理對象方法傳遞的參數
public class RemoteInvocationHandler implements InvocationHandler {
private String host;
private int prot;
public RemoteInvocationHandler(String host, int prot) {
this.host = host;
this.prot = prot;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//打樁
System.out.println("come in");
RpcRequest rpcRequest = new RpcRequest();
// --------------- 開始 設置 RpcRequest 請求參數 ------------------
//設置類名
rpcRequest.setClassName(method.getDeclaringClass().getName());
//設置方法名
rpcRequest.setMethodName(method.getName());
//參數
rpcRequest.setParameters(args);
//版本號
rpcRequest.setVersion("v1.0");
// --------------- 結束 設置 RpcRequest 請求參數 ------------------
//遠程通信,將rpcRequest進行輸出。
RpcNetTransport netTransport = new RpcNetTransport(host,prot);
Object result = netTransport.send(rpcRequest);
//返回結果
return result;
}
}
2.3.2 RpcClient
完成了上述操作,現在我就只需要對接口進行代理處理來實現RPC通信了。
public class RpcProxyClient {
public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
/**
* 使用java自帶的代理類 將接口進行代理來進行 RPC遠程通信,該方法會自動去調用
* InvocationHandler 中的invok方法
*/
return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(),
new Class<?>[]{interfaceCls},new RemoteInvocationHandler(host,port));
}
}
2.3.3 啓動類
我們只需要寫個main方法來啓動就行
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
RpcProxyClient rpcProxyClient = new RpcProxyClient();
//傳入需要調用的接口,主機地址,端口號
IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost",9527);
//接口調用
String result = iHelloService.sayHello("Ccc");
//輸出返回結果
System.out.println(result);
}
}
三、測試。
啓動Rpc-Server…沒有報錯
啓動Rpc-Client…
我們查看一下server接收到的參數
發現客戶端的標識是 Ccc
客戶端接收的返回結果
接口調用成功。。
我們測試一下 不添加版本號調用:
在RemoteInvocationHandler 中取出對version的設置。。
客戶端報錯:
原因很簡單,因爲我們在Server端對接口的的註解@RpcService中進行了版本控制所以serverName應該是serverName-version的格式,而且我們是通過serverName作爲key來獲取類對象的。而在客戶端不傳遞的version版本的時候,我們默認是使用serverName的,所以服務端通過這個key就沒法找到對應的value。
我們在修改version爲v2.0
version2.0 輸出成功。
到此,簡單的通信功能也就完成,如果您發現有啥錯誤的地方,懇請指出批評。在此感謝!