RPC笔记之初探RPC:DIY简单RPC框架
一、什么是RPC
RPC(Remote Procedure Call)即远程过程调用,简单的说就是在A机器上去调用B机器上的某个方法,在分布式系统中极其常用。
rpc原理其实很简单,比较容易理解,在rpc中有两个角色,rpc server和rpc client,先从client开始讨论,因为client要像调用普通类的方法那样调用server端的方法,比如client要调用server端的Foo#bar()方法,所以它必须首先获取到Foo的实例,但是又不能直接new,因为直接new的话还是相当于在本地调用,所以这个时候就必须有个什么机制能够把Foo包一下,使得表面上看起来和Foo完全一样但是调用它的bar方法时底层替换为去调用server的bar,这个机制就是代理,代理提供了一种类似于拦截的机制,可以把调用bar方法替换成为自己的实现,比如本地调用bar方法大致执行过程(粗糙概括):
1 2 | execute bar() return result |
代理替换之后的bar方法(粗糙概括):
1 2 3 | call rpc server execute bar() get result from server return result |
第一步的call rpc server execute bar(),client如何告诉server自己要调用哪个方法呢,这个方法就比较多了,比较常见的是约定一种协议,比如第几个字节是表示的嘛意思,然后server接收后解析按照指令执行就可以了,这样网络传输的数据比较少,或者不太讲究的直接将现成的协议拿过来用,比如通过socket直接传json、传xml、传对象流等等,再或者甚至用http请求的,反正能够把自己要调用哪个方法告诉server,同时还有调用方法时需要传递的参数,然后等待server执行完获取到其结果就可以了。
然后就是server端的处理,如果是使用socket传输数据的话,server应该启动一个服务监听在约定的端口(不约定好的话客户端不知道去连谁啊),一个while循环不断地等待客户端的连接,每来一个客户端就启动一个新的线程去处理(此处没有考虑高并发情况下的负载和优化,只是基本的实现),在新线程中读取socket流看客户端要调用哪个方法,然后调用本地的此方法,调用的时候将client传过来的参数传入进去,待方法执行完再传回给client,传回的方法和client传数据过来相同,无非是走socket自定义协议、xml、json、http等等,再然后client读取到结果返回,一次rpc调用就完成了。至此,一个简单的rpc框架的雏形已经完成。
二、DIY简单RPC框架
这一章节基于上面讨论的rpc调用的过程,实现一个简单的rpc框架,其中代理使用JDK提供的代理实现,传输层使用Java的ObjectInputStream和ObjectOutputStream实现。
定义一个工具类,提供两个方法,分别用于服务端启动rpc server和客户端获取相关serviceProvider的代理对象。
RpcServiceProviderUtil.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | package cc11001100.diySimpleRpcFramework.util; import java.io.Closeable; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.ServerSocket; import java.net.Socket; import static cc11001100.diySimpleRpcFramework.util.RpcLogUtil.info; /** * @author CC11001100 */ public class RpcServiceProviderUtil { /** * 在指定的端口上启动rpc服务,用于server端启动rpc服务 * * @param port * @throws IOException */ public static <T> void startService(T object, int port) throws IOException { ServerSocket serverSocket = new ServerSocket(port); while ( true ) { final Socket socket = serverSocket.accept(); info(socket, "start" ); new Thread(() -> { ObjectInputStream ois = null ; ObjectOutputStream oos = null ; try { // 从输入流中读取要调用的方法名和调用时传入的参数 ois = new ObjectInputStream(socket.getInputStream()); String methodName = ois.readUTF(); Class[] parameterTypes = (Class[]) ois.readObject(); Object[] parameterValues = (Object[]) ois.readObject(); Method method = object.getClass().getMethod(methodName, parameterTypes); // 调用方法执行 info(socket, "begin invoke method " , methodName); long start = System.currentTimeMillis(); Object invoke = method.invoke(object, parameterValues); long cost = System.currentTimeMillis() - start; info(socket, "exec method " , methodName, " done, cost=" , cost, "ms" ); // 将执行结果传回调用端 oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(invoke); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { // 如果发生了异常,反馈给调用端 try { info(socket, "exec exception, e=" , e.getClass(), ", cause=" , e.getCause()); oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(e); } catch (IOException e1) { e1.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } finally { close(ois); close(oos); close(socket); } info(socket, "end" ); }).start(); } } private static void close(Closeable closeable) { if (closeable != null ) { try { closeable.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * 用于客户端获取rpc的endpoint * * @param clazz * @param remoteHost * @param remotePort * @param <T> * @return */ public static <T> T wrap(Class<T> clazz, String remoteHost, int remotePort) { return (T) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), (proxy, method, args) -> { // 获取要调用的方法的名字 String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); // 调用rpc server端去执行 Socket socket = new Socket(remoteHost, remotePort); ObjectOutputStream oos = null ; ObjectInputStream ois = null ; Object result = null ; try { oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeUTF(methodName); oos.writeObject(parameterTypes); oos.writeObject(args); // 读取rpc server端执行结果 ois = new ObjectInputStream(socket.getInputStream()); result = ois.readObject(); // 此处不catch,执行时出了异常尽管抛出 } finally { close(ois); close(oos); close(socket); } // 检测server端执行是否抛了异常 if (result != null && result instanceof Throwable) { throw (Throwable) result; } return result; }); } } |
RpcLogUtil.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package cc11001100.diySimpleRpcFramework.util; import java.net.Socket; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * @author CC11001100 */ public class RpcLogUtil { private static String now() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss.SSS" )); } public static void info(Socket socket, Object... messages) { String remoteAddress = socket.getRemoteSocketAddress().toString(); StringBuilder sb = new StringBuilder(); sb.append( "[" ).append(now()).append( "]" ) .append( " - " ).append(remoteAddress).append( ":" ).append( " - " ); for (Object msg : messages) { sb.append(msg.toString()); } System.out.println(sb.toString()); } } |
因为代理是使用JDK提供的代理机制实现的,这种代理方式要求必须要定义一个接口然后实现它,所以首先定义一个接口:
1 2 3 4 5 6 7 | package cc11001100.diySimpleRpcFramework.rpcServiceProvider; public interface FooRpcServiceProvider { int add( int a, int b); } |
然后实现它:
1 2 3 4 5 6 7 8 9 10 11 12 13 | package cc11001100.diySimpleRpcFramework.rpcServiceProvider; /** * @author CC11001100 */ public class FooRpcServiceProviderImpl implements FooRpcServiceProvider { @Override public int add( int a, int b) { return a + b; } } |
测试一下此RPC server是否可用,先启动一个rpc server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package cc11001100.diySimpleRpcFramework.test; import cc11001100.diySimpleRpcFramework.rpcServiceProvider.FooRpcServiceProviderImpl; import cc11001100.diySimpleRpcFramework.util.RpcServiceProviderUtil; import java.io.IOException; /** * 启动rpc server端 * * @author CC11001100 */ public class FooRpcServiceProviderServerTest { public static void main(String[] args) throws IOException { RpcServiceProviderUtil.startService( new FooRpcServiceProviderImpl(), 10086 ); } } |
然后启动client去调用server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package cc11001100.diySimpleRpcFramework.test; import cc11001100.diySimpleRpcFramework.rpcServiceProvider.FooRpcServiceProvider; import cc11001100.diySimpleRpcFramework.rpcServiceProvider.FooRpcServiceProviderImpl; import cc11001100.diySimpleRpcFramework.util.RpcServiceProviderUtil; import java.util.Random; import java.util.concurrent.TimeUnit; /** * rpc客户端调用 * * @author CC11001100 */ public class FooRpcServiceProviderClientTest { public static void main(String[] args) throws InterruptedException { FooRpcServiceProvider foo = RpcServiceProviderUtil.wrap(FooRpcServiceProviderImpl. class , "localhost" , 10086 ); Random random = new Random(); while ( true ) { int a = random.nextInt( 10 ); int b = random.nextInt( 10 ); int result = foo.add(a, b); System.out.printf( "%d + %d = %d\n" , a, b, result); TimeUnit.MILLISECONDS.sleep(random.nextInt( 900 ) + 100 ); } } } |
控制台输出:
我写的rpc server精通10以内加法,这点client可以作证。
.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架