本文转载自微信公众号《大鱼仙人》,作者大鱼仙人。转载本文请联系大禹仙人公众号。在序言之前,服务被暴露并被引用。服务提供者暴露服务,服务消费者引用服务。最后一步是消费者和提供者之间的调用。调用是真正的通信RPC过程,既然涉及到通信,就涉及到相应的客户端和服务端之间的交互协议,协议,以及序列化和反序列化机制。根据参数,参数类型,告诉服务端调用哪个接口,让服务端知道调用哪个接口,服务端就可以执行应用层协议的交互。一般有三种形式,分别是:fixedLength形式、特殊字符区间形式和header+body形式,dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等协议,但是dubbo官网推荐我们使用Dubbo协议的Dubbo协议,随便看看吧,下面主要了解下相应的特性。Dubbo协议采用单长连接,NIO异步通信,适用于数据量小,服务调用并发量大,服务消费者机器数远大于服务提供者机器数的情况;不适合传输大量数据的服务,比如文件传输、视频传输等,除非请求量很低。适用范围:传入和传出的参数数据包较小(建议小于100K),消费者数量多于提供者,单个消费者无法填满提供者。尽量不要使用dubbo协议来传输大文件或者非常大的字符串。为什么消费者应该多于提供者?因为dubbo协议使用的是单长连接,假设网络是千兆网卡(1024Mbit=128MByte),根据测试经验数据,每个连接最多只能填7MByte(不同环境可能会有差异)一样,对于参考),理论上,一个服务提供者需要20个服务消费者来填满网卡。消费者很多,整个网站可能都在访问服务。如果使用常规服务,服务提供商很容易不堪重负。通过单一连接,保证单一消费者不会压垮提供者。长连接减少连接握手验证等,使用异步IO复用线程池,防止网络崩溃。向接口添加方法对客户端没有影响。如果客户端不需要该方法,客户端不需要重新部署;向输入参数和结果集添加属性对客户端没有影响。如果客户端不需要新的属性,则不需要重新部署还有一点需要特别注意的是序列化。说到交互,就会涉及到对象的传递,这就会涉及到序列化。序列化是将内存中的数据对象转换为二进制流,用于数据持久化和网络传输,将数据对象转换为二进制流的过程称为对象序列化(Serialization)。相反,将二进制流恢复为数据对象的过程称为反序列化。序列化需要保留足够的信息来恢复数据对象,但为了节省存储空间和网络带宽,序列化后的二进制流应该尽可能小。常见的序列化方式有3种:Java原生序列化、Hessian序列化、Json序列化Java原生序列化:Java类通过实现Serializable接口来实现本类对象的序列化。建议为实现Serializable接口的类设置serialVersionUID字段的值。实现,包括类名、接口名、方法和属性等,自动生成serialVersionUID。如果类的源代码被修改,重新编译后serialVersionUID的值可能会改变。因此,实现Serializable接口的类必须显式定义serialVersionUID属性的值。修改类时,需要根据兼容性决定是否修改serialVersionUID值:如果是兼容升级,请不要修改serialVersionUID字段,以免反序列化失败。如果是不兼容升级,需要修改serialVersionUID值,避免反序列化混淆。Hessian序列化:其实现机制以数据为中心,类型信息方式简单。就像Integera=1,hessian会像I1一样序列化成stream,I表示int或者Integer,1是数据内容。对于复杂的对象,通过Java的反射机制,Hessian将对象的所有属性序列化为一个Map,并且在序列化过程中,如果之前出现过一个对象,Hessian会直接插入一个块比如Rindex来代表一个引用位置,这样就节省了重新序列化和反序列化的时间在父类和子类有同名成员变量的情况下,Hessian序列化时,先序列化子类,再序列化父类,所以反序列化了转换结果会导致子类的同名成员变量被父类的值覆盖。Json序列化:是一种轻量级的数据交换格式。JSON序列化是将数据对象转换为JSON字符串。类型信息在序列化过程中被丢弃,所以反序列化只有在反序列化时提供类型信息才能准确反序列化。与前两种方式相比,JSON的可读性更强,也更容易调试。序列化通常通过网络传输对象,而对象往往包含敏感数据,因此攻击者可以巧妙地利用反序列化过程构造恶意代码,使程序在反序列化过程中执行任意代码。Java项目中广泛使用的ApacheCommonsCollections、Jackson、fastjson等都出现了反序列化漏洞。如何防止这种黑客攻击?一些对象的敏感属性不需要序列化和传输。您可以添加transient关键字以避免将此属性信息转换为序列化二进制流。如果必须传输对象的敏感属性,可以采用对称和非对称加密方式进行独立传输,然后再使用一种方法将属性恢复到对象。总之就是要有一定的预防意识。接下来我们要分析的是调用过程。我们来看看官网的流程图。首先,服务消费者通过代理对象Proxy发起远程调用,然后通过网络客户端Client发送编码后的请求。它被发送到服务提供者的网络层,即Server。服务器收到请求后,首先要做的就是对数据包进行解码。然后将解码后的请求发送给调度器Dispatcher,然后调度器将请求调度到指定的线程池,最后由线程池调用具体的服务。这是一个远程调用请求的发送和接收过程。整个调用环节大致分为三步。我们按照以下步骤来分析一下:1.消费者发起调用请求2.提供者接收并处理请求3.消费者处理并响应。消费者发起调用请求,消费者调用Invoker时,实际上调用的是一个Java动态代理生成的代理对象。代理对象通过Cluster层的路由和负载均衡找到服务节点,将调用参数封装成Request形式,通过NettyClient将数据序列化,通过Netty发送给相应的服务提供者。调用具体接口会调用生成的代理类,代理类会生成一个RpcInvocation对象来调用MockClusterInvoke#invoke方法,包括方法名、参数类和参数值。其实最后调用的是InvocationHandlerGuys中的AbstractClusterInvoker#invoker方法入口,这个类中的invoke会获取调用结果,并将结果返回给调用者。InvokerInvocationHandler中invoker成员变量的类型为MockClusterInvoker,服务降级逻辑封装在MockClusterInvoker内部。让我们快速看一下:只看服务降级。首先,当然,这不是服务调用的重点。上面的代码就是AbstractInvoker类中的invoke方法。注释是准备RPC的调用列表,然后才是真正的调用。并返回结果,如果是异步,则等待结果返回;重点是第二步的doInvokeAndReturn,点进去看到确实执行了doInvoke方法,而这个方法是本类中的一个抽象方法,需要子类提供实现,我们在DubboInvoker中看一下。以上代码包含了Dubbo对同步和异步调用的处理逻辑。理解以上代码后,你会对Dubbo的同步和异步调用方式有更深入的理解。Dubbo实现同步和异步调用的关键点是谁调用了CompletableFuture的get方法。同步调用方式是框架自己调用CompletableFuture的get方法。在异步调用方式下,方法由用户调用。当服务消费者还没有收到调用结果时,用户线程在调用get方法时会被阻塞。同步调用方式下,框架获取到DefaultFuture对象后,会立即调用get方法等待。在异步模式下,将对象封装成一个FutureAdapter实例,并将FutureAdapter实例设置在RpcContext中供用户使用。接下来我们来看一个客户端HeaderExchangeClient。HeaderExchangeClient中的很多方法只有一行代码,即调用HeaderExchangeChannel对象Signature方法的同一个方法。那么HeaderExchangeClient有什么用呢?答案是在封装了一些心跳检测的逻辑,来到它的内部属性HeaderExchangeChannel之后,大家终于看到了Request语义。上面的方法首先定义了一个Request对象,然后将该对象传递给NettyClient的send方法。对于后续的调用,需要注意的是send方法并没有在NettyClient中实现。该方法继承自父类AbstractPeer。看到其子类AbstractClient类中的send实现,然后是发送NettyChannel的send。提供者接收并处理请求。默认情况下,Dubbo使用Netty作为底层通信框架。Netty检测到有数据入站后,会先通过解码器对数据进行解码,并将解码后的数据打包成一个请求对象,传递给下一个入站处理器的指定方法。解码器将数据包解析成Request对象后,NettyHandler的messageReceived方法会接收到该对象并向下传递。在此期间,对象会依次传递给NettyServer、MultiMessageHandler、HeartbeatHandler和AllChannelHandler。最后AllChannelHandler将对象封装成Runnable实现类对象,并将Runnable放入线程池中执行后续调用逻辑。了解一下Dubbo的线程调度模型:背景是如果一个处理事件执行的很快,此时可以直接在IO线程上执行,但是如果处理比较耗时,比如逻辑可能会发起一个DB查询或HTTP请求。这时候事件处理逻辑不应该在IO线程上执行,而是直接派发到线程池中执行,原因很简单。IO线程主要用于接收请求。如果IO已满并阻塞,则无法接收新请求。比如一个大公司,业务量很大,核心部门A主要负责分销业务的处理,其他部门单独处理。一些很简单的业务处理,连分配任务的时间都没有,核心部门直接处理。想一想,发送一个任务一秒,如果处理这个业务只需要0.5秒,那么就不用发送这个业务了,自己处理就好,所以就有了这个线程调度模型。Dispatcher是一个线程调度器,但它本身不具备线程调度能力。职责是创建具有线程调度能力的ChannelHandler,如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHandler等。Dubbo支持5种不同的线程调度策略。Dubbo默认使用alldispatch策略,就是把所有的消息都派发到线程池中处理的逻辑我觉得不用详细分析了。无非就是封装成一个Runnable,交给handler分发的线程处理,然后把结果封装成response,返回给consumer。响应数据解码后,Dubbo会将响应对象派发到线程池上。需要注意的是,线程池中的线程不是用户的调用线程,所以要想办法将响应对象从线程池线程传递给用户线程。一般来说,服务消费者会同时调用多个服务。每个用户线程发送请求后,都会调用不同DefaultFuture对象的get方法等待。一段时间后,服务消费者的线程池会收到多个响应对象。这时候就有一个问题需要考虑,如何将每一个response对象无错的传递给对应的DefaultFuture对象。消费者收到提供者的响应,解码,放入线程分发器,放入线程池。放入线程池的是一个DefaultFuture对象,里面包含了响应结果。在上面第一步发起调用请求的过程中,负载均衡后的调用是通过RpcInvocation代理对象使用DefaultFuture.get()方法异步获取响应内容,这也是RPC远程调用变化的方式从同步到异步。答案是通过电话号码。当创建一个DefaultFuture时,需要传入一个Request对象,此时DefaultFuture可以从Request对象中获取调用号,并将
