Netty基础—6.Netty实现RPC服务

大纲

1.RPC的相关概念

2.RPC服务调用端动态代理实现

3.Netty客户端之RPC远程调用过程分析

4.RPC网络通信中的编码解码器

5.Netty服务端之RPC服务提供端的处理

6.RPC服务调用端实现超时功能

1.RPC的相关概念

(1)什么是RPC

(2)什么是静态代理

(3)什么是动态代理

(4)动态代理总结

(1)什么是RPC

本地只有一个方法的接口,需要在本地对这个方法进行远程调用,而对这个方法进行调用其实就是对该接口的动态代理进行调用。

动态代理的底层会把对这个方法的调用封装成一个请求,然后把这个请求序列化成一个二进制数据请求,之后再通过Netty网络通信将二进制数据请求发送给远程机器。

远程机器会启动一个Netty服务端来监听连接和请求,然后把二进制数据反序列化成请求对象,接着再根据这个请求对象在本地找到要调用的那个方法,最后通过反射去调用这个方法并获取结果进行返回。

(2)代理模式

代理模式一般分为静态代理和动态代理两种。

静态代理:就是提前创建好代理类,并且在程序运行前代理类已经被编译成字节码了。

动态代理:就是运行时才生成代理类,即代理类的字节码会在运行时动态生成并载入到ClassLoader中。

(3)静态代理

如果要对一个实现某接口的类的一个方法进行增强,在不影响原接口的前提下只能重新实现该接口。如果要增强的类有很多,那么每一个类都需要重新实现一遍,比较麻烦。比如在下面的例子中,如果还要代理IReceiver接口的实现类,那么还需要定义一个ProxyReceiver代理类去实现IReceiver接口。因为具体的代理类是需要实现被代理类的接口的。

java 复制代码
//第一步:定义接口
public interface ISender {
    public boolean send();
}

//第二步:定义真实的实现类,被代理类
public class SmsSender implements ISender {
    public boolean send() {
        System.out.println("Sending msg");
        return true;
    }
}

//第三步:定义代理类,封装实现类
//代理类在不影响真实类的情况下,实现功能的扩展
//如果还要代理IReceiver接口的实现类,那么还需要定义一个ProxyReceiver实现IReceiver接口
public class ProxySender implements ISender {
    private ISender sender;
    
    public ProxySender(ISender sender) {
        this.sender = sender;
    }
    
    public boolean send() {
        System.out.println("处理前");
        boolean result = sender.send();
        System.out.println("处理后");
        return result;
    }
}

//第四步:客户端调用
@Test
public void testStaticProxy() {
    ISender sender = new ProxySender(new SmsSender());
    boolean result = sender.send();
    System.out.println("输出结果:" + result);
}

(4)动态代理

与静态代理相比,动态代理有更多优势。动态代理不仅不需要定义代理类,甚至可以在运行时指定代理类的执行逻辑,从而大大提升系统的灵活性。比如下面的JDK动态代理例子中,只需要定义一个实现InvocationHandler接口的JdkProxyHandler类,就能同时代理ISender和IReceiver接口的实现类了。因为JdkProxyHandler类是不依赖具体的被代理类的接口的。

目前动态代理类的生成方法有如下几种:

JDK动态代理:内置在JDK中,不需要引入第三方jar,使用简单,但功能比较弱。

CGLib和Javassist:这两个都是高级字节码生成库,总体性能比JDK动态代理好,且功能强大。

ASM:低级字节码生成工具,近乎使用字节码编码,对开发人员要求最高,当然性能也是最好。

一.JDK动态代理

首先需要定义一个InvocationHandler类,该类要实现InvocationHandler接口的invoke()方法来拦截对被代理类接口方法的调用。

然后当客户端调用Proxy.newProxyInstance()方法,并传入被代理类的接口和一个封装了被代理对象的InvocationHandler对象时,便会动态生成一个代理类并返回一个实现了被代理类接口的代理对象。

后续就可以向代理对象调用被代理类的接口方法,对应的方法调用就会转发给InvocationHandler对象的invoke()方法,从而实现对被代理对象的方法进行调用时的拦截和增强。

typescript 复制代码
//第一步:定义接口
public interface ISender {
    public boolean send();
}

//第二步:定义实现上述接口的被代理类
public class SmsSender implements ISender {
    public boolean send() {
        System.out.println("Sending msg");
        return true;
    }
}

//第三步:定义一个InvocationHandler类
public class JdkProxyHandler implements InvocationHandler {
    private Object target;
    
