netty实现简单的rpc,支持服务集群

前言

简介

最近了解了下netty相关知识,简单实现一个基于nettyrpc demo,参考了几篇文章,其中这篇清幽之地大佬的RPC基本原理以及如何用Netty来实现RPC 非常不错 ,给我不少启迪,关于rpc的相关知识可以阅读该文,本文主要对如何阐述如何使用netty实现rpc。与清幽之地大佬的demo相比,增添

1. 按服务名加载集群,采用随机选择服务方式负载
2. 每个实例可以同时作为消费者和生产者

demo主要围绕以下两图开发:

Dubbo 架构图

dubbo架构图
rpc调用示意图
rpc调用

环境准备

主要环境nettySpringBootzookeeperzkclient

  • netty 作为通信基础框架
  • SpringBoot 作为基础框架,打包、测试、运行
  • zookeeper 作为服务注册中心
  • zkclient zookeeper客户端

Netty 处理器链设计

netty开发,处理器链pipeline设计是非常重要的一个过程,从byte数据入站,设计不同的入站处理器将其一步步转换我们可直接使用的数据类型,且因为tcp连接保证数据传输的先后顺序,但由于网络传输拥塞窗口等原因影响,可能会发生拆包、粘包情况,可以借助netty框架内置的处理器和自定义数据协议方式解决。出站处理器设计则是逆向上述过程,将我们的数据转化为byte的过程。

使用到的处理器和相关实体类
1. ByteMsgToDataMsgDecoder 借助netty本身的ReplayingDecoder 加自定义数据协议的方式解决拆、粘包问题,将入站数数解码为DataMsg类型的解码器

public class ByteMsgToDataMsgDecoder extends ReplayingDecoder<DataMsg> {
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> out) throws Exception {
        int length = in.readInt();
        byte[] content = new byte[length];
        in.readBytes(content);
        DataMsg msg = new DataMsg(length, content);
        out.add(msg);
    }
}

数据传输实体DataMsg

@Data
@AllArgsConstructor
public class DataMsg {
    //数据长度
    private int length;
    //数据
    private byte[] data;
}

2. DataMsgToByteMsgEncoder 将出站DataMsg编码为Byte的解码器

public class DataMsgToByteMsgEncoder extends MessageToByteEncoder<DataMsg> {
    protected void encode(ChannelHandlerContext ctx, DataMsg msg, ByteBuf out) throws Exception {
        out.writeInt(msg.getLength());
        out.writeBytes(msg.getData());
    }
}

3.DataMsgToRequestOrResponseConvert 根据入参将DataMsg其中数据转换为请求、响应数据,根据服务消费者和生产者入站数据不同,进行不同的反序列化操作。

public class DataMsgToRequestOrResponseConvert extends SimpleChannelInboundHandler<DataMsg> {
    private Logger logger= LoggerFactory.getLogger(DataMsgToRequestOrResponseConvert.class);
    //需要转换目标类,即Response 或者 Request
    private Class<?> target;

    public DataMsgToRequestOrResponseConvert(Class<?> target) {
        this.target = target;
    }

    protected void channelRead0(ChannelHandlerContext ctx, DataMsg msg) throws Exception {
        final byte[] data = msg.getData();
        Object object = JsonUtil.bytesToObject(data, target);
        ctx.fireChannelRead(object);
        logger.info("{}数据:{}",target.getSimpleName(),JsonUtil.objectToJsonString(object));
    }
}

请求数据类Request 封装消费者rpc服务时的参数

@Data
public class Request {
    //请求ID
    private String id;
    // 类名
    private String className;
    // 函数名称
    private String methodName;
    // 参数类型
    private Class<?>[] parameterTypes;
    // 参数列表
    private Object[] parameters;

}

响应数据类Response 封装rpc生产者返回的数据

@Data
public class Response {
	//请求ID
    private String requestId;
    //代码
    private int code;
    //错误信息
    private String errorMsg;
    //数据
    private Object data;
}
  1. NettyServerRequestHandler 生产者核心处理器(后续切入)
  2. NettyClientResponseHandler 消费者核心处理(后续切入)

服务生产者链式示意图:
在这里插入图片描述

服务消费者链式示意图:

在这里插入图片描述

消费者RPC代理工厂设计

为所有服务消费者对目标服务生产者的调用,生成代理,从代理转入NettyClientResponseHandler类处理,借助spring ApplicationContextAware、以及jdk的动态代理实现上述操作。
相关核心代码
spring bean工具类

