使用Netty三分鐘手寫一個RPC

zanwensicheng發表於2019-03-30

個人技術部落格:www.zhenganwen.top

流程概覽

image

專案結構

依賴

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>5.0.0.Alpha1</version>
</dependency>

<!-- 服務提供方根據呼叫資訊反射獲取實現類時需要 -->
<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.9.10</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.18</version>
    <scope>provided</scope>
</dependency>
複製程式碼

通用模組

image

ClassInfo

實體類,封裝了服務呼叫資訊:

package top.zhenganwen.rpc.common;

import lombok.Data;

import java.io.Serializable;

/**
 * ClassInfo class
 *      使用JDK的序列化技術必須實現介面Serializable
 *
 * @author : zaw
 * @date : 2019/3/30
 */
@Data
public class ClassInfo implements Serializable {

    /**
     * 呼叫服務的介面名
     */
    private String className;
    /**
     * 呼叫服務的方法名
     */
    private String methodName;
    /**
     * 呼叫方法的引數列表型別
     */
    private Class[] paramTypes;
    /**
     * 呼叫服務傳參
     */
    private Object[] params;
}
複製程式碼

需要注意的是客戶端在傳送呼叫資訊時會將該類物件序列化併傳送給服務端,而服務的則需要反序列化回來,如果使用的是JDK的序列化技術則需要將此類實現Serializable介面

服務介面

為了便於維護,服務介面通常會被獨立出來到通用模組中,以jar包的形式被服務呼叫方和服務提供方依賴。這裡簡單的寫了兩個介面,一個包含無參服務,一個包含有參服務。

public interface HasArgsHelloService {
    String hello(String msg);
}

public interface NoArgsHelloService {
    String hello();
}
複製程式碼

服務呼叫方

image

client

這個包中是依賴Service介面的一些類,RPC服務的呼叫對於他們來說是透明的,他們僅通過client_stub中的ServiceProxy來獲取服務實現類並呼叫服務。

public class RPCClient {

    public static void main(String[] args){
        NoArgsHelloService noArgsHelloService = (NoArgsHelloService) ServiceProxy.create(NoArgsHelloService.class);
        System.out.println(noArgsHelloService.hello());

        HasArgsHelloService hasArgsHelloService = (HasArgsHelloService) ServiceProxy.create(HasArgsHelloService.class);
        System.out.println(hasArgsHelloService.hello("hello netty rpc"));
    }

}
複製程式碼

client_stub

真正處理RPC呼叫邏輯的包,ServiceProxy通過JDK代理Proxy.newProxyInstance來代理所有的服務,所有client中呼叫服務的動作都將被該代理邏輯中設定的InvocationHandler攔截,攔截後獲取呼叫資訊(介面名、方法名、方法參列型別、實參列表)並通過Netty與服務端建立連線傳送呼叫資訊,然後阻塞等待連線關閉事件(RPCClientHandler在收到服務端返回的呼叫結果時會儲存該結果並關閉連線),若此事件被觸發說明RPCClientHandler已拿到呼叫結果,於是此次InvocationHandler的攔截可以返回了。

  • ServiceProxy
package top.zhenganwen.rpc.client_stub;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import top.zhenganwen.rpc.common.ClassInfo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * ServiceProxy class
 *
 * @author : zaw
 * @date : 2019/3/30
 */
public class ServiceProxy {

    public static Object create(Class clazz) {
        return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //構造呼叫資訊
                ClassInfo classInfo = new ClassInfo();
                classInfo.setClassName(clazz.getName());
                classInfo.setMethodName(method.getName());
                classInfo.setParamTypes(method.getParameterTypes());
                classInfo.setParams(args);

                //使用netty傳送呼叫資訊給服務提供方
                NioEventLoopGroup group = new NioEventLoopGroup();
                Bootstrap bootstrap = new Bootstrap();
                RPCClientHandler rpcClientHandler = new RPCClientHandler();
                try {
                    bootstrap.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.SO_KEEPALIVE, true)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new ObjectEncoder());
                                //反序列化物件時指定類解析器,null表示使用預設的類載入器
                                ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
                                ch.pipeline().addLast(rpcClientHandler);

                            }
                        });
                    //connect是非同步的,但呼叫其future的sync則是同步等待連線成功
                    ChannelFuture future = bootstrap.connect("127.0.0.1", 80).sync();
                    //同步等待呼叫資訊傳送成功
                    future.channel().writeAndFlush(classInfo).sync();
                    //同步等待RPCClientHandler的channelRead被觸發後(意味著收到了呼叫結果)
                    future.channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    group.shutdownGracefully();
                }

                //返回撥用結果
                return rpcClientHandler.getRpcResult();
            }
        });
    }

}
複製程式碼
  • PRCClientHandler
