c++socket多个客户端通过不同端口与一个服务端通信_手写RPC,深入底层理解整个RPC通信...
一、前言
RPC,遠程過程調用,調用遠程方法像調用本地方法一樣。RPC交互分為客戶端和服務端,客戶端調用服務端方法,服務端接收數據并打印到控制臺,并response響應給客戶端。
RPC和HTTP的聯系與區別
聯系:都是遠程通信的網絡協議,任何網絡協議都包括語法、語義、時序三個特征。
區別:
HTTP協議是應用層協議,更多地用于前后端通信或移動端、PC端與后端通信,其他的常用應用層協議還包括SSH協議、ftp協議。
RPC是遠程過程調用,更多地用于分布式架構中各個服務模塊之間的通信,其中的知識涉及socket通信、序列化和反序列化、動態代理、反射調用。如下程序,服務端要發布socket服務+線程池io處理多個客戶端連接+反射調用,客戶端要動態代理實例化對象,參與網絡傳輸的Bean要實現Serializable接口變為可序列化+IO流序列化/反序列化。
本文意義:我們會使用到很多RPC框架,這些RPC框架底層都是封裝好了的,socket通信、序列化(本文使用JavaIO實現序列化)、反射、代理,本文意義在于自己手寫一個RPC通信,理解RPC通信底層,以后看RPC框架的源碼就簡單多了。
tip:手寫RPC,一步步來看,由于沒有兩臺電腦,就用一個電腦上的兩個工程進行交互。
二、客戶端動態代理
2.1 構建工程結構
分布式就是在兩個JVM進程的通信,現實中環境中是兩個電腦上的兩個JVM進程,筆者只有一個電腦,所以使用一個電腦上的兩個Java程序,本質上是一樣的,只要是兩個JVM程序通信即可(唯一不同的是,兩個電腦的通信就是要將服務端的api模塊發布到私服上供客戶端maven依賴使用,但是一個電腦上的上兩個應用程序是將api模塊maven install到本地倉庫供客戶端maven依賴使用)。
使用idea新建兩個maven Project,架構為quickstart(因為我們只是應用程序,不是web程序),分別為rpcServer 和 rpcClient ,在rpcServer中新建兩個Modul,也都是maven quickstart,分別為rpcServerApi和rpcServerProvider。
如圖:
注意:無論新建Maven Project還是Maven Modul,事先要設置好idea中的maven home,user settings file,maven repository,類似筆者電腦如下:
2.2 rpcServerApi被rpcServerProvider 和 rpcClient 引用
將rpcServerApi 作為依賴在rpcServerProvider中使用,然后,將rpcServerApi Maven clean,再Maven install,就可以生成jar包安裝到本地的maven repository中,這樣,讓rpcClient再次引入rpcServerApi作為依賴。
如圖:
注意1:maven quickstart生成為jar包,maven webapp生成為war包,這里rpcServerApi是quickstart,所以maven install是生成jar包(到本地maven repository)。
注意2:pom.xml依賴來源于兩種,一種是遠程依賴,一種是本地依賴,這里的rpcServerApi經過maven install就是本地依賴了。
2.3 客戶端動態代理
2.3.1 rpcServerApi 提供接口
2.3.2 rpcServerProvider
publisher()方法為服務端發布一個服務
2.3.3 rpcClient 動態代理使用IHelloService
由于客戶端和服務端是兩個JVM中的程序,即使現在將服務端工程的api模塊maven install到本地倉庫,客戶端程序可以通過導入依賴找到IHelloService,但是如何實例化IHelloService呢?畢竟IHelloService是接口,無法自己實例化,api模塊中也沒有其子類實現(provider模塊中倒是有IHelloService的子類實現,但是客戶端程序并未導入其依賴)。所以,由于api模塊本身無法實例化IHelloService,所以客戶端無法通過new 實例化IHelloService,這里采用動態代理的方式。即客戶端拿到的IHelloService只是一個引用,需要使用遠程代理。
2.4 客戶端動態代理成功
先運行rpcServerProvider工程的Main類的main方法,啟動服務端,綁定8080端口;然后,啟動rpcClient工程的App類的main方法,去連接8080端口,運行成功:
代理對象調用sayhello()方法就是執行invoke()方法里面的邏輯,這里執行了,打印成功。
三、客戶端連接服務端并數據傳送
3.1 服務端 rpcServerProvider
服務端rpcServerApi模塊,提供網絡通信Bean類 RpcRequest,因為這個Bean類用于網絡通信,所以變為可序列化,實現Serializable接口。將 rpcServerProvider maven install 更新下,這樣更改對于rpcClient就可見了。
3.2 客戶端 rpcClient
客戶端提供RpcNetTransport類,用于Socket連接,newSocket()方法提供連接服務端socket,send()提供調用newSocket()連接服務端。
注意到,服務端api模塊中,RpcRequest是一個實體類,所以客戶端可以直接使用new新建實例,但是剛才的IHelloService不行,只能使用動態代理。
四、多線程、傳送數據Bean的序列化和反序列化
4.1 服務端 rpcServerProvider 接收客戶端發送的數據
4.2 客戶端 rpcClient 寫數據到服務端
五、服務端反射調用并返回給客戶端
5.1 rpcServerProvider 服務端返回數據給客戶端
服務端收到客戶端發來的數據反序列為RpcRequest,這里面裝載著classname methodname type Parameters,服務端invoke()根據這些信息反射調用方法,將得到的結果記為result,并發送給客戶端。
5.2 rpcClient 客戶端接收數據
客戶端的send()方法中應該完善與服務端的交互。
客戶端的動態代理invoke()方法是新建一個RpcRequest類,制定好classname methodname type parameters ,綁定端口號,發送給服務端。
六、成功交互
整個流程如下:
服務端運行,然后客戶端運行連接上服務端,然后客戶端 helloService.sayHello(“Mic”);客戶端組裝好數據報文,發送給服務端,服務端接收數據報文,根據數據報文反射調用方法,則服務端打印 “服務端收到一個請求: Mic”,然后將返回值 “這里是服務端sayHello的實現” 發送給客戶端,客戶端將其打印出來。
整個過程涉及到socket通信、序列化和反序列化、動態代理、反射調用,其中,服務端要發布socket服務+線程池io處理多個客戶端連接+反射調用,客戶端要動態代理實例化對象,參與網絡傳輸的Bean要實現Serializable接口變為可序列化+IO流序列化/反序列化。
七、面試金手指
7.1 JDK反射調用(InvocationHandler接口)+動態代理(Proxy.newInstance())(一)
7.1.1 JDK動態代理:實際接口
首先是要被代理的接口:
/** * 被代理的主體需要實現的接口 */public interface Subject { String doSomething(String thingsNeedParm); String doOtherNotImportantThing(String otherThingsNeedParm);}7.1.2 JDK動態代理:實際接口的實現類
然后是代理接口的實現類:
public class SubjectIpml implements Subject { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String doSomething(String thingsNeedParm) { System.out.println("使用" + thingsNeedParm + "做了一些事情"); return "調用成功"; } @Override public String doOtherNotImportantThing(String otherThingsNeedParm) { System.out.println("使用" + otherThingsNeedParm + "做了一些事情"); return "調用成功"; }}7.1.3 JDK動態代理:代理類(實現InvocationHandler接口重寫invoke()方法)+動態代理Proxy.newInstance()
然后就是代理類:
public class SubjectProxy implements InvocationHandler { private Subject subject; SubjectProxy(Subject subject){ this.subject = subject; } /** * @param proxy 調用這個方法的代理實例 * @param method 要調用的方法 * @param args 方法調用時所需要的參數 * @return 方法調用的結果 * @throws Throwable 異常 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //進行method過濾,如果是其他方法就不調用 if (method.getName().equals("doSomething")){ System.out.println("做某些事前的準備"); Object object = method.invoke(subject,args); System.out.println("做某些事后期收尾"); return object; } return "調用失敗"; } /** * 獲取被代理接口實例對象 */ public Subject getProxy() { return (Subject) Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), this); } }7.1.4 測試類:動態代理+反射調用
然后是測試類:
public class ProxyTest { public static void main(String[] args) { Subject subject = new SubjectIpml(); SubjectProxy subjectProxy = new SubjectProxy(subject); // 實際類構造注入到代理類 Subject proxy = subjectProxy.getProxy(); // 獲取代理對象 proxy.doSomething("改錐"); // 代理對象反射調用方法 proxy.doOtherNotImportantThing("紙片"); // 代理對象反射調用方法就是執行invoke()里面的邏輯,比真實對象調用方法多了一個前打印和后打印 }}7.1.5 測試結果
測試結果:
做某些事前的準備使用改錐做了一些事情做某些事后期收尾做某些事前的準備使用紙片做了一些事情做某些事后期收尾實現JDK動態代理很簡單,只要實現InvocationHandler接口重寫invoke()方法,就好了。
調用invoke()方法的時候,返回代理對象,然后使用代理對象來調用方法就好。
7.1.6 InvocationHandler接口的invoke()方法(三個參數+返回值),以Mybatis源碼為例
InvocationHandler的invoke()的三個參數到底有什么用呢,經過筆者翻看Mybatis的MapperProxy動態代理的源碼,筆者發現了他的用處。
先貼上Mybatis的MapperProxy的源碼:
public?class?MapperProxy?implements?InvocationHandler,?Serializable?{ private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { final Constructorconstructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); if (!constructor.isAccessible()) { constructor.setAccessible(true); } final Class> declaringClass = method.getDeclaringClass(); return constructor .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } /** * Backport of java.lang.reflect.Method#isDefault() */ private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); }}看invokeDefaultMethod這個方法,就用到了invoke() 的第一個參數proxy,這個invokeDefaultMethod只是為了修復Mybatis早期版本不能調用Java8默認方法的bug。
他把默認方法綁定到了調用方法的代理實例,然后再傳入參數,調用默認方法
金手指:InvocationHandler接口的invoke()方法(三個參數+返回值)
第一個參數proxy,可以用來綁定實例接口的方法;
第二個參數method ,是調用的方法,可以用來方法過濾,得到方法的聲明類等等;
第三個參數就僅僅是被調用方法的參數罷了;
返回值:類型為Object,可以自己任意返回,可以返回invoke()調用的函數的返回值,也可以返回字符串。
7.1.7 Proxy.newInstance()方法
newProxyInstance方法(調用對象+三個參數+返回值)
調用對象:在需要創建代理實例的時候才調用,這里是測試類中調用,因為是靜態方法,所以是直接類名調用,就是Proxy.newInstance()這樣用。
三個參數:
loader: 用哪個類加載器去加載代理對象;
interfaces:動態代理類需要實現的接口;
h:動態代理方法在執行時,會調用h里面的invoke方法去執行。
返回值:返回類型為Object,但是如果去接收的話,返回的就是一個代理對象,第一個測試類(封裝一層)和第二個測試類都是使用一個引用接收代理對象。
7.2 JDK反射調用(InvocationHandler接口)+動態代理(Proxy.newInstance())(二)
利用Java的反射技術(Java Reflection),在運行時創建一個實現某些給定接口的新類(這個類就是“動態代理類”,代理的是接口,創建一個實現了該接口的類,所以代理類和實際類是實現同一個接口)及其實例(對象),代理的是接口(Interfaces),不是類(Class),也不是抽象類。在運行時才知道具體的實現,spring aop就是此原理。
金手指:Proxy.newInstance()方法三個點
1、運行時才創建;
2、創建一個實現了給定接口的類,這個類就是代理類,所以代理類和實際類實現同一個接口;
3、并創建代理類的實例,返回代理類實例的引用。
金手指:第一個測試類和第二個測試類區別
Subject proxy = subjectProxy.getProxy(); // 獲取代理對象 proxy.doSomething("改錐"); // 代理對象反射調用方法proxy.doOtherNotImportantThing("紙片");???//?代理對象反射調用方法第一個測試類,通過調用封裝好的getProxy()方法調用底層的Proxy.newInstance(),在運行時創建實現接口的代理類的對象,然后return返回引用,使用Subject proxy接收引用。然后使用代理對象的引用調用方法,實際上是調用實際實現類的方法。
IVehical vehical = (IVehical)Proxy.newProxyInstance(car.getClass().getClassLoader(), Car.class.getInterfaces(), new VehicalInvacationHandler(car));vehical.run();第二個測試類,Main類中調用Proxy.newInstance()返回,在運行時創建實現接口的代理類的對象,然后return返回引用,使用IVehical vehical接收引用,然后使用代理對象的引用調用方法,實際上是調用實際實現類的方法。
小結:兩者是一樣的,只是第一個實現類包了一層。
public static Object newProxyInstance(ClassLoader loader,?????Class>[]?interfaces,?InvocationHandler?h)?throws?IllegalArgumentException7.2.1 JDK動態代理:實際接口
public interface IVehical { void run();}7.2.2 JDK動態代理:實際接口的實現類
public class Car implements IVehical { public void run() { System.out.println("Car會跑"); }}7.2.3 JDK動態代理:代理類,實現Invocationhandler接口,重寫invoke()方法,在invoke()方法中使用反射調用接口實現類中的方法
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method; public class VehicalInvacationHandler implements InvocationHandler { private final IVehical vehical; public VehicalInvacationHandler(IVehical vehical){ this.vehical = vehical; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("---------before-------"); Object invoke = method.invoke(vehical, args); System.out.println("---------after-------"); return invoke; }}7.2.4 測試類
import java.lang.reflect.Proxy; public class App { public static void main(String[] args) { IVehical car = new Car(); IVehical vehical = (IVehical)Proxy.newProxyInstance(car.getClass().getClassLoader(), Car.class.getInterfaces(), new VehicalInvacationHandler(car)); // 獲取代理對象 vehical.run(); //代理對象調用run()方法就是執行invoke()里面的邏輯,比真實對象調用run()方法多了一個前打印和后打印 }}上面代碼中,代理car對象,調用run方法時,自動執行invocationhandler中的invoke方法。
7.2.5 測試結果
---------before-------Car會跑---------after--------7.3 RPC總結
7.3.1 RPC全體過程(動態代理)
金手指1:客戶端動態代理
服務端只有一個api模塊maven install,客戶端可以識別,但是客戶端拿到的api模塊的不是只是一個接口,沒實例化,服務端的provider模塊雖然實例化了,但是沒有maven install,現在客戶端實現動態代理,使用Proxy.newInstance()實現動態代理,得到代理對象,然后再用代理對象調用實現類的方法,就是執行invoke()方法。
金手指2:Proxy.newInstance()
Proxy.newInstance()該函數要傳遞三個參數
loader: 用哪個類加載器去加載代理對象,意義:接口的類加載器;
interfaces:動態代理類需要實現的接口,意義:接口的字節碼,代理和實現是同一個接口;
h:動態代理方法在執行時,會調用h里面的invoke方法去執行,意義:實參是傳入InvocationHandler接口實現類,要執行里面的invoke()方法。
金手指3:Proxy.newInstance()
Proxy.newInstance()第三個參數需要一個InvocationHandler接口實現類,所以要提供一個InvocationHandler接口實現類,實現invoke()方法。
invoke()方法三個參數,第一個proxy,一般沒用,第二個是method,用來調用方法method.invoke(),既然是調用方法,就要傳遞對象和參數,第三個是args參數。
這樣,Proxy.newInstance()得到代理對象,然后代理對象.functionXxx(“參數”)就調用了invoke(),代理對象 functionXxx “參數” 分別確定invoke()中的三個參數。
金手指4:InvocationHandler接口的invoke()方法(三個參數+返回值)
第一個參數proxy,可以用來綁定實例接口的方法;
第二個參數method ,是調用的方法,可以用來方法過濾,得到方法的聲明類等等;
第三個參數就僅僅是被調用方法的參數罷了。
返回值:類型為Object,可以自己任意返回,可以返回invoke()調用的函數的返回值,也可以返回字符串。
金手指5:newProxyInstance方法(調用對象+三個參數+返回值)
調用對象:在需要創建代理實例的時候才調用,這里是測試類中調用,因為是靜態方法,所以是直接類名調用,就是Proxy.newInstance()這樣用。
三個參數:
loader: 用哪個類加載器去加載代理對象
interfaces:動態代理類需要實現的接口
h:動態代理方法在執行時,會調用h里面的invoke方法去執行
返回值:返回類型為Object,但是如果去接收的話,返回的就是一個代理對象,第一個測試類(封裝一層)和第二個測試類都是使用一個引用接收代理對象。
金手指6:Proxy.newInstance()方法和 InvocationHandler接口的invoke()方法的關系
Proxy.newInstance()方法的第三個參數是InvocationHandler接口的實現類,用來新建 InvocationHandler接口實現類的實例對象,后面動態代理調用方法的時候,就是調用這個 InvocationHandler接口實現類的對象invoke()方法了,而不是其他 InvocationHandler接口實現類的對象的invoke()方法。
7.3.2 RPC全體過程
RPC整體過程:
1、先啟動服務端,服務端publisher服務,等待連接,因為是服務端不能斷,所以用while(true);
2、然后啟動客戶端,Proxy.newInstance()得到代理對象,然后代理對象.functionXxx(“參數”)就調用了invoke(),先打印一句?System.out.println("Hello Proxy:invoke");?,然后發送網絡請求;
3、客戶端中的invoke()方法中的method參數沒有用來調用客戶端自己的方法(因為不需要調用自己方法,要完成網絡通信,去調用服務端的方法(就是使用ip:port建立連接,然后生成代理對象,開始傳遞類名、方法名、參數類型列表,args參數用來傳遞參數,傳遞給服務端,讓服務端根據提供的信息發射調用自己的方法,然后將反射調用方法的返回值傳遞過來,客戶端接收)),而是用來設置類名、方法名、參數類型列表,客戶端中的invoke()方法中的args參數也是用來傳遞參數,傳遞給服務端,讓服務端根據提供的信息發射調用自己的方法(金手指:客戶端傳遞之前將數據包裝成一個RpcRequest對象,所以服務端getOutputStream()直接強轉(RpcRequest));
4、服務端將反射調用方法的返回值傳遞過來,客戶端接收,客戶端打印"這里是服務端sayHello的實現",這樣完成客戶端調用服務端的方法就像調用自己的方法一樣,這就是RPC整個過程。
值得注意的是,客戶端調用的這個sayhello()是有參數的,在客戶端的InvocationHandler接口實現類的invoke()方法中,使用 request.setParameters(args); 將實參傳遞過去,對于客戶端傳遞的四個參數,服務端的invoke()方法中對其接收處理,如下:
Class clazz = Class.forName(rpcRequest.getClassName()); // className用于確定字節碼Method method = clazz.getMethod(rpcRequest.getMethodName(), rpcRequest.getType()); // getMethod需要方法簽名 methodname和getType設置Object?result?=?method.invoke(service,?rpcRequest.getParameters());??//?invoke需要對象和實參????service是new?HelloService();??實參使用?args?傳遞過來的四個參數與反射:
四個參數:classname得到字節碼,methodname+type 方法簽名 args 方法參數;
反射:因為是反射調用,所以對象必須是實現類,客戶端中動態代理的invoke中的對象參數也是實現類。
7.3.3 RPC涉及的技術(五個)
RPC目的:客戶端調用服務端的方法就像調用自己的一樣。
要完成RPC的目的,整個RPC涉及的技術:
1、通信:客戶端給服務端通信,服務端給客戶端通信,這里用socket通信
2、通信的內容,既要通信,一定不能沒有通信內容,通信內容類一定要是可序列化的,實現Serializable接口,通信發送和接收一定要序列化和反序列化,這是使用javaIO流實現。
3、通信的內容,通信內容類要實現Serialable接口,就要指定調用服務端的哪個類的哪個方法,包括確定方法+調用方法:
確定方法:類使用字節碼作為一個標識,方法使用方法簽名作為唯一標識,使用客戶端InvocationHandler實現類中的invoke()方法中的method參數來完成。
調用方法:給出方法實參列表,使用客戶端InvocationHandler實現類中的invoke()方法中的args參數來完成。
4、客戶端動態代理:代理分為兩種靜態代理和動態代理,編譯時確定代理的實際類和運行時確定代理的實際類。這里使用Proxy.newInstance()完成。
5、服務端反射調用:客戶端要調用服務端的方法,傳遞方法的唯一坐標,服務端不大可能new一個對應的對象來調用方法(也可以這樣做,只是不優雅,RPC框架源碼沒有new的),一般是使用反射調用。
金手指:RPC涉及的技術
三者必不可少:通信+通信內容Serializable和序列化+客戶端動態代理與服務端反射調用;
其余兩個:注冊中心管理服務器地址 + Spring IOC管理bean(xml或者注解+掃描)。
7.3.4 手寫RPC和RPC框架
這里是我們自己寫RPC框架,所以底層五個技術都是自己實現,好的RPC框架都是封裝好的,本文方便我們理解rpc框架。
八、尾聲
手寫RPC,深入底層理解整個RPC通信,完成了。
天天打碼,天天進步!!!
總結
以上是生活随笔為你收集整理的c++socket多个客户端通过不同端口与一个服务端通信_手写RPC,深入底层理解整个RPC通信...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: runfile python_pytho
- 下一篇: 打印dog信息java_java – 打