    public JdkProxyHandler(Object target) {
        this.target = target;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("处理前");
        Object result = method.invoke(target, args);
        System.out.println("处理后");
        return result;
    }
}

//第四步:客户端调用
@Test
public void testJdkProxy() {
    //动态生成一个代理类,并返回一个实现了被代理类接口的代理对象
    //入参是:类加载器、被代理类的类型、封装了一个被代理对象的InvocationHandler对象
    ISender sender = (ISender) Proxy.newProxyInstance(
        ClassLoader.getSystemClassLoader(),
        new Class[]{ISender.class},
        new JdkProxyHandler(new SmsSender())
    );
    
    //向代理对象调用被代理类的接口方法
    boolean result = sender.send();
    System.out.println("代理对象:" + sender.getClass().getName());
    System.out.println("输出结果:" + result);
}

JDK动态代理是通过Proxy.newProxyInstance()方法来动态生成代理对象的,JDK动态代理的底层是通过Java反射机制实现的,并且需要目标对象(被代理对象)继承自一个接口才能生成它的代理类。

java 复制代码
public class Proxy implements java.io.Serializable {
    ...
    //该方法有3个参数:
    //loader:用哪个类加载器去加载代理对象,生成目标对象的代理需要确保其类加载器相同,所以需要将目标对象的类加载器作为参数传递
    //interfaces:代理类需实现的接口列表,JDK动态代理技术需要代理类和目标对象都继承自同一接口,所以需要将目标对象的接口作为参数传递
    //h:调用处理器,调用实现了InvocationHandler类的一个回调方法,对目标对象的增强逻辑在这个实现类中
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
        Objects.requireNonNull(h);
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
        //获取被代理类的类型
        Class<?> cl = getProxyClass0(loader, intfs);
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            //通过反射创建代理对象实例
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }
    ...
}

二.CGLib动态代理

和JDK动态代理不同,CGLib动态代理不需要目标对象实现自一个接口,只需要实现一个处理代理逻辑的切入类以及实现MethodInterceptor接口。

CGLib动态代理的特点如下:

使用CGLib实现动态代理,完全不受被代理类必须实现自一个接口的限制。CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类比使用Java反射的效率要高。CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类。

typescript 复制代码
//使用CGLib动态代理前需要引入依赖
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

//第一步:定义一个被代理类
public class BdSender {
    public boolean send() {
        System.out.println("Sending msg");
        return true;
    }
}

//第二步:实现一个处理代理逻辑的切入类以及实现MethodInterceptor接口
public class CglibProxyInterceptor implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();
    
    //获取代理类
    //@param clazz 被代理类
    public Object getProxy(Class clazz) {
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("处理前");
        Object result = methodProxy.invokeSuper(object,args);
        System.out.println("处理后");
        return result;
    }
}

//第三步:客户端调用
@Test
public void testCglibProxy(){
    BdSender sender = (BdSender) new CglibProxyInterceptor().getProxy(BdSender.class);
    boolean result = sender.send();
    System.out.println("代理对象:" + sender.getClass().getName());
    System.out.println("输出结果:" + result);
}

三.Javassist动态代理

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。相对于ASM这些工具,开发者不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。

使用Javassist生成动态代理有以下两种方式:

代理工厂创建:需要实现一个用于处理代理逻辑的切入类以及实现MethodHandler接口,类似CGLib。

动态代码创建:可以通过Java代码生成字节码,这种方式创建的动态代理非常灵活,甚至可以在运行时生成业务逻辑。

方式一:代理工厂创建

typescript 复制代码
//使用Javassist前需要引入依赖
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

//第一步:定义一个被代理类
public class BdSender {
    public boolean send() {
        System.out.println("Sending msg");
        return true;
    }
}

//第二步:实现一个处理代理逻辑的切入类以及实现MethodHandler接口
public class JavassistProxyHandler implements MethodHandler {
    private ProxyFactory proxyFactory = new ProxyFactory();
    
    //获取代理对象
    //@param clazz 被代理类
    public Object getProxy(Class clazz) throws Exception {
        proxyFactory.setSuperclass(clazz);
        Class<?> factoryClass = proxyFactory.createClass();
        Object proxy = factoryClass.newInstance();
        ((ProxyObject)proxy).setHandler(this);
        return proxy;
    }
    
    @Override
    public Object invoke(Object object, Method method, Method method1, Object[] args) throws Throwable {
        System.out.println("处理前");
        Object result = method1.invoke(object,args);
        System.out.println("处理后");
        return result;
    }
}