package top.zhenganwen.rpc.client_stub;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

/**
 * RPCClientHandler class
 *
 * @author : zaw
 * @date : 2019/3/30
 */
public class RPCClientHandler extends ChannelHandlerAdapter {

    /**
     * RPC呼叫返回的結果
     */
    private Object rpcResult;

    public Object getRpcResult() {
        return rpcResult;
    }

    public void setRpcResult(Object rpcResult) {
        this.rpcResult = rpcResult;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        setRpcResult(msg);
        ctx.close();
    }
}
複製程式碼

服務提供方

image

server

首先服務提供方有具體的服務實現類,然後它通過RPCServer建立Netty服務端24小時監聽客戶端的服務呼叫請求。請求將被RPCServerHandler處理,它根據請求中的呼叫資訊通過反射找到實現類和服務方法並反射呼叫獲取結果,並立即將結果傳送給客戶端。

  • 服務實現類
public class NoArgsHelloServiceImpl implements NoArgsHelloService {

    @Override
    public String hello() {
        return "hello";
    }
}

public class HasArgsHelloServiceImpl implements HasArgsHelloService {

    @Override
    public String hello(String msg) {
        return msg;
    }
}
複製程式碼
  • PRCServer
package top.zhenganwen.rpc.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import top.zhenganwen.rpc.server_stub.RPCServerHandler;

/**
 * RPCServer class
 *
 * @author : zaw
 * @date : 2019/3/30
 */
public class RPCServer {

    public static void main(String[] args){
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            bootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new ObjectEncoder());
                        ch.pipeline().addLast(new ObjectDecoder(1024 * 64, ClassResolvers.cacheDisabled(null)));
                        ch.pipeline().addLast(new RPCServerHandler());
                    }
                });
            //bind初始化埠是非同步的,但呼叫sync則會同步阻塞等待埠繫結成功
            ChannelFuture future = bootstrap.bind(80).sync();
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

}
複製程式碼

server_stub

真正根據呼叫請求反射呼叫的業務處理類

  • RPCServerHandler
package top.zhenganwen.rpc.server_stub;

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import org.reflections.Reflections;
import top.zhenganwen.rpc.common.ClassInfo;

import java.lang.reflect.Method;
import java.util.Set;

/**
 * RPCServerHandler class
 *
 * @author : zaw
 * @date : 2019/3/30
 */
public class RPCServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //獲取呼叫資訊,尋找服務實現類
        ClassInfo classInfo = (ClassInfo) msg;
        String implName = getImplClassName(classInfo.getClassName());
        Class<?> clazz = Class.forName(implName);
        Method method = clazz.getMethod(classInfo.getMethodName(), classInfo.getParamTypes());
        Object result = method.invoke(clazz.newInstance(), classInfo.getParams());
        ctx.writeAndFlush(result);
    }

    private String getImplClassName(String interfaceName) throws ClassNotFoundException {
        Class interClass = Class.forName(interfaceName);
        String servicePath = "top.zhenganwen.rpc.server";
        Reflections reflections = new Reflections(servicePath);
        Set<Class> implClasses = reflections.getSubTypesOf(interClass);
        if (implClasses.isEmpty()) {
            System.err.println("impl class is not found!");
        } else if (implClasses.size() > 1) {
            System.err.println("there are many impl classes, not sure invoke which");
        } else {
            Class[] classes = implClasses.toArray(new Class[1]);
            return classes[0].getName();
        }
        return null;
    }
}
複製程式碼

相關文章