当前位置: 首页 > 科技观察

RPC更好,还是HTTP更好?不要弄错!

时间:2023-03-20 15:30:41 科技观察

图片来自抱图网。以下是RPC的演进史。一开始是RMI,但是之前仅限于Java与Java之间的通信,不能跨语言;要知道xml很大,占用网络资源也很多。然后是http+json,非常轻量级,需要写很多重复的非业务代码;然后是框架阶段,Google的GRPC,Facebook的Thrift(现在交给Apache了),阿里的Dubbo,最后是SpringCloud用的Restful。在这里补充一下,不要说RPC好,也不要说HTTP好,两者各有千秋。本质上,两者是可读性和效率之间,通用性和易用性之间的选择。最终谁能发展得更好还不好说。RPC流程图下面是网上的一般流程图。当发起请求时,调用者通过动态代理将请求的参数序列化,然后通过网络到达被调用者。被调用者获取参数并进行反序列化更改。然后在本地反射调用方法,最后将计算结果序列化返回给调用者,反序列化调用方法获取值。整个就是这样一个过程:下面是这个手写RPC的流程图:用户在客户端发起访问rpc-user-service服务的请求,rpc-user-service调用rpc-order-service服务器端查询订单信息的服务。它还将经历序列化和反序列化过程。代码实现①serverrpc-order-serviceorderservicerpc-order-service,这是一个maven工程,这是一个父pom,然后创建两个子工程,order-api和order-provider。这两个也是maven项目,项目结构如下:②order-apiorder-api是一个契约,定义了接口,order-provider需要实现。然后打成jar包上传到nexus私服,因为rpc-user-service也需要引用它,调用订单服务提供的合约。RpcRequest类是定义rpc-user-service请求rpc-order-service时,告诉order调用哪个类的哪个方法,传入什么参数。这里我没有搭建私服。一般公司都有私人服务器。可以在自己的电脑上使用install安装到maven本地仓库:@DatapublicclassRpcRequestimplementsSerializable{privateStringclassName;privateStringmethodName;privateObject[]args;}③order-provider先看项目中的类,有很多类,后面我们会分别解释。首先,服务层实现契约。既然是实现,先参考order-api的pom:com.jackorder-api1.0-SNAPSHOTImplementationclassOrderServiceImpl.class://注解bean加载后,bean信息会被保存到哈希表@JackRemoteServicepublicclassOrderServiceImplimplementsIOrderService{@OverridepublicStringqueryOrderList(){return"thisisrpc-order-servicequeryOrderListmethod";}@OverridepublicStringorderById(Stringid){return"thisisrpc-order-serviceorderByIdmethod,paramis"+id;}}细心的朋友发现这里打了一个自定义注解@JackRemoteService。这个注解的作用是在bean加载完成后上传bean的信息。保存到哈希表以供以后反射调用。@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Componentpublic@interfaceJackRemoteService{}注解是一个标记函数,标记后需要有人识别。这里需要实现BeanPostProcessor接口,重写里面的postProcessAfterInitialization方法。这个方法的作用是检查当前加载的bean是否有JackRemoteService注解,如果有,则将bean中的所有方法添加到哈希表中。/***@authorjackxu*加载bean后将bean信息保存到哈希表中*/@ComponentpublicclassInitialMerdiatorimplementsBeanPostProcessor{@OverridepublicObjectpostProcessAfterInitialization(Objectbean,StringbeanName)throwsBeansException{if(bean.getClass().isAnnotationPresent(JackRemoteService.class)){Method[]methods=bean.getClass().getDeclaredMethods();for(Methodmethod:methods){//接口名称。方法名Stringkey=bean.getClass().getInterfaces()[0].getName()+"."+method.getName();BeanInfobeanInfo=newBeanInfo();beanInfo.setBean(bean);beanInfo.setMethod(method);Mediator.getInstance().put(key,beanInfo);}}returnbean;}}哈希表的定义是Mediator.class,key是类名。方法名称:publicclassMediator{publicMapmap=newConcurrentHashMap<>();privateMediator(){}privatestaticvolatileMediatorinstance;publicstaticMediatorgetInstance(){if(instance==null){synchronized(Mediator.class){if(instance==null){instance=newMediator();}}}返回实例;}publicMapgetMap(){returnmap;}publicvoidput(Stringkey,BeanInfobeanInfo){map.put(key,beanInfo);}}最后,在所有bean加载完成后,启动一个socketmonitor,这样服务端就写好了,等待客户端的请求Spring有一些内置的Events,当某些操作完成时会发出某些事件操作。比如监听ContextRefreshedEvent事件,当所有的bean都初始化加载成功后,就会触发该事件。实现ApplicationListener接口接收监听动作,然后自己写逻辑。SocketServerInitial.class://spring容器启动后,一个ContextRefreshedEvent@ComponentpublicclassSocketServerInitialimplementsApplicationListener{//ThreadpoolprivatefinalExecutorServiceexecutorService=newThreadPoolExecutor(5,10,0L,TimeUnit.Quutn.MILLISECONDS,newArrayBdefaultThreadFactory(),newThreadPoolExecutor.AbortPolicy());@OverridepublicvoidonApplicationEvent(ContextRefreshedEventcontextRefreshedEvent){//启动服务ServerSocketserverSocket=null;try{ServiceSocket=newServerSocket(8888);while(true){Socketsockceptorcutorcute=serverSocket(exe;exe)(newProcessorHandler(socket));}}catch(Exceptione){e.printStackTrace();}finally{//关闭socketif(serverSocket!=null){try{serverSocket.close();}catch(IOExceptione){e.printStackTrace();}}}}}在线程池中执行的方法是先将接收到的socket请求反序列化为RpcRequest,然后根据传入的接口和方法在哈希表中找到方法,然后通过反射调用,最后返回结果。/***@authorjackxu*/publicclassProcessorHandlerimplementsRunnable{privateSocketsocket;publicProcessorHandler(Socketsocket){this.socket=socket;}@Overridepublicvoidrun(){ObjectOutputStreamoutputStream=null;ObjectInputStreaminputStream=null;try{inputStream=newObjectInputStream(socket.getInputStream());//反序列化RpcRequestrequest=(RpcRequest)inputStream.readObject();//根据传入的参数执行方法System.out.println("request:"+request);Objectresult=processor(request);System.out.println("response:"+result);//将计算结果写入输出流e.printStackTrace();}finally{//关闭流if(inputStream!=null){try{inputStream.close();}catch(IOExceptione){e.printStackTrace();}}if(outputStream!=null){try{outputStream.close();}catch(IOExceptione){e.printStackTrace();}}}}publicObjectprocessor(RpcRequestrequest){try{Mapmap=Mediator.getInstance().getMap();//接口名称。方法名Stringkey=request.getClassName()+"."+request.getMethodName();//获取方法BeanInfobeanInfo=map.get(key);if(beanInfo==null){returnnull;}//bean对象Objectbean=beanInfo.getBean();//methodMethodmethod=beanInfo.getMethod();//反射returnmethod.invoke(bean,request.getArgs());}catch(Exceptione){e.printStackTrace();returnull;}}}采用BIO传输方式,执行完一个请求后必须执行下一个请求,会导致效率低下,所以采用线程池的方式来解决这个问题,但是如果请求过多,还是会出现成为拥堵。最好的方法是使用Netty来实现RPC。④Clientrpc-user-servicerpc-user-service是一个springboot项目,因为最后我们会通过restful来调用。如果用ssm搭建太慢,我们先看看项目的整体结构。让我们从控制器层开始。首先,引用接口order-api。因为我们已经安装在本地的maven仓库中,所以我们可以直接引用pom。com.jackorder-api1.0-SNAPSHOT@RestControllerpublicclassUserController{//这里的作用是封装接口进入代理对象@JackReferenceprivateIOrderServiceorderService;@JackReferenceprivateIGoodServicegoodService;@GetMapping("/test")publicStringtest(){returnorderService.queryOrderList();}@GetMapping("/get")publicStringget(){returngoodService.getGoodInfoById(1L);}}我们看到这里还有一个自定义的注解JackReference,它的作用就是把这个注解标记的接口变成一个代理对象。@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Componentpublic@interfaceJackReference{}我们还是按照例子。在bean加载之前,这里是postProcessBeforeInitialization方法,将JackReference注解的接口设置为代理对象。@ComponentpublicclassReferenceInvokeProxyiimplementsBeanPostProcessor{@AutowiredRemoteInvocationHandlerinvocationHandler;@OverridepublicObjectpostProcessBeforeInitialization(Objectbean,StringbeanName){//获取所有字段Field[]fields=bean.getClass().getDeclaredFields();for(Fieldfield.fields){class)){field.setAccessible(true));Objectproxy=Proxy.newProxyInstance(field.getType().getClassLoader(),newClass[]{field.getType()},invocationHandler);try{field.set(bean,proxy);}catch(IllegalAccessExceptione){e.printStackTrace();}}}returnbean;}}我们知道orderService.queryOrderList()本地没有这个实例,无法执行,所以代理对象所做的就是将要执行的方法和参数封装进去Rpc请求。然后通过Socket发送给服务器,然后拿到返回的数据,这样我们看起来就像是在本地执行的一样。其实代理对象为我们做了很多事情。@ComponentpublicclassRemoteInvocationHandlerimplementsInvocationHandler{@Value("${rpc.host}")privateStringhost;@Value("${rpc.port}")privateintport;@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args){RpcRequestrequest=newRpcRequest();request.setArgs(args);request.setClassName(method.getDeclaringClass().getName());request.setMethodName(method.getName());returnsend(request);}publicObjectsend(RpcRequestrequest){ObjectOutputStreamoutputStream=null;ObjectInputStreaminputStream=null;try{Socketsocket=newSocket(host,port);//IO操作outputStream=newObjectOutputStream(socket.getOutputStream());outputStream.writeObject(request);outputStream.flush();inputStream=newObjectInputStream(socket.getInputStream());returninputStream.readObject();}catch(Exceptione){e.printStackTrace();returnull;}最后{//关闭流if(inputStream!=null){try{inputStream.close();}catch(IOExceptione){e.printStackTrace();}}if(outputStream!=null){try{outputStream.close();}catch(IOExceptione){e.printStackTrace();}}}}}测试首先启动服务器,服务器的代码是这样写的,需要添加ComponentScan扫描包:/***@authorjackxu*/@Configuration@ComponentScan("com.jack")publicclassBootstrap{publicstaticvoidmain(String[]args){ApplicationContextapplicationContext=newAnnotationConfigApplicationContext(Bootstrap.class);}}已经运行,等待客户端请求:客户端是一个springboot项目,正常启动即可:@SpringBootApplicationpublicclassRpcUserServiceApplication{publicstaticvoidmain(String[]args){SpringApplication.run(RpcUserServiceApplication.class,args);}}也运行:然后打开浏览器访问,成功得到结果:服务端也会打印出相应的日志,一个完整的RPC请求结束。本文源码在Github上:rpc-user-service:https://github.com/xuhaoj/rpc-user-servicerpc-order-service:https://github.com/xuhaoj/rpc-order-service最后总结一下,我们使用的是多线程+B??IO的模式。有兴趣的小伙伴可以改成Netty方式。另外这里请求的地址是硬编码的,没有负载均衡。一般与注册中心配合使用。更完善的还会有监控等功能。真正的Dubbo做了很多事情。本文只讨论两个服务之间通信的研究!作者:晓杰博士编辑:陶家龙来源:juejin.cn/post/6994803207838892063