RPC远程过程调用可以说是分布式系统的基础。本文将演示通过Java在普通rpc调用中发生的情况。阿芬曾经在网上看到有人问为什么RPC叫远程过程调用,而不叫RMC远程方法调用。但是阿芬认为RPC这个名字是合理的,远程调用是一定的过程,不一定是具体的方法。(后面看第一版的代码就明白了)整个过程可以用一句话概括:机器A通过网络与机器B建立连接,A向B发送一些参数,B执行某个处理,并将结果返回给A。"写代码之前先说一个背景,假设我们有一个商品类:publicclassProductimplementsSerializable{privateIntegerid;privateStringname;publicProduct(Integerid,Stringname){this.id=id;this.name=name;}//toString()//getset方法}有一个产品服务接口:publicinterfaceIPProductService{ProductgetProductById(Integerid);}服务端有一个产品服务接口的实现类:publicclassProductServiceImplimplementsIPproductService{@OverridepublicProductgetProductById(Integerid){//其实这里应该查询数据库获取数据,下面简化为returnnewProduct(id,"手机");}}接下来我们通过客户端向服务端发送一个商品id,服务端获取到id后通过商品服务类获取商品信息,并返回给客户端:publicclassClient{publicstaticvoidmain(String[]args)throwsException{//CreateSocketSocketsocket=newSocket("127.0.0.1",8888);//获取outputstreamByteArrayOutputStreambaos=newByteArrayOutputStream();DataOutputStreamdos=newDataOutputStream(baos);//将商品Id通过网络终端传递给服务dos.writeInt(123);socket.getOutputStream().write(baos.toByteArray());socket.getOutputStream().flush();//读取服务器返回的商品信息DataInputStreamdis=newDataInputStream(socket.getInputStream());Integerid=dis.readInt();//产品idStringname=dis.readUTF();//产品名称Productproduct=newProduct(id,name);//通过服务器返回的产品信息生成产品System.out.println(product);//关闭streamresource为方便阅读,dos.close();baos.close();socket.close();}}publicclassServer{privatestaticbooleanrunning=true;publicstaticvoidmain(String[]args)throwsException{//建立服务器SocketServerSocketss=newServerSocket(8888);//持续监听并处理客户端请求while(running){Socketsocket=ss.accept();process(socket);socket.close();}ss.close();}privatestaticvoidprocess(Socketsocket)throwsException{InputStreamis=socket.getInputStream();OutputStreamos=socket.getOutputStream();DataInputStreamdis=newDataInputStream(is);DataOutputStreamdos=newDataOutputStream(os);//读取客户端发送的idIntegerid=dis.readInt();//调用生成产品的服务类IProductServiceservice=newProductServiceImpl();Productproduct=service.getProductById(id);//将商品信息写回客户端dos.writeInt(id);dos.writeUTF(product.getName());dos.flush();dos.close();dis.close();os.close();is.close();}}上面是RPC远程调用的原始简单版本,可以看到网络代码硬写在客户端,网络部分该代码与getProductById()结合使用。如果想把其他方法改成远程调用,就得重写联网的代码,很麻烦。在实际使用中,我们会编写各种远程调用。比如IProductService接口以后可能会扩展成这样:不得不想一个方法为所有方法嵌入一个通用的网络连接代码。应该如何嵌入?这里我们可以使用代理模式。Java中很多优秀的框架都采用代理模式进行代码嵌入,比如Mybatis。它通过代理的方式,将JDBC连接部分的代码嵌入到sql语句周围,让我们可以专注于编写sql。首先,需要修改服务器端的代码。由于多个方法共享一组联网的代码,所以我们需要在服务器端识别调用了哪个方法:Socketsocket)throwsException{//获取输入流、输出流InputStreamis=socket.getInputStream();OutputStreamos=socket.getOutputStream();ObjectInputStreamois=newObjectInputStream(is);ObjectOutputStreamoos=newObjectOutputStream(os);//获取this的方法名remotecallStringmethodName=ois.readUTF();//获取本次远程调用方法的参数类型Class[]parameterTypes=(Class[])ois.readObject();//获取具体的参数对象Object[]args=(Object[])ois.readObject();//创建一个产品服务类实例,(以后可以在这里继续优化)IProductServiceservice=newProductServiceImpl();//根据远程获取的方法名和参数,调用对应的方法Methodmethod=service。getClass().getMethod(methodName,parameterTypes);Productproduct=(Product)method.invoke(service,args);//将结果写回客户端oos.writeObject(product);oos.close();ois.close();socket.close();}}然后在客户端,我们新建一个代理类,并提供getStub方法获取代理类。使用JDK的动态代理需要三个参数,一个是类加载器,一个是接口的class类,最后一个是InvocationHandler实例。JDK动态代理背后的逻辑是这样的:JVM会根据接口的class类动态创建一个代理类对象。这个代理对象实现了传入的接口,也就是说,它有接口中所有方法的实现。方法的具体实现可以由用户指定,即调用InvocationHandler的invoke方法。invoke方法中有三个参数,分别是proxy代理类,方法调用的方法,args调用方法的参数。我们可以在invoke方法中增强具体的实现方法,本例就是进行网络调用。publicclassStub{publicstaticIProductServicegetStub(){InvocationHandlerh=newInvocationHandler(){@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{//与服务端建立Socket连接Socketsocket=newSocket("127.0.0.1",8888);Object=newObjectOutputStreamo(socket.getOutputStream());//获取远程调用的方法名StringmethodmethodName=method.getName();//获取远程调用方法的参数类型Class[]parametersTypes=method.getParameterTypes();//获取方法名传递给服务器oos.writeUTF(methodName);//方法参数类型传递给服务器oos.writeObject(parametersTypes);//方法参数传递给服务器oos.writeObject(args);oos.flush();//获取远程调用的返回结果();socket.close();returnproduct;}};Objecto=Proxy.newProxyInstance(IProductService.class.getClassLoader(),newClass[]{IProductService.class},h);return(IProductService)o;}}这个新版本比第一个版本更好,但实际上可以继续优化。现在我们的代理只能返回IProductService的实现类,我们得想办法让它返回任意类型的服务实现类。这个想法类似于远程调用方法。远程调用方法时,我们将方法名、参数类型、参数传递给服务器;现在要动态创建一个服务类,我们可以将服务接口的名称传递给服务器。服务器获取到远程接口的名称后,就可以从服务注册中心中找到对应的服务实现类。至于服务实现类如何注册到服务注册中心,这里有一个思路:可以考虑使用Spring注解注入。这类似于我们通常编写spring代码的方式。创建服务实现类后,我们会添加注解@Service,这样我们就可以使用@Service遍历Bean,在接收到远程调用后找到对应的实现类。.总结如果要搭建一个简单的RPC框架,阿芬认为有4点:代理问题服务实例化问题序列化问题通信问题本文主要关注前两个,代理问题和服务实例化问题,我们来回顾一下:第一,每个远程方法的组网问题都是通过代理模式解决的。服务实例化的问题是通过传递方法名、方法参数类型、参数以及后面提到的直接接口名来解决的。这背后的原理就是反射,传递这些参数也是为了反射。作为另外两点的延伸,通过网络传输数据无法避免序列化问题。Java自带的序列化方式,效率比较低。如果我们要搭建RPC框架,可以考虑引入其他第三方序列化框架。通信问题也很重要,现有的RPC框架使用不同的通信协议,各有优缺点。不过这部分手动实现可能比较困难,可以根据语言本身封装的API来选择。
