概述
本门主要以dubbo为切入点,简单讲讲dubbo的简单使用和大致原理。后面再通过给大家简单搭建一个mini版dubbo,通过具体代码来加深大家对dubbo等rpc框架的大致原理。
有兴趣的同学可以基于本项目进行衍生扩展开发,也可以去dubbo官网深入学习。
开门篇
一、dubbo的常规使用
1、服务提供者通过@service进行服务暴露:
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello, " + name;
}
}
2、服务调用者通过 @Reference来引用服务:
@Reference
private HelloService helloService;
public String sayHello(String name){
return helloService.sayHello(name);
}
在了解基本使用后,大家是否会很好奇这中间过程是怎么样的,为什么基于一个注解便可以轻松完成调用了。
能够这么轻松的使用,主要还得感谢dubbo框架封装得很好,让用户通过注解便可以简单使用了。
其dubbo的底层如下:
别走,本篇文章不会一点点去剖析那张图,只是给大家过过眼,了解一下底层。
二、dubbo大致原理
整体流程:
- 服务提供方会向注册中心(zookeeper)注册服务。
(存入接口全限定类名和服务提供方地址信息等)
-
服务调用方会向注册中心拉取服务列表并缓存到本地。
-
服务调用方根据接口信息和服务列表向提供者发起调用请求,并接收数据。
4、若服务列表发生更新,注册中心会去通知调用者去更新服务列表。
5、监视器会记录调用次数、调用时长等信息。
调用方与提供方的交互:
1、服务消费方(client)以本地调用方式调用服务;
2、client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
3、client stub找到服务地址,并将消息发送到服务端;
4、server stub收到消息后进行解码;
5、server stub根据解码结果调用本地的服务;
6、本地服务执行并将结果返回给server stub;
7、server stub将返回结果打包成消息并发送至消费方;
8、client stub接收到消息,并进行解码;
9、服务消费方得到最终结果。
总而言之:
(1)调用者会调用接口方法,底层会根据接口方法信息去获取到服务方地址信息,然后向服务方发起调用请求。
(2)提供者接收到调用请求后,会根据信息去执行本地方法,然后将结果值返回给调用者。
插入一句话:
细节只是为了辅助理解,不要过于陷入细节。在理解整体架构后,细节便可由浅及深。
造山篇
ps:本篇将会带你体验创造的乐趣,在创造的过程中去理解rpc框架的大致原理。
本项目的github地址在这里,readme写得比较清晰了,这里主要给大家讲讲整体流程。
项目架构图:
为了简化项目配置以及降低大家的学习难度,本项目中的注册中心基于本地文件来实现。即提供者的地址信息会写入到文件中,调用者会从本地文件中获取提供者信息。
1-1、服务提供者
- 提供者首先向本地注册表中注册信息(接口名-实现类),方便后续接收到调用请求时知道该执行哪个实现类方法。
- 提供者向注册中心注册本机地址信息(map结构:接口名 - List ),方便后期调用者根据接口名获取到服务者地址。
- 根据配置,启动服务器监听请求。(本项目默认采用基于tomcat的http)。
@Component
public class Provider {
@Resource
private LocalRegister localRegister;
@Resource
private IRegister remoteRegister;
@Resource
private ProtocolFactory protocolFactory;
public void toStart() {
//1 本地注册
//{服务名:实现类}
localRegister.registLocal(HelloService.class.getName(), HelloServiceImp.class);
//2 远程注册中心
//{服务名:list<URL>}
URL url = new URL("localhost", 8080);
remoteRegister.registRemote(HelloService.class.getName(), url);
//3 读取配置,按配置启动服务器
IProtocol protocol = protocolFactory.getProtocol();
protocol.start(url);
}
}
1-2、服务提供者所依赖的dubbo-mini-api
基于工厂模式和配置,protocolFactory.getProtocol()会返回一个HTTPProtocol对象,其
protocol.start(url)主要逻辑为:监听端口,接收http请求,将请求转发给HttpServerHandler
@Component
public class HttpServerHandler {
@Resource
private IRegister localRegister;
public void handler(HttpServletRequest req, HttpServletResponse resp) throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
InputStream inputStream = req.getInputStream();
ObjectInputStream ois = new ObjectInputStream(inputStream);
// 读取调用信息
Invocation invocation = (Invocation) ois.readObject();
Class implClass = localRegister.getLocal(invocation.getInterfaceName());
Method method = implClass.getMethod(invocation.getMethodName(), invocation.getParamTypes());
// 调用方法
String result = (String) method.invoke(implClass.newInstance(), invocation.getParams());
// 写回结果
ObjectOutputStream objectOutputStream = new ObjectOutputStream(resp.getOutputStream());
objectOutputStream.writeObject(result);
objectOutputStream.flush();
objectOutputStream.close();
}
}
上述代码的业务逻辑为:
基于本地注册表,根据接口名找到实现类,再通过反射机制执行本地方法,最后将结果值进行返回。
2-1 服务调用者
- 首先,基于接口创建代理类;
- 其次,调用接口方法触发代理类拦截方法;
- 在拦截方法中进行调用请求,并返回结果。
@Component
public class Consumer {
public void toUse() {
// 获取代理类
HelloService helloService = ProxyFactory.getProxy(HelloService.class);
// 调用接口方法,获取返回值
String result = helloService.sayHello("watermelonhit");
System.out.println("===========>" + result);
}
}
2-2 调用者所依赖的dubbo-mini-api
当服务调用方调用接口方法时,就会自动执行该方法(动态代理):
(1)在此方法中会获取到调用的接口信息
(2)然后向注册中心(本地文件)拉取服务方的地址信息
(3)基于负载均衡,从服务列表中获取到一个url
(4)其次向服务方发起调用信息
(5)最后向服务调用方返回结果
@Component
public class ProxyFactory {
private static LoadBalance randomBalance;
private static IRegister remoteRegister;
@Autowired
public ProxyFactory(LoadBalance loadBalance, IRegister remoteRegister) {
ProxyFactory.randomBalance = loadBalance;
ProxyFactory.remoteRegister = remoteRegister;
}
/**
* 当服务调用方调用接口方法时,就会自动执行该方法(动态代理)
* (1)在此方法中会获取到调用的接口信息
* (2)然后向注册中心(本地文件)拉取服务方的地址信息
* (3)其次向服务方发起调用信息
* (4)最后向服务调用方返回结果
*
* @param interfaceClass 接口类
* @param <T>
* @return
*/
public static <T> T getProxy(final Class interfaceClass) {
Object newProxyInstance = Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
IProtocol protocol = ProtocolFactory.getProtocol();
Invocation invocation = new Invocation(interfaceClass.getName(), method.getName(), method.getParameterTypes(), args);
List<URL> urlList = remoteRegister.getRemote(interfaceClass.getName());
URL url = randomBalance.getInstance(urlList);
String result = protocol.send(url, invocation);
return result;
}
});
return (T) newProxyInstance;
}
}
测试结果
先运行服务提供方,再运行服务调用方。
@RunWith(SpringRunner.class)
@SpringBootTest()
public class ApiTest {
@Resource
private Provider provider;
@Resource
private Consumer consumer;
@Test
public void start() {
provider.toStart();
}
@Test
public void consumer() {
consumer.toUse();
}
}
运行结果:
调用方操作台会打印:
===========>provider say: watermelonhit hello!
最后,来个小小的总结:
(1)服务提供方:
- 将接口-实现类信息保存到本地中
- 向注册中心注册本地地址信息
- 监听端口,执行本地方法,返回结果值
(2)服务调用者:
- 创建代理对象
- 调用接口方法,执行拦截方法,发送调用请求,获取返回值
创建代理对象这一步是否跟dubbo不一样呢?dubbo做了进一步的处理,将代理对象注入到spring容器中,后面直接嗲用代理对象,本质上还是一样的。
最后的最后,希望这篇文章能够对您有所帮助吧🌹
本文到此结束,谢谢阅读。
好久没写博客了,鉴于网上关于手写dubbo版很多但是代码写得都很混乱,逻辑也不是很清晰。故简单的写了这篇文章。