//第三步:客户端调用
@Test
public void testJavassistProxy() throws Exception {
    BdSender sender = (BdSender) new JavassistProxyHandler().getProxy(BdSender.class);
    boolean result = sender.send();
    System.out.println("代理对象:" + sender.getClass().getName());
    System.out.println("输出结果:" + result);
}

方式二:动态代码创建

java 复制代码
//第一步:定义一个被代理类
public class BdSender {
    public boolean send() {
        System.out.println("Sending msg");
        return true;
    }
}

//第二步:生成字节码
public class JavassistDynamicCodeProxy {
    public static Object getProxy(Class clazz) throws Exception {
        ClassPool mPool = ClassPool.getDefault();
        CtClass c0 = mPool.get(clazz.getName());
        //定义代理类名称
        CtClass mCtc = mPool.makeClass(clazz.getName() + "$$BytecodeProxy");
        //添加父类继承
        mCtc.setSuperclass(c0);
        //添加类的字段信息
        CtField field = new CtField(c0, "real", mCtc);
        field.setModifiers(AccessFlag.PRIVATE);
        mCtc.addField(field);
        //添加构造函数
        CtConstructor constructor = new CtConstructor(new CtClass[]{c0},mCtc);
        constructor.setBody("{$0.real = $1;}"); // $0代表this, $1代表构造函数的第1个参数
        mCtc.addConstructor(constructor);
        //添加方法
        CtMethod ctMethod = mCtc.getSuperclass().getDeclaredMethod("send");
        CtMethod newMethod = new CtMethod(ctMethod.getReturnType(), ctMethod.getName(),ctMethod.getParameterTypes(), mCtc);
        newMethod.setBody("{" +
            "System.out.println("处理前");" +
            "boolean result = $0.real.send();" +
            "System.out.println("处理后");" +
            "return result;}");
        mCtc.addMethod(newMethod);
        //生成动态类
        return mCtc.toClass().getConstructor(clazz).newInstance(clazz.newInstance());
    }
}

//第三步:客户端调用
@Test
public void testJavassisBytecodetProxy() throws Exception {
    BdSender sender = (BdSender) JavassistDynamicCodeProxy.getProxy(BdSender.class);
    boolean result = sender.send();
    System.out.println("代理对象:" + sender.getClass().getName());
    System.out.println("输出结果:" + result);
}

(4)动态代理总结

JDK动态代理需要实现InvocationHandler接口,CGLib动态代理需要实现MethodInterceptor接口,Javassist动态代理需要实现MethodHandler接口或者直接生成字节码。

动态代理中的动态是针对静态代理而言的,动态代理的优势不在于省去了编写静态代理类时的代码量,而是实现了可以在被代理类未知的时候就确定代理类的代理行为。

2.RPC服务调用端动态代理实现

RPC服务对目标接口发起调用时,首先会使用比如JDK动态代理的方式去创建一个动态代理类,然后再由这个动态代理类通过Netty将调用请求发送给目标机器进行处理。

所以关键代码如下:

Proxy.newProxyInstance() ->

nettyRpcClient.connect() ->

nettyRpcClient.remoteCall(rpcRequest)

java 复制代码
public class NettyRpcClientTest {
    private static final Logger logger = LogManager.getLogger(NettyRpcClientTest.class);
    public static void main(String[] args) {
        //发起RPC调用时,是针对哪一个接口发起的
        ReferenceConfig referenceConfig = new ReferenceConfig(TestService.class);
        //创建动态代理类
        TestService testService = (TestService) RpcServiceProxy.createProxy(referenceConfig);
        //下面的调用会走到RpcServiceProxy.ServiceProxyInvocationHandler的invoke()方法去
        String result = testService.sayHello("zhangsan");
        logger.info("rpc call finished: " + result);
    }
}

public class RpcServiceProxy {
    //创建代理
    public static Object createProxy(ReferenceConfig referenceConfig) {
        return Proxy.newProxyInstance(
            RpcServiceProxy.class.getClassLoader(),
            new Class[]{referenceConfig.getServiceInterfaceClass()},
            new ServiceProxyInvocationHandler(referenceConfig)
        );
    }
    static class ServiceProxyInvocationHandler implements InvocationHandler {
        private ReferenceConfig referenceConfig;
        public ServiceProxyInvocationHandler(ReferenceConfig referenceConfig) {
            this.referenceConfig = referenceConfig;
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //发起连接
            NettyRpcClient nettyRpcClient = new NettyRpcClient(referenceConfig);
            nettyRpcClient.connect();


            RpcRequest rpcRequest = new RpcRequest();
            rpcRequest.setRequestId(UUID.randomUUID().toString().replace("-", ""));
            rpcRequest.setServiceInterfaceClass(referenceConfig.getServiceInterfaceClass().getName());
            rpcRequest.setMethodName(method.getName());
            rpcRequest.setParameterTypes(method.getParameterTypes());
            rpcRequest.setArgs(args);
        
            //进行RPC调用
            RpcResponse rpcResponse = nettyRpcClient.remoteCall(rpcRequest);
            return rpcResponse.getResult();
        }
    }
}

