基於Netty的RPC實現

前言

這個demo,主要是展示了RPC與Netty在一起所產生的一種奇妙的化學反應,與傳統的,阻塞的通信不同,基於Netty的PRC框架,可以實現,兩個服務之前,異步的方法調用。
其實這篇文章的關鍵詞已經給出來了,即:兩個服務之前,異步的,方法調用。

一、RPC是什麼

如果不清楚RPC是什麼,推薦閱讀我的博客《PRC原理分析:從一個簡單的DEMO開始》
https://blog.csdn.net/m13797378901/article/details/86538744
當然,我這裏也再一次說明一下RPC是什麼:

RPC,全稱爲Remote Procedure Call,即遠程過程調用,它是一個計算機通信協議。它允許像調用本地服務一樣調用遠程服務。它可以有不同的實現方式。如RMI(遠程方法調用)、Hessian、Http invoker等。另外,RPC是與語言無關的。

以上是來自原網上其他博客的說明,這裏我補充一點:RPC是與語言無關的,他是一種理念,思想。

二、Netty是什麼

Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,作爲一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。作爲當前最流行的NIO框架,Netty在互聯網領域、大數據分佈式計算領域、遊戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。

Netty是一種先進的NIO解決方案,他完善了java中NIO並且對其進行了優化,使之可以更高效的更方便的進行使用。
那麼NIO是什麼呢?
臭不要臉的博主推薦閱讀自己的博客《與NIO的第一次親密接觸》 =_=
https://blog.csdn.net/m13797378901/article/details/88977996

三、源碼分析

廢話不多少,我們直接開幹吧。

1、項目結構

服務端
在這裏插入圖片描述
客戶端
在這裏插入圖片描述
公共契約公共類
在這裏插入圖片描述
上方圖1爲服務端架構,圖2位客戶端架構,圖3爲公共類以及契約接口

(1)服務端包結構說明
  • api:存放契約實現
  • netty:網絡通信,以及核心業務實現
  • utils:工具包,Static的數據集合
(2)客戶端包結構說明
  • interfaces:存放一個回調接口INettyCallBack
  • netty:網絡通信,以及核心業務實現
(3)公共包以及契約
  • api: 客戶端與服務器共同遵守的契約,即客戶端負責調用契約,服務器負責實現契約
  • model:公共實體
  • utils:公共的工具包,如json相關,字符串相關的處理

2、服務端代碼分析

(1)Starter 類

該類是啓動類,負責聲明端口,註冊中心數據注入,以及netty的啓動。

 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-09 21:31
 */
public class Starter {
    private static Logger logger = Logger.getLogger("Starter");

    public static void main(String [] agres) throws Exception {
    	//EchoServerImpl類,實現了IEchoServer 接口
        IEchoServer echoServer=new EchoServerImpl(8088);
        //將契約註冊到註冊中心,契約儲存的Key:契約,Value:契約實現類。
        echoServer.register(IUserService.class, UserServiceImpl.class);
        //啓動
        echoServer.start();
    }
}
(2)IEchoServer 接口
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-09 20:23
 */
public interface IEchoServer {
    //停止服務
    public void stop();

    //開始服務
    public void start() throws Exception;

    //註冊服務,註冊服務,就是講接口,以及對應的實現,放到了一個Map中
    public void register(Class serviceInterface, Class impl);

    //判斷當前服務是否在運行
    public boolean isRunning();

    //獲取使用的端口
    public int getPort();
}
(3)EchoServerImpl 核心實現類

值得注意的是, StaticData.serviceRegistry是邏輯上的註冊中心,但本質上是一個HashMap。
在start方法中,將EchoServerHandler注入到ChannelPipeline中。

在Netty裏,Channel是通訊的載體,而ChannelHandler負責Channel中的邏輯處理。那麼ChannelPipeline是什麼呢?我覺得可以理解爲ChannelHandler的容器:一個Channel包含一個ChannelPipeline,所有ChannelHandler都會註冊到ChannelPipeline中,並按順序組織起來。
在Netty中,ChannelEvent是數據或者狀態的載體,例如傳輸的數據對應MessageEvent,狀態的改變對應ChannelStateEvent。當對Channel進行操作時,會產生一個ChannelEvent,併發送到ChannelPipeline。ChannelPipeline會選擇一個ChannelHandler進行處理。這個ChannelHandler處理之後,可能會產生新的ChannelEvent,並流轉到下一個ChannelHandler。

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-06 14:47
 */
public class EchoServerImpl implements IEchoServer{
    private final Integer port;
    private static Boolean isRunning = false;
    private static Logger logger = Logger.getLogger("EchoServerImpl");

    public EchoServerImpl(int port){
        this.port=port;
    }