@Component
public class SpringBeanUtil implements ApplicationContextAware, InitializingBean {

    static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringBeanUtil.context = applicationContext;
    }

    public static <T> T getBeanByClass(Class<?> clazz) {
        return (T) context.getBean(clazz);
    }
    @Override
    public void afterPropertiesSet() throws Exception {
    	//获取含有指定注解的bean
        Map<String, Object> beans = context.getBeansWithAnnotation(RpcClientTarget.class);
        Iterator<Object> iterator = beans.values().iterator();
        while (iterator.hasNext()) {
            Object target = iterator.next();
            Field[] fields = target.getClass().getDeclaredFields();
            for (Field field : fields) {
            	//获取目标bean有需要被代理的field
                if (field.getAnnotationsByType(MyResource.class) != null) {
                    field.setAccessible(true);
                    try {
                        field.set(target, ProxyBeanUtil.getProxy(field.getType()));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

jdk代理工具类

@Component
public class RpcFactory implements InvocationHandler {

    @Autowired
    NettyClient client;

    Logger logger = LoggerFactory.getLogger(this.getClass());

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Object.class) {
            logger.info("method:{}代理执行,为Object声明的不进行代理执行", method.getName());
            return method.invoke(proxy, args);
        }
        logger.info("method:{}代理执行", method.getName());
        Request request = new Request();
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParameters(args);
        request.setParameterTypes(method.getParameterTypes());
        request.setId(UUID.randomUUID().toString());
        String serverName = method.getDeclaringClass().getAnnotation(RpcClient.class).name();
        Response response = client.sendMsgV2(serverName,request);
        Class<?> returnType = method.getReturnType();
        //返回错误
        if (response.getCode() == ErrorCodeEnums.FAIL.getCode()) {
            throw new Exception(response.getErrorMsg());
        }
        /*
         *1.返回结果是基础类型或者String类型,直接返回
         *2.其他类型转换为该类型
         */
        if (returnType.isPrimitive() || String.class.isAssignableFrom(returnType)) {
            return response.getData();
        } else if (Collection.class.isAssignableFrom(returnType)) {
            return JsonUtil.jsonStringToObject(response.getData().toString(), Object.class);
        } else if (Map.class.isAssignableFrom(returnType)) {
            return JsonUtil.jsonStringToObject(response.getData().toString(), Map.class);
        } else {
            Object data = response.getData();
            return JsonUtil.jsonStringToObject(data.toString(), returnType);
        }
    }
}

相关注解类集合

/**
 * @author jxy
 * @className RpcClientTarget
 * @description 含有rpc的目标类
 * @date 2021/3/12 21:52
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcClientTarget {
}
/**
 * @author : porteryao
 * @description : Rpc客户端注解
 * @date : 2021/3/11 11:14
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcClient {
    String name();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyResource {
}

demo Controller

@RestController
@RpcClientTarget //方便命中
public class TestController {
    @MyResource
    private StuService stuService;

    @GetMapping("/test")
    public String test() {
        StuInfo info = new StuInfo("jxt", 18);
        return stuService.sayHello(info);
    }
}

service 接口

@RpcClient(name = "stuservice")
public interface StuService {
    String sayHello(StuInfo stuInfo);
}

上述操作可将需要代理执行rpc的接口,转到我们自定义的netty 相关代码执行

netty rpc消费者核心设计

  1. 根据需要代理执行service上注解RpcClient获取其服务名,并从连接管理器ConnectManager获取通道
  2. 通道不为空,则发送数据,并在阻塞队列中等待返回
  3. 将返回结果按照service 上的method封装

相关方法代码:

public Response sendMsgV2(String appName, Request request) throws InterruptedException {
        Channel channel = connectManage.chooseChannelByServiceName(appName);
        if (channel == null || (!channel.isActive())) {
            logger.error(appName + "无可用服务!");
            Response res = new Response();
            res.setCode(ErrorCodeEnums.FAIL.getCode());
            res.setErrorMsg(appName + "无可用服务!");
            return res;
        }
        //发送请求
        return clientResponseHandler.sendMsg(channel, request).take();
    }
        /**
     * 等待响应队列
     */
    private Map<String, BlockingQueue<Response>> questMap = new ConcurrentHashMap<>();
 /**
     * 将返回结果放在阻塞队列
     */
    public BlockingQueue<Response> sendMsg(Channel channel, Request request) {
        return getResponses(channel, request, questMap);
    }

    static BlockingQueue<Response> getResponses(Channel channel, Request request,
                                                Map<String, BlockingQueue<Response>> questMap) {
        BlockingQueue<Response> blockingQueue = new LinkedBlockingQueue<>();
        questMap.put(request.getId(), blockingQueue);
        byte[] bytes = JsonUtil.objectToJsonByteArray(request);
        DataMsg dataMsg = new DataMsg(bytes.length, bytes);
        channel.writeAndFlush(dataMsg);
        return blockingQueue;
    }
	
    static BlockingQueue<Response> getResponses(Channel channel, Request request,
                                                Map<String, BlockingQueue<Response>> questMap) {
        BlockingQueue<Response> blockingQueue = new LinkedBlockingQueue<>();
        questMap.put(request.getId(), blockingQueue);
        byte[] bytes = JsonUtil.objectToJsonByteArray(request);
        DataMsg dataMsg = new DataMsg(bytes.length, bytes);
        channel.writeAndFlush(dataMsg);
        return blockingQueue;
    }


    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Response response) {
        BlockingQueue<Response> responseBlockingQueue = questMap.get(response.getRequestId());
        responseBlockingQueue.add(response);
        questMap.remove(response.getRequestId());
    }

由于netty是异步执行,所以这里需要发送者等待,笔者这里take()是无限期等待,可以设置一定时间等待,避免服务端长时间无响应造成调用方阻塞(后续会优化)。
致此,我们完成了rpc调用示意图,关于消费者调用设计的部分。

netty rpc生产者核心设计

生产者这块相对来说比较简单,根据消费者请求数据执行方法,并返回数据。

  1. 启动时将自身netty连接信息注册到注册中心
  2. 接受请求时按Request 其中class类型查询目标bean
  3. 执行目标bean Request 请求的方法
  4. 将结果序列化返回

核心方法

 protected void channelRead0(ChannelHandlerContext ctx, Request request)
            throws Exception {
        if (serviceMap.isEmpty()) {
            logger.warn("没有需要rpc调用的服务");
            return;
        }
        final Object instance = serviceMap.get(request.getClassName());
        if (instance != null) {
            Class<?> instanceClass = instance.getClass();
            Method method = instanceClass
                    .getDeclaredMethod(request.getMethodName(), request.getParameterTypes());
            method.setAccessible(true);//避免权限问题
            Object o = method.invoke(instance,
                    parametersConvert(request.getParameterTypes(), request.getParameters()));
            Response response = new Response();
            response.setCode(ErrorCodeEnums.SUCCESS.getCode());
            response.setData(o);
            response.setErrorMsg(ErrorCodeEnums.SUCCESS.getMsg());
            response.setRequestId(request.getId());
            String jsonString = JsonUtil.objectToJsonString(response);
            byte[] data = jsonString.getBytes(CharsetUtil.UTF_8);
            DataMsg dataMsg = new DataMsg(data.length, data);
            ctx.writeAndFlush(dataMsg);
        }
    }

服务注册、发现以集群

ps:本文篇幅较长,且这块由于逻辑处理复杂,笔者会在闲暇之余补充在单独的文章
主要步骤:

  1. 服务提供者按照application name在zookeeper注册其节点,节点类型为临时节点,便于zookeeper客户端感知
  2. 消费者根据服务调用时检测目标服务是否已存在通道,初次需从zookeeper获取,并设置监听
  3. 根据zookeeper节点信息创建通道连接
  4. zookeeper客户端监听到某服务注册的节点发送变化,并更新对应服务通道

演示Demo

到这了,来看看演示吧:

zookeeper已注册节点
在这里插入图片描述
idea启动情况:

在这里插入图片描述
clientApp:9002实例调用ServerApp
在这里插入图片描述
在这里插入图片描述
由于是随机数负载,可能需要尝试几次才能看到效果

ServerApp:9007实例调用clientApp

在这里插入图片描述
在这里插入图片描述
上述demo,即一个实例即可成为消费者也可成为生产者,并在调用时负载。

尾言

笔者仅做个人学习,措辞不合理,知识不够深入,请勿怪!Thank

相关链接

参考文献
[1]: https://www.jianshu.com/p/8876c9f3cd7f
仓库地址
gitee https://gitee.com/junxiaoyao/jxy_netty_rpc_study
注:master分支为不包含 zookeeper注册中心的demo,如需查看请切换至feature/auto_registry_zookeepe分支

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值