使用Netty和動態代理實現一個簡單的RPC

划水的魚dm發表於2021-11-08

RPC(remote procedure call)遠端過程呼叫

RPC是為了在分散式應用中,兩臺主機的Java程式進行通訊,當A主機呼叫B主機的方法時,過程簡潔,就像是呼叫自己程式裡的方法一樣。
RPC框架的職責就是,封裝好底層呼叫的細節,客戶端只要呼叫方法,就能夠獲取服務提供者的響應,方便開發者編寫程式碼。
RPC底層使用的是TCP協議,服務端和客戶端和點對點通訊。

作用

在RPC的應用場景中,客戶端呼叫服務端的程式碼

客戶端需要有相應的api介面,將方法名、方法引數型別、具體引數等等都傳送給服務端

服務端需要有方法的具體實現,在接收到客戶端的請求後,根據資訊呼叫對應的方法,並返回響應給客戶端

 

 

流程圖演示

 

 

 

程式碼實現

首先客戶端要知道服務端的介面,然後封裝一個請求物件,傳送給服務端

要呼叫一個方法需要有:方法名、方法引數型別、具體引數、執行方法的類名

使用Netty和動態代理實現一個簡單的RPC
@Data
public class RpcRequest {

    private String  methodName;

    private String className;

    private Class[] paramType;

    private Object[] args;
}
View Code

由服務端返回給客戶端的響應(方法呼叫結果)也使用一個物件進行封裝

使用Netty和動態代理實現一個簡單的RPC
@Data
public class RpcResponse {

    private int code;

    private Object result;
}
View Code
  • 如果是在多執行緒呼叫中,需要具體把每個響應返回給對應的請求,可以加一個ID進行標識

將物件通過網路傳輸,需要先進行序列化操作,這裡使用的是jackson工具

<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.4</version>
</dependency>
使用Netty和動態代理實現一個簡單的RPC
public class JsonSerialization {

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    public static byte[] serialize(Object output) throws JsonProcessingException {
        byte[] bytes = objectMapper.writeValueAsBytes(output);
        return bytes;
    }

    public static Object deserialize(byte[] input,Class clazz) throws IOException {
        Object parse = objectMapper.readValue(input,clazz);
        return parse;
    }
}
View Code
  • 在反序列化過程中,需要指定要轉化的型別,而服務端接收request,客戶端接收response,二者型別是不一樣的,所以在後續傳輸時指定型別

有了需要傳輸的資料後,使用Netty開啟網路服務進行傳輸

服務端

繫結埠號,開啟連線

public class ServerNetty {

    public static void connect(int port) throws InterruptedException {

        EventLoopGroup workGroup = new NioEventLoopGroup();
        EventLoopGroup bossGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.channel(NioServerSocketChannel.class)
                .group(bossGroup,workGroup)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        /**
                         * 加入自定義協議的資料處理器,指定接收到的資料型別
                         * 加入服務端處理器
                         */
                        ch.pipeline().addLast(new NettyProtocolHandler(RpcRequest.class));

                        ch.pipeline().addLast(new ServerHandler());
                    }
                });

        bootstrap.bind(port).sync();
    }
}

Netty中繫結了兩個資料處理器

一個是資料處理器,服務端接收到請求->呼叫方法->返回響應,這些過程都在資料處理器中執行

public class ServerHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        RpcRequest rpcRequest = (RpcRequest)msg;

        // 獲取使用反射需要的各個引數
        String methodName = rpcRequest.getMethodName();
        Class[] paramTypes = rpcRequest.getParamType();
        Object[] args = rpcRequest.getArgs();
        String className = rpcRequest.getClassName();

        //從註冊中心容器中獲取物件
        Object object = Server.hashMap.get(className);

        Method method = object.getClass().getMethod(methodName,paramTypes);
        //反射呼叫方法
       String result = (String) method.invoke(object,args);


        // 將響應結果封裝好後傳送回去
        RpcResponse rpcResponse = new RpcResponse();
        rpcResponse.setCode(200);
        rpcResponse.setResult(result);

        ctx.writeAndFlush(rpcResponse);
    }
}
  • 這裡從hash表中獲取物件,有一個預先進行的操作:將有可能被遠端呼叫的物件放入容器中,等待使用

