RPC基本介绍
设计模式
算法与数据结构
1)RPC(Remote Procedure Call)
远程过程调用,是一个计算机通讯协议,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外的为这个
交互作用编程;
2)2个或多个应用程序都分布在不同的服务器上,他们之间的调用都像是本地方法调用一样;
3)常用的RPC框架有: 比较知名的如:
阿里的Dubbo
google的gRPC
Go语言的rpcX
Apache的thrift
Spring旗下的Spring Cloud
4)基本流程
1.请求 2.编码 3.发送 10.接收 11.解码 12.结果
4.接收 5.解码 6.调用api 7.响应 8.编码 9.发送
在RPC中的2个术语:
Client: 服务消费者
Server: 服务提供者
5)RPC调用流程说明
(1)服务消费方(Client)以本地调用方式调用服务
(2)client stub接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
(3)client stub将消息进行编码并发送到服务端
(4)server stub收到消息后进行解码
(5)server stub根据解码结果调用本地服务
(6)本地服务执行并将结果返回给server stub
(7)server stub将返回导入结果进行编码并发送至消费方
(8)client stub接收到消息并进行解码
(9)服务消息方(Client)得到结果
小结: RPC的目标就是将2-8这些步骤都封装起来,用户无需关心这些细节,就可以像调用本地方法一样即可完成远程服务调用;
6)RPC 实现的4个部分
customer: 客户端(消费者)
ClientBootstrap // 启动一个消费者-->NettyClient
provider: 服务端(提供者)
HelloServiceImpl类
ServerBootstrap // 启动一个服务提供者-->NettyServer
netty: 底层封装
客户端:
1.NettyClient
2.NettyClientHandler
服务端:
1.NettyServer
2.NettyServerHandler
publicinterface: 共有的接口
HelloService
7)最核心的地方: 如何去创建一个代理对象(代理模式)
8)编码器和解码器:
(1)编码器:StringEncoder
(2)解码器:StringDecoder
(3)业务处理器: BusinessHandler
客户端发送过来请求信息时,每次发送消息必须以 某个协议字符串开头;
9)启动层:包上一层
startServer(根据传递的参数调用不同的方式,这个是对外暴露的接口):
startServer0
10)先写NettyClientHandler(下面2个和它有关)
(1)下面2个和NettyClientHandler有关
1:NettyClient
2:ClientBootstrap
(2)(重点)synchronized Object call: 也就是实现了Callable接口
调用有关;
被代理对象调用,发送数据给服务端 --> wait --> 等待唤醒 --> 返回结果;
因此不用犹豫不用彷徨,加上synchronized;
(3)3个基本接口
1:channelActive(与服务器的连接创建后,就会被调用,ctx传递给context,做成属性)
2:synchronized channelRead(收到服务器的数据后调用)
理解为何 要用notify: 去唤醒等待的线程:
client--->server
客户端Handler有个Call方法,客户端先调用Call方法把数据发送给服务端(通过反射机制调用),
发送完毕后,消息还没有马上回来,因此在Call里面我会进行一个wait;
基于Netty的原则,服务器端会送数据是会送到channelRead方法, Call中一直在等待(wait),还没有返回呢,
通过代理对象调用Handler的Call方法,调用之后把信息发送过去就在这等待,数据是发送给Handler的另外一个方法,
因此我需要用notify来唤醒它,唤醒后,代码接着继续执行,把结果给到了result,
这个时候,我们再把结果返回给代理对象;
因此,等在channelRead中唤醒call方法时,结果已经拿到了
因此: channelRead和call是有一个同步的关系的,
因此call中用了wait,
channelRead 和notify,
并且call和channelRead中也要在方法前加上synchronized
3:exceptionCaught
我的疑问是:
wait后, 是线程阻塞这了吗? 如果是阻塞的,那么需要谁去唤醒接着执行呢?
channelRead是在哪个线程被调用的呢?(这个通过打印当前线程信息即可知道)
(4)3个属性
1:ChannelHandlerContext context; // 因为要在 其它方法里面 使用到当前Handler里面的上下文,因此需要保存下上下文当成属性
2:String result; // 将来客户端调用后,返回的结果
3:String patam; // 客户端调用方法时传递进来的参数
客户端发送数据前,将这个参数传递给服务端,服务端即可知道想要调用的是什么方法,
反射一下调用相关方法,然后客户端即可在channelRead中得到数据
11)方法调用先后顺序
channelActive
setPara
call-->wait后阻塞
channelRead-->等待返回,并且notify
call-->在notify后,继续执行
如果出了异常,就调用exceptionCaught
12)synchronized的理解
(1)NettyClientHandler可能在每个workerGroup中使用,而且如果是业务线程池去执行任务,那么
加上synchronized在方法前,就只能有一个线程去访问到这个synchronized;
(2)试试多个线程去加和减去一个全局变量???
13)NettyClientHandler.java
也是有一个线程池,因为它只连接一个服务器,因此thread name每一个都是一样的
14)最难的地方: 创建代理对象, getBean. 使用代理模式获取代理对象
serivceClass: 要创建的哪个service对象
providerName: 与服务器通讯时的协议头是什么
Thread.currentThread().getContextClassLoader():
获取类的加载器,用当前线程去获取;
new Class>[]{serivceClass}:
获取类的一个实例
(proxy, method, args) : 将来代理对象将要调用的方法得到的东西
proxy: 代理对象本身
method: 方法
args: 将来传递的参数-->‘协议头’ 传递进来的信息,如:
providerName: 协议头
args[0]: 就是客户端调用api时, 如: hello(xxx) 里面的xxx参数
用lambda表达式进行调用:
(proxy, method, args)->{
... // 这段代码客户端每调用一次,就是会进入到该代码块,反复执行的!
}
返回值: return executor.submit(client).get();
submit(client)这个是一个Callable对象,也就是接收的是实现了Callable接口的对象,
刚好就是我们的client;
然后.get()它的结果:
这个get是做什么的呢? 就是Handler中实现的call方法的return的结果拿到
15)ClientBootstrap.java
(1)里面写好协议头
(2)创建一个消费者: NettyClient
(3)获取代理对象,进行强转一下:
HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
16)思考:既然RPC实现的是调用远程的方法跟本地的一样,所以没必要搞成异步的
17)服务提供方是并不是同一个实例
private int count = 0;
发现每次打印都是打印的1;
private static int count = 0;
不管你服务端产生多少个这样的对象实例,都是共用的;
18)channelRead和call方法必须有一个同步的控制;
19).hello真正被调用的地方在于: getBean里面的 ->{xxx}这里:
只要通过代理对象去调用,那么就可以进入
20)把业务放到线程池中执行,并得到它的结果
executor.submit(client).get()