public class NettyRpcClient {
    private static final Logger logger = LogManager.getLogger(NettyRpcClient.class);

    private String serviceHost;
    private int servicePort;
    private ChannelFuture channelFuture;
    private NettyRpcClientHandler nettyRpcClientHandler;

    public NettyRpcClient(String serviceHost, int servicePort) {
        this.serviceHost = serviceHost;
        this.servicePort = servicePort;
        this.nettyRpcClientHandler = new NettyRpcClientHandler();
    }

    public void connect() {
        logger.info("connecting to Netty RPC server: " + serviceHost + ":" + servicePort);
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
        .group(eventLoopGroup)
        .channel(NioSocketChannel.class)
        .option(ChannelOption.SO_KEEPALIVE, true)//长时间没有通信就发送一个检测包
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                socketChannel.pipeline()
                .addLast(new RpcEncoder(RpcRequest.class))
                .addLast(new RpcDecoder(RpcResponse.class))
                .addLast(new NettyRpcReadTimeoutHandler())
                .addLast(nettyRpcClientHandler);
            }


        });

        try {
            if (serviceHost != null && !serviceHost.equals("")) {
                //通过connect()方法建立连接后,就会通过sync()方法进行同步阻塞
                channelFuture = bootstrap.connect(serviceHost, servicePort).sync();
                logger.info("successfully connected.");
            }
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public RpcResponse remoteCall(RpcRequest rpcRequest) throws Throwable {
        channelFuture.channel().writeAndFlush(rpcRequest).sync();
        RpcResponse rpcResponse = nettyRpcClientHandler.getRpcResponse();
        logger.info("receives response from netty rpc server.");

        if (rpcResponse.isSuccess()) {
            return rpcResponse;
        }
        throw rpcResponse.getException();
    }
}

3.Netty客户端之RPC远程调用过程分析

NettyRpcClient.remoteCall()方法的执行逻辑:

说明一:Netty的客户端和服务端通过connect()方法建立连接后,就会通过sync()方法进行同步阻塞。

说明二:RPC调用其实就是通过调用remoteCall()方法,往Netty客户端的Channel的writeAndFlush()方法写入请求数据,同时也通过sync()方法进行同步阻塞,以便可以等到Netty服务端的响应,从而获得RPC调用结果。

说明三:writeAndFlush()所写的请求数据会经过客户端Channel的pipeline进行处理如编码成二进制字节数组,然后传输给服务端的Channel。

说明四:服务端的Channel收到请求数据后,会经过其pipeline处理如解码二进制字节数据成对象来反射调用对应的方法,然后服务端将反射调用的结果作为响应数据编码后发送回客户端,最后客户端的Channel收到数据解码后获取的响应对象便是RPC调用结果。

java 复制代码
public class NettyRpcClient {
    ...
    //如果要实现超时功能,需要在remoteCall()方法被执行时设置该请求的发起时间
    //然后在NettyRpcClientHandler的channelRead()中计算是否超时
    public RpcResponse remoteCall(RpcRequest rpcRequest) throws Throwable {
        channelFuture.channel().writeAndFlush(rpcRequest).sync();
        RpcResponse rpcResponse = nettyRpcClientHandler.getRpcResponse();
        logger.info("receives response from netty rpc server.");

        if (rpcResponse.isSuccess()) {
            return rpcResponse;
        }
        throw rpcResponse.getException();
    }
}

public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LogManager.getLogger(NettyRpcClientHandler.class);
    private RpcResponse rpcResponse;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        this.rpcResponse = (RpcResponse) msg;
        logger.error("Netty RPC client receives the response: " + rpcResponse);
    }

    public RpcResponse getRpcResponse() {
        while (rpcResponse == null) {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                logger.error("wait for response interrupted", e);
            }
        }
        return rpcResponse;
    }
}

4.RPC网络通信中的编码解码器

(1)RPC的请求响应通信协议