    @Override
    public void start() throws Exception {
        logger.info("start");
        NioEventLoopGroup group=new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap=new ServerBootstrap();
            bootstrap.group(group)
                    //指定使用 NIO 的傳輸 Channel
                    .channel(NioServerSocketChannel.class)
                    //.設置 socket 地址使用所選的端口
                    .localAddress(new InetSocketAddress(port))
                    //添加 EchoServerHandler 到 Channel 的 ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            //綁定的服務器;sync 等待服務器關閉
            ChannelFuture future=bootstrap.bind().sync();
            //關閉 channel 和 塊,直到它被關閉
            future.channel().closeFuture().sync();
        }finally {
            //關機的 EventLoopGroup,釋放所有資源。
            group.shutdownGracefully().sync();
        }
    }

    @Override
    public void register(Class serviceInterface, Class impl) {
        StaticData.serviceRegistry.put(serviceInterface.getName(), impl);
    }
}

(4)EchoServerHandler 具體業務實現

channelRead方法:接受到客戶端消息後,會進入此方法。
接受到客戶端消息後,將字節轉爲字符串,並且,對字符串進行json解析,得到實體模型,其中包含了類,方法,參數類型,參數值。
再使用java中的invoke,可以直接調用方法,並且獲取返回值。
獲取返回值後,轉爲ByteBuf類型,再寫入流中,返回到客戶端。

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-06 14:28
 */
//@Sharable 標識這類的實例之間可以在 channel 裏面共享
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    private static Logger logger = Logger.getLogger("EchoServerHandler");

    @Override
    public void channelRead(ChannelHandlerContext context, Object msg){
        logger.info("channelRead");
        ByteBuf in= (ByteBuf) msg;
        //將所接收的數據返回給發送者。注意,這裏還沒有沖刷數據
        /**
         * 解析數據,處理數據,調用invoke
         * 將invoke數據返回
         */
        String resultStr="";
        try {
            String read= in.toString(CharsetUtil.UTF_8);
            RPCNetty entity = (RPCNetty) JsonMapper.fromJsonString(read,RPCNetty.class);
            Class clz= StaticData.serviceRegistry.get(entity.getClzName());
            Method method=clz.getMethod(entity.getMethodName(),entity.getParamTypes());
            
            Object resultObj=method.invoke(clz.newInstance(),entity.getArguments());
            resultStr=JsonMapper.toJsonString(resultObj);
        }catch (Exception e){
            logger.info("Exception>"+e.getMessage());
        }

        ByteBuf resultIn= Unpooled.buffer();
        resultIn.writeBytes(resultStr.getBytes());
        //必須寫入ByteBuf字節流
        context.write(resultIn);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext context) throws Exception{
        //沖刷所有待審消息到遠程節點。關閉通道後,操作完成
        logger.info("channelReadComplete");

        context.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

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

3、客戶端代碼分析

(1)Starter 啓動類

聲明端口,通過代理聲明IUserService ,代理調用接口的方法。
由於是異步通信,這裏採用回調的方式來接收數據。

 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-09 21:31
 */
public class Starter {
    private static java.util.logging.Logger logger = Logger.getLogger("Starter");

    public static void main(String [] agres){
        final String host = "127.0.0.1";
        final int port = 8088;
        InetSocketAddress inetSocketAddress=new InetSocketAddress(host, port);

        IUserService userService=RPCFactory.getRemoteProxyObj(IUserService.class,inetSocketAddress,
                new INettyCallBack() {
                    @Override
                    public Object calllBack(Object obj) {
                        logger.info("obj>"+ JsonMapper.toJsonString(obj));
                        return null;
                    }
                });
        userService.getUser("1");
    }
}
(2)IUserService 契約
//契約接口
public interface IUserService {
    String getUser(String var1);
}

//契約實現
@Service
public class UserServiceImpl implements IUserService {
    @Override
    public String getUser(String id) {
        return "I am Service, Hi Client "+id +" , nice to meet you :)";
    }
}
(3)RPCFactory 代理實現類

傳遞網絡連接需要的host以及port。
將相關的對象,來封裝實體對象傳遞到Netty處理類中,進行進一步的處理

    private static java.util.logging.Logger logger = Logger.getLogger("EchoClientHandler");

    public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr, final INettyCallBack nettyCallBack) {
        // 1.將本地的接口調用轉換成JDK的動態代理,在動態代理中實現接口的遠程調用
        return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                new Class<?>[]{serviceInterface},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        logger.info("invoke");
                        EchoClient echoClient=new EchoClient(addr);
                        RPCNetty rpcNetty=new RPCNetty();
                        rpcNetty.setClzName(serviceInterface.getName());
                        rpcNetty.setMethodName(method.getName());
                        rpcNetty.setParamTypes(method.getParameterTypes());
                        rpcNetty.setArguments(args);
                        echoClient.setRpcNetty(rpcNetty);
                        echoClient.setNettyCallBack(nettyCallBack);
                        echoClient.start();
                        return null;
                    }
                });
    }
}
(4)EchoClient 中

與服務端中的start方法類似,
都是綁定對應的Channel,聲明遠程端口,在回調方法 中,聲明Handler對象,將相關對象,設置到此對象中,最後,將Handler綁定到Channel的管道上。

 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-06 17:52
 */
