如何寫一個RPC框架(五):服務器端實現

在後續一段時間裏, 我會寫一系列文章來講述如何實現一個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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章