一 Netty简单RPC调用

1.1 RPC基本介绍

  1. RPC(Remote Procedure Call),远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
  2. 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)
    image.png
  3. 常见的 RPC 框架有: 比较知名的如阿里的Dubbo、google的gRPC、Go语言的rpcx、Apache的thrift, Spring 旗下的 Spring Cloud。

RPC调用流程:
image.png

在RPC 中, Client 叫服务消费者,Server 叫服务提供者

PRC调用流程说明

  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)得到结果

image.png
小结:RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。

1.2 代码示例

服务接口:

  1. public interface HelloService {
  2. String hello(String message);
  3. }

服务端服务接口实现类:

  1. public class HelloServiceImpl implements HelloService {
  2. @Override
  3. public String hello(String message) {
  4. System.out.println("收到客户端消息=" + message);
  5. //根据 message 返回不同的结果
  6. if(message != null) {
  7. return "你好客户端,我已经收到你的消息【" + message + "】";
  8. } else {
  9. return "你好客户端,我已经收到你的消息。";
  10. }
  11. }
  12. }

服务端初始化Netty:

  1. public class NettyServer {
  2. public static void startServer(String hostName, int port) {
  3. startServer0(hostName, port);
  4. }
  5. private static void startServer0(String hostname, int port) {
  6. NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
  7. NioEventLoopGroup workerGroup = new NioEventLoopGroup();
  8. try {
  9. ServerBootstrap serverBootstrap = new ServerBootstrap();
  10. serverBootstrap.group(bossGroup, workerGroup)
  11. .channel(NioServerSocketChannel.class)
  12. .childHandler(new ChannelInitializer<SocketChannel>() {
  13. @Override
  14. protected void initChannel(SocketChannel ch) throws Exception {
  15. ChannelPipeline pipeline = ch.pipeline();
  16. pipeline.addLast(new StringDecoder());
  17. pipeline.addLast(new StringEncoder());
  18. pipeline.addLast(new NettyServerHandler()); //业务处理器
  19. }
  20. });
  21. ChannelFuture channelFuture = serverBootstrap.bind(hostname,port).sync();
  22. System.out.println("服务提供方开始运行");
  23. channelFuture.channel().closeFuture().sync();
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. } finally {
  27. bossGroup.shutdownGracefully();
  28. workerGroup.shutdownGracefully();
  29. }
  30. }
  31. }

服务端Handler:

  1. public class NettyServerHandler extends ChannelInboundHandlerAdapter {
  2. @Override
  3. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  4. //获取客户端发送的消息,并调用服务
  5. System.out.println("msg=" + msg);
  6. //客户端在调用服务器的api 时,我们需要定义一个协议
  7. //比如要求,每次发消息时,都必须以某个字符串开头 "HelloService#hello#你好"
  8. if (msg.toString().startsWith("HelloService#hello#")) {
  9. String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
  10. ctx.writeAndFlush(result);
  11. }
  12. }
  13. @Override
  14. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  15. ctx.close();
  16. }
  17. }

Netty客户端

  1. public class NettyClient {
  2. //创建一个线程池
  3. private static ExecutorService executor= Executors.newFixedThreadPool(5);
  4. private static NettyClientHandler client;
  5. //编写方法,使用代理模式,获取一个代理对象
  6. public Object getBean(final Class<?> serviceClass, final String providerName) {
  7. return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{serviceClass},
  8. (proxy, method, args) -> {
  9. System.out.println("代理被调用");
  10. if (client == null)
  11. initClient();
  12. //设置要发给服务器端的信息
  13. client.setPara(providerName + args[0]);
  14. return executor.submit(client).get();
  15. });
  16. }
  17. //初始化客户端
  18. private static void initClient() {
  19. client = new NettyClientHandler();
  20. //创建EventLoopGroup
  21. NioEventLoopGroup group = new NioEventLoopGroup();
  22. try {
  23. Bootstrap bootstrap = new Bootstrap();
  24. bootstrap.group(group)
  25. .channel(NioSocketChannel.class)
  26. .option(ChannelOption.TCP_NODELAY, true)
  27. .handler(new ChannelInitializer<SocketChannel>() {
  28. @Override
  29. protected void initChannel(SocketChannel ch) throws Exception {
  30. ChannelPipeline pipeline = ch.pipeline();
  31. pipeline.addLast(new StringDecoder());
  32. pipeline.addLast(new StringEncoder());
  33. pipeline.addLast(client);
  34. }
  35. });
  36. ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7000).sync();
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }

客户端Handler

  1. public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
  2. private ChannelHandlerContext context; //上下文
  3. private String result; //返回的结果
  4. private String para; //客户端调用方法时,传入的参数
  5. //与服务端创建连接后调用
  6. @Override
  7. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  8. System.out.println("通道连接成功");
  9. context = ctx; //因为我们在其他方法会使用到 ctx
  10. }
  11. @Override
  12. public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  13. result = msg.toString();
  14. notify(); //唤醒等待的线程
  15. }
  16. @Override
  17. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  18. cause.printStackTrace();
  19. ctx.close();
  20. }
  21. //被代理对象的调用,真正发送数据给服务器,发送完后就阻塞,等待被唤醒(channelRead)
  22. @Override
  23. public synchronized Object call() throws Exception {
  24. System.out.println("线程被调用-----");
  25. context.writeAndFlush(para);
  26. //进行wait
  27. wait(); //等待 channelRead 获取到服务器的结果后,进行唤醒。
  28. return result; //服务方返回的结果
  29. }
  30. public void setPara(String para){
  31. this.para = para;
  32. }
  33. }

客户端启动并调用远程方法:

  1. public class ClientBootStrap {
  2. //这里定义协议头
  3. public static final String providerName = "HelloService#hello#";
  4. public static void main(String[] args) throws InterruptedException {
  5. //创建一个消费者
  6. NettyClient customer = new NettyClient();
  7. //创建代理对象
  8. HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
  9. //通过代理对象调用服务提供者的方法
  10. String res = service.hello("你好 Dubbo");
  11. System.out.println("调用的结果,res = " + res);
  12. Thread.sleep(2000);
  13. }
  14. }

二 总结

一个简单的rpc还是很好实现的, 例如A系统搞个HelloService类 提供一个方法,如果B系统想去调用
A系统的接口,肯定需要走网络,因为我们模仿dubbo,像本地一样的调用,那么我们B系统应该这样做。
B系统

  1. 拷贝HelloService一份接口
  2. 动态代理该放类,在方法里加入远程调用A系统的逻辑
  3. 然后返回远程调用的结果

A系统:

  1. 接受到B系统的请求,并且根据请求参数判断是哪个实现类的接口,然后调用该接口的本地方法,获取返回值。
  2. 将返回值返回给B系统