public class EchoClient {
    private  String host;
    private  int port;
    private InetSocketAddress inetSocketAddress;
    private INettyCallBack nettyCallBack;
    private RPCNetty rpcNetty;
    private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger("EchoClient");

    public void start() throws Exception{
        EventLoopGroup group=new NioEventLoopGroup();
        try {
            Bootstrap b=new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
                    .remoteAddress(inetSocketAddress)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            EchoClientHandler echoClientHandler=new EchoClientHandler();
                            echoClientHandler.setRpcNetty(rpcNetty);
                            echoClientHandler.setNettyCallBack(nettyCallBack);;

                            socketChannel.pipeline().addLast(echoClientHandler);
                        }
                    });

            ChannelFuture future=b.connect().sync();
            future.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully().sync();
        }
    }
    ......
}
(5)EchoClientHandler 具體業務處理方法

當Handler被處理,激活以後,會觸發channelActive方法,在此方法中,可以將由代理方法中傳輸的對象,傳遞到服務器中,具體傳輸方式,同於服務端接受數據,即,將實體模型轉爲Json字符串,再將字符串轉爲字節流寫入到服務器中。
服務器如果接受到數據,會對其中的參數,即,接口類,方法,方法參數,方法參數類型,進行處理,即對其進行代理,找到服務器中具體的實現類,實現此方法。
當服務器處理完畢,得到了返回值,再將值轉爲json字符串字節,傳遞到客戶端。
channelRead0方法,就是客戶端接受數據之後的回調。
由於是異步進行處理,所以我們再其中同樣也進行溢出接口調用,在其他位置,只要實現此接口,就可以獲取到服務器傳輸的數據。

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-03-06 17:40
 */
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
    private static java.util.logging.Logger logger = Logger.getLogger("EchoClientHandler");

    private RPCNetty rpcNetty;
    private INettyCallBack nettyCallBack;
	......
    /**
     * 請求數據
     */
    @Override
    public void channelActive(ChannelHandlerContext context){
        logger.info("client channelActive >>"+ JsonMapper.toJsonString(rpcNetty));
        context.writeAndFlush(Unpooled.copiedBuffer(JsonMapper.toJsonString(rpcNetty),
                CharsetUtil.UTF_8));
    }

    /**
     * 服務器返回數據
     */
    @Override
    protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {
        logger.info("client channelRead0 >>"+byteBuf.toString(CharsetUtil.UTF_8));
        nettyCallBack.calllBack(byteBuf.toString(CharsetUtil.UTF_8));
    }


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

3、公共服務

(1)RPCNetty 通信實體
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-04-08 18:57
 */
public class RPCNetty implements Serializable {
    private String clzName;
    private Class clz;
    private String methodName;
    private Class<?>[] paramTypes;
    private Object[] arguments;
    }

4、運行結果

(1)啓動服務端

四月 10, 2019 5:57:54 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerImpl start
信息: start

(2)啓動客戶端

四月 10, 2019 6:24:15 下午 com.xyp.iodemo.nettyrpcclient.netty.RPCFactory$1 invoke
信息: invoke
四月 10, 2019 6:24:15 下午 com.xyp.iodemo.nettyrpcclient.netty.EchoClientHandler channelActive
信息: client channelActive >>{“clzName”:“com.xyp.iodemo.nettyrpccommon.api.IUserService”,“methodName”:“getUser”,“paramTypes”:[“java.lang.String”],“arguments”:[“1”]}
四月 10, 2019 6:24:16 下午 com.xyp.iodemo.nettyrpcclient.netty.EchoClientHandler channelRead0
信息: client channelRead0 >>“I am Service, Hi Client 1 , nice to meet you 😃”
四月 10, 2019 6:24:16 下午 com.xyp.iodemo.nettyrpcclient.Starter$1 calllBack
信息: obj>"“I am Service, Hi Client 1 , nice to meet you 😃”"

(3)服務器響應

四月 10, 2019 5:57:54 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerImpl start
信息: start
四月 10, 2019 6:24:15 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerHandler channelRead
信息: channelRead
四月 10, 2019 6:24:15 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerHandler channelRead
信息: read>>>>{“clzName”:“com.xyp.iodemo.nettyrpccommon.api.IUserService”,“methodName”:“getUser”,“paramTypes”:[“java.lang.String”],“arguments”:[“1”]}
四月 10, 2019 6:24:16 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerHandler channelRead
信息: resultStr>>>>“I am Service, Hi Client 1 , nice to meet you 😃”
四月 10, 2019 6:24:16 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerHandler channelRead
信息: context.write>>>>“I am Service, Hi Client 1 , nice to meet you 😃”
四月 10, 2019 6:24:16 下午 com.xyp.iodemo.nettyrpcserver.netty.EchoServerHandler channelReadComplete
信息: channelReadComplete

四、總結

Netty穩定且高效,易用,是進行NIO通信的不錯之選。
我們採用了Netty的通信方式,來處理RPC的底層的數據通信,這樣穩定且高效。

參考:
《Netty權威指南》書籍
《與NIO的第一次親密接觸》 https://blog.csdn.net/m13797378901/article/details/86538744

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章