(2)使用Hessian进行序列化与反序列化

(3)RPC的编码器

(4)RPC的解码器

(5)如何解决粘包拆包问题

(1)RPC的请求响应通信协议

typescript 复制代码
//RPC请求
public class RpcRequest {
    private String requestId;
    private String className;
    private String methodName;
    private String[] parameterClasses;//参数类型
    private Object[] parameters;//参数值
    private String invokerApplicationName;//调用方的服务名称
    private String invokerIp;//调用方的IP地址
    ...
}

//RPC响应
public class RpcResponse {
    private String requestId;
    private boolean isSuccess;
    private Object result;
    private Throwable exception;
    ...
}

(2)Hessian序列化与反序列化

需要将请求对象和响应对象序列化成二进制字节数组,以及将获取到的二进制字节数组反序列化成请求对象和响应对象,这里使用Hessian框架来实现序列化与反序列化。

java 复制代码
public class HessianSerialization {
    //序列化:将对象序列化成字节数组
    public static byte[] serialize(Object object) throws IOException {
        //new一个字节数组输出流
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        //根据字节数组输出流new一个Hessian序列化输出流
        HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
        //用Hessian序列化输出流去写object
        hessianOutput.writeObject(object);
        //将Hessian序列化输出流转化为字节数组
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;
    }
    
    //反序列化:将字节数组还原成对象
    public static Object deserialize(byte[] bytes, Class clazz) throws IOException {
        //先封装一个字节数组输入流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        //将字节数组输入流封装到Hessian序列化输入流里去
        HessianInput hessianInput = new HessianInput(byteArrayInputStream);
        //从Hessian序列化输入流读出一个对象
        Object object = hessianInput.readObject(clazz);
        return object;
    }
}

下面对RpcRequest和RpcResponse进行Hessian序列化测试。注意:RpcRequest和RpcResponse这两个类必须要实现Serializable。

arduino 复制代码
public class HessianSerializationTest {
    public static void main(String[] args) throws Exception {
        RpcRequest rpcRequest = new RpcRequest();//先new一个RpcRequest对象
        rpcRequest.setRequestId(UUID.randomUUID().toString().replace("-", ""));
        rpcRequest.setClassName("TestClass");
        rpcRequest.setMethodName("sayHello");
        rpcRequest.setParameterClasses(new String[]{"String"});
        rpcRequest.setParameters(new Object[]{"wjunt"});
        rpcRequest.setInvokerApplicationName("RpcClient");
        rpcRequest.setInvokerIp("127.0.0.1");

        byte[] bytes = HessianSerialization.serialize(rpcRequest);//进行序列化
        System.out.println(bytes.length);

        RpcRequest deSerializedRpcRequest = (RpcRequest) HessianSerialization.deserialize(bytes, RpcRequest.class);
        System.out.println(deSerializedRpcRequest);
    }
}

(3)RPC的编码器

编码可以理解为进行序列化操作,解码可以理解为进行反序列化操作。

编码器RpcEncoder需要继承Netty的MessageToByteEncoder类,解码器RpcDecoder需要继承Netty的ByteToMessageDecoder类。

反序列化的逻辑需要根据序列化时数据的封装逻辑来处理,比如下面编码后的一条数据是由字节数组长度 + 字节数组组成的,因此反序列化需要根据此来写对应的逻辑。

scala 复制代码
public class RpcEncoder extends MessageToByteEncoder {
    //要进行序列化的目标类
    private Class<?> targetClass;
    
    public RpcEncoder(Class<?> targetClass) {
        this.targetClass = targetClass;
    }
    
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        //传入的对象o是否是Encoder所指定的类的实例对象
        if (targetClass.isInstance(o)) {
            byte[] bytes = HessianSerialization.serialize(o);

            //将序列化好的字节数组写到byteBuf里去
            //先写数据长度到byteBuf,这个长度就是4个字节的bytes的length
            byteBuf.writeInt(bytes.length);
            //然后再写完整的bytes数组到byteBuf
            byteBuf.writeBytes(bytes);
        }
    }
}

(4)RPC的解码器

解码器的主要步骤如下:

步骤一:消息长度校验与读索引标记

步骤二:消息长度负值校验与拆包校验

步骤三:拆包问题处理与读索引复位

步骤四:将字节数组反序列化为指定类

java 复制代码
public class RpcDecoder extends ByteToMessageDecoder {
    private static final int MESSAGE_LENGTH_BYTES = 4;
    private static final int MESSAGE_LENGTH_VALID_MINIMUM_VALUE = 0;

