第 11 章用 Netty 實現 Dubbo RPC

11.1 RPC 基本介紹

  1. RPC(Remote Procedure Call)— 遠程過程調用,是一個計算機通信協議。該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外地爲這個交互作用編程

  2. 兩個或多個應用程序都分佈在不同的服務器上,它們之間的調用都像是本地方法調用一樣
    在這裏插入圖片描述
    3) 常見的 RPC 框架有: 比較知名的如阿里的Dubbo、google的gRPC、Go語言的rpcx、Apache的thrift,Spring
    旗下的 Spring Cloud。
    在這裏插入圖片描述 在這裏插入圖片描述

11.2 RPC 調用流程圖

在這裏插入圖片描述

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

小結:RPC 的目標就是將 2-8 這些步驟都封裝起來,用戶無需關心這些細節,可以像調用本地方法一樣即可完成遠程服務調用

11.4 自己實現 dubbo RPC(基於 Netty)

11.4.1 需求說明

  1. dubbo 底層使用了 Netty 作爲網絡通訊框架,要求用 Netty 實現一個簡單的 RPC 框架
  2. 模仿 dubbo,消費者和提供者約定接口和協議,消費者遠程調用提供者的服務,提供者返回一個字符串,消費者打印提供者返回的數據。底層網絡通信使用 Netty 4.1.20

11.4.2 設計說明

  1. 創建一個接口,定義抽象方法。用於消費者和提供者之間的約定。
  2. 創建一個提供者,該類需要監聽消費者的請求,並按照約定返回數據。
  3. 創建一個消費者,該類需要透明的調用自己不存在的方法,內部需要使用 Netty 請求提供者返回數據
  4. 開發的分析圖

在這裏插入圖片描述

11.4.3 代碼實現

//這個是接口,是服務提供方和 服務消費方都需要
public interface HelloService {
    String hello(String mes);
}

public class HelloServiceImpl implements HelloService{

    private static int count = 0;
    //當有消費方調用該方法時, 就返回一個結果
    @Override
    public String hello(String mes) {
        System.out.println("收到客戶端消息=" + mes);
        //根據mes 返回不同的結果
        if(mes != null) {
            return "你好客戶端, 我已經收到你的消息 [" + mes + "] 第" + (++count) + " 次";
        } else {
            return "你好客戶端, 我已經收到你的消息 ";
        }
    }
}
Server
//ServerBootstrap 會啓動一個服務提供者,就是 NettyServer
public class ServerBootstrap {
    public static void main(String[] args) {
        NettyServer.startServer("127.0.0.1", 7000);
    }
}

public class NettyServer {
    public static void startServer(String hostName, int port) {
        //不同的啓動方式
        startServer0(hostName,port);
    }

    //編寫一個方法,完成對NettyServer的初始化和啓動

    private static void startServer0(String hostname, int port) {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                                      @Override
                                      protected void initChannel(SocketChannel ch) throws Exception {
                                          ChannelPipeline pipeline = ch.pipeline();
                                          pipeline.addLast(new StringDecoder());//編碼
                                          pipeline.addLast(new StringEncoder());//解碼
                                          pipeline.addLast(new NettyServerHandler()); //業務處理器

                                      }
                                  }

                    );

            ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
            System.out.println("服務提供方開始提供服務~~");
            channelFuture.channel().closeFuture().sync();

        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

//服務器這邊handler比較簡單
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //獲取客戶端發送的消息,並調用服務
        System.out.println("msg=" + msg);
        //客戶端在調用服務器的api 時,我們需要定義一個協議
        //比如我們要求 每次發消息是都必須以某個字符串開頭 "HelloService#hello#你好"
        if(msg.toString().startsWith(ClientBootstrap.providerName)) {

            String result = new HelloServiceImpl().hello(msg.toString()
            .substring(msg.toString().lastIndexOf("#") + 1));
            ctx.writeAndFlush(result);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

Client
public class ClientBootstrap {
    //這裏自定義協議頭
    public static final String providerName = "HelloService#hello#";

    public static void main(String[] args) throws  Exception{

        //創建一個消費者
        NettyClient customer = new NettyClient();

        //創建代理對象
        HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);

        for (;; ) {
            Thread.sleep(2 * 1000);
            //通過代理對象調用服務提供者的方法(服務)
            String res = service.hello("你好 dubbo~");
            System.out.println("調用的結果 res= " + res);
        }
    }
}

public class NettyClient {

    //創建線程池
    private static ExecutorService executor = Executors.
    newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static NettyClientHandler client;
    private int count = 0;

    //編寫方法使用代理模式,獲取一個代理對象 與服務器交互的自定義的協議信息
    public Object getBean(final Class<?> serivceClass, final String providerName) {

        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[]{serivceClass}, (proxy, method, args) -> {

                    System.out.println("(proxy, method, args) 進入...." + (++count) + " 次");
                    //{}  部分的代碼,客戶端每調用一次 hello, 就會進入到該代碼
                    if (client == null) {
                        initClient();
                    }

                    //設置要發給服務器端的信息
                    //providerName 協議頭 args[0] 就是客戶端調用api hello(???), 參數
                    client.setPara(providerName + args[0]);

                    return executor.submit(client).get();
                });
    }

    //初始化客戶端
    private static void initClient() {
        client = new NettyClientHandler();
        //創建EventLoopGroup
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(
                        new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast(new StringDecoder());
                                pipeline.addLast(new StringEncoder());
                                pipeline.addLast(client);
                            }
                        }
                );

        try {
            bootstrap.connect("127.0.0.1", 7000).sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {

    private ChannelHandlerContext context;//上下文
    private String result; //返回的結果
    private String para; //客戶端調用方法時,傳入的參數


    //與服務器的連接創建後,就會被調用, 這個方法是第一個被調用(1)
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println(" channelActive 被調用  ");
        context = ctx; //因爲我們在其它方法會使用到 ctx,所以賦值傳給了context
    }

    //收到服務器的數據後,調用方法 (4)
    @Override
    public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(" channelRead 被調用  ");
        result = msg.toString();
        notify(); //喚醒等待的線程
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    //被代理對象調用, 發送數據給服務器,-> wait -> 等待被喚醒(channelRead) -> 返回結果 (3)-》5
    @Override
    public synchronized Object call() throws Exception {
        System.out.println(" call1 被調用  ");
        context.writeAndFlush(para);
        //進行wait
        wait(); //等待channelRead 方法獲取到服務器的結果後,喚醒
        System.out.println(" call2 被調用  ");
        return  result; //服務方返回的結果

    }
    //(2)
    void setPara(String para) {
        System.out.println(" setPara  ");
        this.para = para;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章