一個是自定義的TCP協議處理器,為了解決TCP的常見問題:因為客戶端傳送的資料包和服務端接收資料緩衝區之間,大小不匹配導致的粘包、拆包問題。

/**
 * 網路傳輸的自定義TCP協議
 * 傳送時:為傳輸的位元組流新增兩個魔數作為頭部,再計算資料的長度,將資料長度也新增到頭部,最後才是資料
 * 接收時:識別出兩個魔數後,下一個就是首部,最後使用長度對應的位元組陣列接收資料
 */
public class NettyProtocolHandler extends ChannelDuplexHandler {

    private static final byte[] MAGIC = new byte[]{0x15,0x66};

    private Class decodeType;

    public NettyProtocolHandler() {
    }

    public NettyProtocolHandler(Class decodeType){
        this.decodeType = decodeType;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf in = (ByteBuf) msg;
        //接收響應物件
        Object dstObject;

        byte[] header = new byte[2];
        in.readBytes(header);

        byte[] lenByte = new byte[4];
        in.readBytes(lenByte);

        int len = ByteUtils.Bytes2Int_BE(lenByte);

        byte[] object = new byte[len];
        in.readBytes(object);

        dstObject = JsonSerialization.deserialize(object, decodeType);
        //交給下一個資料處理器
        ctx.fireChannelRead(dstObject);

    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {

        ByteBuf byteBuf = Unpooled.buffer();

        //寫入魔數
        byteBuf.writeBytes(MAGIC);

        byte[] object = JsonSerialization.serialize(msg);

        //資料長度轉化為位元組陣列並寫入
        int len = object.length;

        byte[] bodyLen = ByteUtils.int2bytes(len);

        byteBuf.writeBytes(bodyLen);

        //寫入物件
        byteBuf.writeBytes(object);

        ctx.writeAndFlush(byteBuf);
    }
}
  • 這個資料處理器是服務端和客戶端都要使用的,就相當於是一個雙方定好傳輸資料要遵守的協議
  • 在這裡進行了物件的序列化和反序列化,所以反序列化型別在這個處理器中指定
  • 這裡面要將資料的長度傳送,需一個將整數型別轉化為位元組型別的工具

 轉化資料工具類

使用Netty和動態代理實現一個簡單的RPC
public class ByteUtils {

    /** short2\u5B57\u8282\u6570\u7EC4 */
    public static byte[] short2bytes(short v) {
        byte[] b = new byte[4];
        b[1] = (byte) v;
        b[0] = (byte) (v >>> 8);
        return b;
    }

    /** int4\u5B57\u8282\u6570\u7EC4 */
    public static byte[] int2bytes(int v) {
        byte[] b = new byte[4];
        b[3] = (byte) v;
        b[2] = (byte) (v >>> 8);
        b[1] = (byte) (v >>> 16);
        b[0] = (byte) (v >>> 24);
        return b;
    }

    /** long8\u5B57\u8282\u6570\u7EC4 */
    public static byte[] long2bytes(long v) {
        byte[] b = new byte[8];
        b[7] = (byte) v;
        b[6] = (byte) (v >>> 8);
        b[5] = (byte) (v >>> 16);
        b[4] = (byte) (v >>> 24);
        b[3] = (byte) (v >>> 32);
        b[2] = (byte) (v >>> 40);
        b[1] = (byte) (v >>> 48);
        b[0] = (byte) (v >>> 56);
        return b;
    }

    /** \u5B57\u8282\u6570\u7EC4\u8F6C\u5B57\u7B26\u4E32 */
    public static String bytesToHexString(byte[] bs) {
        if (bs == null || bs.length == 0) {
            return null;
        }

        StringBuffer sb = new StringBuffer();
        String tmp = null;
        for (byte b : bs) {
            tmp = Integer.toHexString(Byte.toUnsignedInt(b));
            if (tmp.length() < 2) {
                sb.append(0);
            }
            sb.append(tmp);
        }
        return sb.toString();
    }