    private Class<?> targetClass;
    public RpcDecoder(Class<?> targetClass) {
        this.targetClass = targetClass;
    }

    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        //1.消息长度校验
        //首先校验消息长度的字节数,也就是byteBuf当前可读的字节数,必须达到4个字节,此时才可以继续往下走
        if (byteBuf.readableBytes() < MESSAGE_LENGTH_BYTES) {
            return;
        }
      
        //2.读索引标记
        //对于byteBuf当前可以读的readerIndex,进行mark标记,也就是进行读索引标记
        //后续可以通过这个mark标记,找回来重新发起read读取之前的一个readerIndex位置
        byteBuf.markReaderIndex();

        //3.读取消息长度
        //读取4个字节的int,int代表了消息bytes的长度
        int messageLength = byteBuf.readInt();

        //4.消息长度负值校验
        //如果此时消息长度是小于0,说明此时通信已经出现了故障
        if (messageLength < MESSAGE_LENGTH_VALID_MINIMUM_VALUE) {
            channelHandlerContext.close();
        }
      
        //5.拆包校验
        //判断可读字节数是否小于消息长度,若是则出现了拆包,需要对byteBuf的读索引进行复位,下次再读
        //byteBuf.readableBytes()读完4个字节后继续读byteBuf.readableBytes()
        //如果此时消息字节数据没有接收完整,那么可以读的字节数是比消息字节长度小的,这就是检查经典的拆包问题
        //这时需要进行读索引进行复位,本次不再进行数据处理
        if (byteBuf.readableBytes() < messageLength) {
            byteBuf.resetReaderIndex();
            //出现拆包后,等待下次数据输入时再进行分析
            //EventLoop里有个for循环会不断监听Channel的读事件;
            //当数据还在传输时,由于传输是一个持续的过程,所以在传输数据过程中,Channel会一直产生读事件;
            //这个过程中,只要循环回来执行判断,就肯定满足监听到Channel的读事件;
            //因此在数据还没传输完成时,for循环执行到去判断是否有Channel的读事件,就会出现这种拆包问题;
            //所以只要返回不处理并且复位读索引,那么下次for循环到来又可重新处理该Channel的读事件了;
            return;
        }
      
        //6.将字节数组反序列化为指定类
        byte[] bytes = new byte[messageLength];
        byteBuf.readBytes(bytes);
        Object object = HessianSerialization.deserialize(bytes, targetClass);
        list.add(object);
    }
}

(5)如何解决粘包拆包问题

首先在编码数据包时,需要在数据包开头添加4个字节的int类型的bytes.length,之后任何一个数据包的读取,都必须从4个字节的int(bytes.length)值开始读取,再按照int值读取后续指定数量的字节数,都读取完才能证明读取到一个完整的字节数组。从而解决粘包半包问题,其原理类似于基于长度域的解码器LengthFieldBasedDecoder。

5.Netty服务端之RPC服务提供端的处理

(1)RPC服务提供端NettyServer

(2)基于反射调用请求对象的目标方法

(1)RPC服务提供端NettyRpcServer

java 复制代码
public class ServiceConfig {
    private String serviceName;//调用方的服务名称
    private Class serviceInterfaceClass;//服务接口类型
    private Class serviceClass;
    ...
}

public class NettyRpcServer {
    private static final Logger logger = LogManager.getLogger(NettyRpcServer.class);
    private static final int DEFAULT_PORT = 8998;
    private List<ServiceConfig> serviceConfigs = new CopyOnWriteArrayList<ServiceConfig>();
    private int port;
    
    public NettyRpcServer(int port) {
        this.port = port;
    }
    
    public void start() {
        logger.info("Netty RPC Server Starting...");
        EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup();
        EventLoopGroup workerEventLoopGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
            .group(bossEventLoopGroup, workerEventLoopGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline()
                    .addLast(new RpcDecoder(RpcRequest.class))
                    .addLast(new RpcEncoder(RpcResponse.class))
                    .addLast(new NettyRpcServerHandler(serviceConfigs));
                }
            })
            .option(ChannelOption.SO_BACKLOG, 128)
            .childOption(ChannelOption.SO_KEEPALIVE, true);

            //到这一步为止,server启动了而且监听指定的端口号了
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            logger.info("Netty RPC Server started successfully, listened[" + port + "]");
            //进入一个阻塞的状态,同步一直等待到你的server端要关闭掉
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            logger.error("Netty RPC Server failed to start, listened[" + port + "]");
        } finally {
            bossEventLoopGroup.shutdownGracefully();
            workerEventLoopGroup.shutdownGracefully();
        }
    }
    
    //可以代理多个服务
    public void addServiceConfig(ServiceConfig serviceConfig) {
        this.serviceConfigs.add(serviceConfig);
    }
    
    public static void main(String[] args) {
        ServiceConfig serviceConfig = new ServiceConfig( "TestService", TestService.class, TestServiceImpl.class);
        NettyRpcServer nettyRpcServer = new NettyRpcServer(DEFAULT_PORT);
        nettyRpcServer.addServiceConfig(serviceConfig);
        nettyRpcServer.start();
    }
}

