如何写一个RPC框架(五):服务器端实现

18 篇文章 0 订阅
7 篇文章 2 订阅

在后续一段时间里, 我会写一系列文章来讲述如何实现一个RPC框架(我已经实现了一个示例框架, 代码在我的github上)。 这是系列第五篇文章, 主要讲述了服务器端的实现。

在前面的几篇文章里, 我们已经实现了客户端创建proxy bean, 并利用它来发送请求、处理返回的全部流程:

  1. 扫描package找出需要代理的service
  2. 通过服务注册中心和Load Balancer获取service地址
  3. 利用Netty与service建立连接, 并且复用所创建的channel
  4. 创建request, 用唯一的requestId来标识它, 发送这个请求, 调用future.get()
  5. 收到response,利用response中附带的requestId找到对应future,让future变成done的状态

这篇文章, 我们会介绍server端的实现。

1.获取server端所实现的接口

private List<Class<?>> getServiceInterfaces(ApplicationContext ctx) {
        Class<? extends Annotation> clazz = RPCService.class;
        return ctx.getBeansWithAnnotation(clazz)
                .values().stream()
                .map(AopUtils::getTargetClass)
                .map(cls -> Arrays.asList(cls.getInterfaces()))
                .flatMap(List::stream)
                .filter(cls -> Objects.nonNull(cls.getAnnotation(clazz)))
                .collect(Collectors.toList());
    }

利用Spring的ApplicationContext, 获取bean容器中带有RPCService注解的bean。

2.bean与对应的接口名一一对应并保存在map中

private Map<String, Object> handlerMap = new HashMap<>();
getServiceInterfaces(ctx)
                .stream()
                .forEach(interfaceClazz -> {
                    String serviceName = interfaceClazz.getAnnotation(RPCService.class).value().getName();
                    Object serviceBean = ctx.getBean(interfaceClazz);
                    handlerMap.put(serviceName, serviceBean);
                    log.debug("Put handler: {}, {}", serviceName, serviceBean);
                });

handlerMap的作用是, 收到请求时, 可以通过这个map找到该请求所对应的处理对象。

3.启动服务器并注册所实现的service

private void startServer() {
        // Get ip and port
        String[] addressArray = StringUtils.split(serviceAddress, ":");
        String ip = addressArray[0];
        int port = Integer.parseInt(addressArray[1]);

        log.debug("Starting server on port: {}", port);
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel channel) throws Exception {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new RPCDecoder(RPCRequest.class, new ProtobufSerializer()));
                            pipeline.addLast(new RPCEncoder(RPCResponse.class, new ProtobufSerializer()));
                            pipeline.addLast(new RPCServerHandler(handlerMap));
                        }
                    });
            bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture future = bootstrap.bind(ip, port).sync();
            log.info("Server started");

            registerServices();

            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            throw new RuntimeException("Server shutdown!", e);
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

private void registerServices() {
        if (serviceRegistry != null && serviceAddress != null) {
            for (String interfaceName : handlerMap.keySet()) {
                serviceRegistry.register(interfaceName, serviceAddress.toString());
                log.info("Registering service: {} with address: {}", interfaceName, serviceAddress);
            }
        }
    }

这里几个地方需要说明一下:

  1. 这里的ip和port我直接用了配置文件中传入的, 优化的方案应该是获取本地ip以及找到一个可用端口
  2. 利用Netty创建server, 在pipeline中加入RPCServerHandler, 这个handler将在下文给出
  3. 向服务注册中心注册实现的所有服务

4.RPCServerHandler的实现

@AllArgsConstructor
public class RPCServerHandler extends SimpleChannelInboundHandler<RPCRequest> {

    private Map<String, Object> handlerMap;

    @Override
    public void channelRead0(final ChannelHandlerContext ctx, RPCRequest request) throws Exception {
        log.debug("Get request: {}", request);
        RPCResponse response = new RPCResponse();
        response.setRequestId(request.getRequestId());
        try {
            Object result = handleRequest(request);
            response.setResult(result);
        } catch (Exception e) {
            log.warn("Get exception when hanlding request, exception: {}", e);
            response.setException(e);
        }
        ctx.writeAndFlush(response).addListener(
                (ChannelFutureListener) channelFuture -> {
                    log.debug("Sent response for request: {}", request.getRequestId());
                });
    }

    private Object handleRequest(RPCRequest request) throws Exception {
        // Get service bean
        String serviceName = request.getInterfaceName();
        Object serviceBean = handlerMap.get(serviceName);
        if (serviceBean == null) {
            throw new RuntimeException(String.format("No service bean available: %s", serviceName));
        }

        // Invoke by reflect
        Class<?> serviceClass = serviceBean.getClass();
        String methodName = request.getMethodName();
        Class<?>[] parameterTypes = request.getParameterTypes();
        Object[] parameters = request.getParameters();
        Method method = serviceClass.getMethod(methodName, parameterTypes);
        method.setAccessible(true);
        return method.invoke(serviceBean, parameters);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("server caught exception", cause);
        ctx.close();
    }
}

这个实现相当简单,收到请求之后, 根据servicename,找到对应的handler,再利用反射进行方法调用。

就这样, 一个简单的RPCServer就实现了。 完整代码请看我的github

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值