    /**
     * @return
     */
    public static int Bytes2Int_BE(byte[] bytes) {
        if(bytes.length < 4){
            return -1;
        }
        int iRst = (bytes[0] << 24) & 0xFF;
        iRst |= (bytes[1] << 16) & 0xFF;
        iRst |= (bytes[2] << 8) & 0xFF;
        iRst |= bytes[3] & 0xFF;
        return iRst;
    }

    /**
     * long\u8F6C8\u5B57\u8282\u6570\u7EC4
     */
    public static long bytes2long(byte[] b) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.put(b, 0, b.length);
        buffer.flip();// need flip
        return buffer.getLong();
    }
}
View Code

 

客戶端

將Netty的操作封裝了起來,最後返回一個Channle型別,由它進行傳送資料的操作

public class ClientNetty {

    public static Channel connect(String host,int port) throws InterruptedException {

        InetSocketAddress address = new InetSocketAddress(host,port);

        EventLoopGroup workGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class)
                    .group(workGroup)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            //自定義協議handler(客戶端接收的是response)
                            ch.pipeline().addLast(new NettyProtocolHandler(RpcResponse.class));
                            //處理資料handler
                            ch.pipeline().addLast(new ClientHandler());
                        }
                    });

            Channel channel = bootstrap.connect(address).sync().channel();

            return channel;
    }
}

資料處理器負責接收response,並將響應結果放入在future中,future的使用在後續的動態代理中

public class ClientHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        RpcResponse rpcResponse = (RpcResponse) msg;

        //服務端正常情況返回碼為200
        if(rpcResponse.getCode() != 200){
            throw new Exception();
        }

        //將結果放到future裡
        RPCInvocationHandler.future.complete(rpcResponse.getResult());
    }

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

 

要讓客戶端在呼叫遠端方法時像呼叫本地方法一樣,就需要一個代理物件,供客戶端呼叫,讓代理物件去呼叫服務端的實現。

代理物件構造

public class ProxyFactory {

    public static Object getProxy(Class<?>[] interfaces){

        return Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(),
                interfaces,
                new RPCInvocationHandler());
    }
}

客戶端代理物件的方法執行

將request傳送給服務端後,一直阻塞,等到future裡面有了結果為止。

public class RPCInvocationHandler implements InvocationHandler {


    static public CompletableFuture future;
    static Channel channel;

    static {
        future = new CompletableFuture();
        //開啟netty網路服務
        try {
            channel = ClientNetty.connect("127.0.0.1",8989);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        RpcRequest rpcRequest = new RpcRequest();

        rpcRequest.setArgs(args);
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParamType(method.getParameterTypes());
        rpcRequest.setClassName(method.getDeclaringClass().getSimpleName());

       channel.writeAndFlush(rpcRequest);
        //一個阻塞操作,等待網路傳輸的結果
       String result = (String) future.get();

        return result;
    }
}
  • 這裡用static修飾future和channle,沒有考慮到客戶端去連線多個服務端和多次遠端呼叫
  • 可以使用一個hash表,儲存與不同服務端對應的channle,每次呼叫時從hash表中獲取即可
  • 用hash表儲存與不同request對應的future,每個響應的結果與之對應

客戶端

要進行遠端呼叫需要擁有的介面

public interface OrderService {

    public String buy();
}

預先的操作和測試程式碼

public class Client {

    static OrderService orderService;

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

        //建立一個代理物件給進行遠端呼叫的類
        orderService = (OrderService) ProxyFactory.getProxy(new Class[]{OrderService.class});

        String result = orderService.buy();

        System.out.println(result);
    }
}

 

服務端

要接受遠端呼叫需要擁有的具體實現類

public class OrderImpl implements OrderService {

    public OrderImpl() {
    }

    @Override
    public String buy() {
        System.out.println("呼叫buy方法");
        return "呼叫buy方法成功";
    }
}

預先操作和測試程式碼

public class Server {

   public static HashMap<String ,Object> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        //開啟netty網路服務
        ServerNetty.connect(8989);

        //提前將需要開放的服務註冊到hash表中
        hashMap.put("OrderService",new OrderImpl());

    }
}

 

執行結果

 

 

 

相關文章