(2)基于反射调用请求对象的目标方法

ini 复制代码
//RpcRequest类需要修改字段调整为如下所示
public class RpcRequest implements Serializable {
    private String requestId;
    private String className;
    private String methodName;
    private Class[] parameterTypes;//参数类型
    private Object[] args;//参数值
    private String invokerApplicationName;//调用方的服务名称
    private String invokerIp;//调用方的IP地址
    ...
}

public class NettyRpcServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LogManager.getLogger(NettyRpcServerHandler.class);
    private ConcurrentHashMap<String, ServiceConfig> serviceConfigMap = new ConcurrentHashMap<String, ServiceConfig>();
    
    public NettyRpcServerHandler(List<ServiceConfig> serviceConfigs) {
        for (ServiceConfig serviceConfig : serviceConfigs) {
            String serviceInterfaceClass = serviceConfig.getServiceInterfaceClass().getName();
            serviceConfigMap.put(serviceInterfaceClass, serviceConfig);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        RpcRequest rpcRequest = (RpcRequest)msg;
        logger.info("Netty RPC Server receives the request: " + rpcRequest);
        RpcResponse rpcResponse = new RpcResponse();
        rpcResponse.setRequestId(rpcRequest.getRequestId());
        try {
            //此时我们要实现什么呢?
            //我们需要根据RpcRequest指定的class,获取到这个class
            //然后通过反射构建这个class对象实例
            //接着通过反射获取到这个RpcRequest指定方法和入参类型的method
            //最后通过反射调用,传入方法,拿到返回值

            //根据接口名字拿到接口实现类的名字后再获取类
            ServiceConfig serviceConfig = serviceConfigMap.get(rpcRequest.getServiceInterfaceClass());
            Class clazz = serviceConfig.getServiceClass();
            Object instance = clazz.newInstance();
            Method method = clazz.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
            Object result = method.invoke(instance, rpcRequest.getArgs());

            //把rpc调用结果封装到响应里去
            rpcResponse.setResult(result);
            rpcResponse.setSuccess(RpcResponse.SUCCESS);
        } catch(Exception e) {
            logger.error("Netty RPC Server failed to response the request.", e);
            rpcResponse.setSuccess(RpcResponse.FAILURE);
            rpcResponse.setException(e);
        }
        ctx.write(rpcResponse);
        ctx.flush();
        logger.info("send RPC response to client: " + rpcResponse);
    }
}

6.RPC服务调用端实现超时功能

java 复制代码
public class ReferenceConfig {
    private static final long DEFAULT_TIMEOUT = 5000;
    private static final String DEFAULT_SERVICE_HOST = "127.0.0.1";
    private static final int DEFAULT_SERVICE_PORT = 8998;

    private Class serviceInterfaceClass;
    private String serviceHost;
    private int servicePort;
    private long timeout;
    ...
}

public class NettyRpcClient {
    private static final Logger logger = LogManager.getLogger(NettyRpcClient.class);

    private ReferenceConfig referenceConfig;
    private ChannelFuture channelFuture;
    private NettyRpcClientHandler nettyRpcClientHandler;
    
    public NettyRpcClient(ReferenceConfig referenceConfig) {
        this.referenceConfig = referenceConfig;
        this.nettyRpcClientHandler = new NettyRpcClientHandler(referenceConfig.getTimeout());
    }

    public void connect() {
        logger.info("connecting to Netty RPC server: " + referenceConfig.getServiceHost() + ":" + referenceConfig.getServicePort());
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap
        .group(eventLoopGroup)
        .channel(NioSocketChannel.class)
        .option(ChannelOption.SO_KEEPALIVE, true)//长时间没有通信就发送一个检测包
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                socketChannel.pipeline()
                .addLast(new RpcEncoder(RpcRequest.class))
                .addLast(new RpcDecoder(RpcResponse.class))
                .addLast(new NettyRpcReadTimeoutHandler(referenceConfig.getTimeout()))
                .addLast(nettyRpcClientHandler);
            }
        });       

        try {
            if (referenceConfig.getServiceHost() != null && !referenceConfig.getServiceHost().equals("")) {
                channelFuture = bootstrap.connect(referenceConfig.getServiceHost(), referenceConfig.getServicePort()).sync();
                logger.info("successfully connected.");
            }
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public RpcResponse remoteCall(RpcRequest rpcRequest) throws Throwable {
        //标记一下请求发起的时间
        NettyRpcRequestTimeHolder.put(rpcRequest.getRequestId(), new Date().getTime());
        channelFuture.channel().writeAndFlush(rpcRequest).sync();
        RpcResponse rpcResponse = nettyRpcClientHandler.getRpcResponse(rpcRequest.getRequestId());
        logger.info("receives response from netty rpc server.");
        if (rpcResponse.isSuccess()) {
            return rpcResponse;
        }
        throw rpcResponse.getException();
    }
}

public class NettyRpcReadTimeoutHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LogManager.getLogger(NettyRpcReadTimeoutHandler.class);
    private long timeout;
    public NettyRpcReadTimeoutHandler(long timeout) {
        this.timeout = timeout;
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        RpcResponse rpcResponse = (RpcResponse)msg;
        long requestTime = NettyRpcRequestTimeHolder.get(rpcResponse.getRequestId());
        long now = new Date().getTime();
        if (now - requestTime >= timeout) {
            rpcResponse.setTimeout(true);
            logger.error("Netty RPC response is marked as timeout status: " + rpcResponse);
        }
      
        //移除发起请求时间的标记
        NettyRpcRequestTimeHolder.remove(rpcResponse.getRequestId());
        ctx.fireChannelRead(rpcResponse);
    }
}

public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LogManager.getLogger(NettyRpcClientHandler.class);
    private static final long GET_RPC_RESPONSE_SLEEP_INTERVAL = 5;
    private ConcurrentHashMap<String, RpcResponse> rpcResponses = new ConcurrentHashMap<String, RpcResponse>();
    private long timeout;

    public NettyRpcClientHandler(long timeout) {
        this.timeout = timeout;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        RpcResponse rpcResponse = (RpcResponse) msg;
        if (rpcResponse.getTimeout()) {
            logger.error("Netty RPC client receives the response timeout: " + rpcResponse);
        } else {
            rpcResponses.put(rpcResponse.getRequestId(), rpcResponse);
            logger.info("Netty RPC client receives the response: " + rpcResponse);
        }
    }

    public RpcResponse getRpcResponse(String requestId) throws NettyRpcReadTimeoutException {
        long waitStartTime = new Date().getTime();
        while (rpcResponses.get(requestId) == null) {
            try {
                long now = new Date().getTime();
                if (now - waitStartTime >= timeout) {
                    break;
                }
                Thread.sleep(GET_RPC_RESPONSE_SLEEP_INTERVAL);
            } catch (InterruptedException e) {
                logger.error("wait for response interrupted", e);
            }
        }
        RpcResponse rpcResponse = rpcResponses.get(requestId);
        if (rpcResponse == null) {
            logger.error("Get RPC response timeout.");
            throw new NettyRpcReadTimeoutException("Get RPC response timeout.");
        } else {
            rpcResponses.remove(requestId);
        }
        return rpcResponse;
    }
}
相关推荐
livemetee5 天前
netty单线程并发量评估对比tomcat
java·tomcat·netty
冷环渊11 天前
Finish技术生态计划: FinishRpc
java·后端·nacos·rpc·netty
你熬夜了吗?17 天前
spring中使用netty-socketio部署到服务器(SSL、nginx转发)
服务器·websocket·spring·netty·ssl
异常君18 天前
Netty Reactor 线程模型详解:构建高性能网络应用的关键
java·后端·netty
次元18 天前
初识Netty的奇经八脉
netty
南客先生18 天前
马架构的Netty、MQTT、CoAP面试之旅
java·mqtt·面试·netty·coap
异常君21 天前
一文吃透 Netty 处理粘包拆包的核心原理与实践
java·后端·netty
猫吻鱼22 天前
【Netty4核心原理】【全系列文章目录】
netty
用户905558421480523 天前
AdaptiveRecvByteBuAllocator 源码分析
netty
菜菜的后端私房菜24 天前
深入剖析 Netty 中的 NioEventLoopGroup:架构与实现
java·后端·netty