使用Netty實現一下簡單RPC

前言

最近在做一個自己的小項目,這個小項目分爲客戶端,路由和服務端,服務端和客戶端之間通過Netty來通信,而路由主要用來做負載均衡,用戶上線下線的操作,客戶端和路由之間本來是採用HTTP來通信,後來一想既然都用到netty了,那就乾脆基於netty做一個RPC來實現客戶端和路由的通信吧。

RPC的概念

RPC,全稱是Remote Procedure Call,從字面意思上也容易理解,通常寫代碼,比如本地寫一個類A,裏面一個方法B,我們要調用A類的B方法就很容易,這叫本地方法調用。而如今,現在很多服務都是在不同的服務器上,如果還是這樣的場景,我們要調用另外一臺服務器上的A類的B方法,這時候就可以用RPC,這使得我們調用A類的B方法就如同調用本地方法一樣。

RPC的實現

可以先簡單羅列一下,實現上面的想法需要哪些東西

  • 網絡通信 netty

這必然是需要的,因爲這裏服務在兩臺服務器上,這裏使用netty來作爲兩端之間的通信框架。

  • 解碼編碼 Jackson

爲了方便通信,使用Json來作爲統一的編碼格式,使用spring默認的Jackson來實現json和對象之間的轉化。

  • 服務註冊與發現 Nacos

服務端是多實例部署,那自然需要一個註冊中心,客戶端選擇的時候可以通過一定的負載均衡算法來選擇一個服務實例進行通信。這裏使用阿里的nacos。

  • 心跳機制

客戶端和服務端建立連接之後每隔一定的時間上報心跳,這裏使用netty自帶的心跳機制來實現。

  • 反射和動態代理

客戶端是沒有調用的接口的具體實現的,所以在服務端需要從請求報文裏得到請求的接口,參數,方法名等信息,通過反射來得到具體的類和方法,再通過動態代理來實現具體的接口方法的調用。

  • 項目腳手架 Springboot
  • ......

當然,這裏如果要做細緻還有很多東西需要考慮,包括熔斷,限流,緩存,這些之後再考慮,這裏先完成我們最初的需求。接下來一個個把這些都實現。爲此花了一個簡單的圖來方便理解這裏面有啥需要實現的模塊。

avatar

公共模塊

  • 實體類

這裏介紹兩個主要的實體,一個是請求類

public class RpcRequest {
    /**
     * 請求id,唯一,有雪花算法生成
     * */
    @Getter
    @Setter
    private Long id;

    /**
     * 請求類型:正常請求0,心跳:1
     * */
    @Getter
    @Setter
    private RequestTypeEnum type;

    /**
     * 類名稱
     * */
    @Getter
    @Setter
    private String className;

    /**
     * 指定運行的方法名稱
     * */
    @Getter
    @Setter
    private String methodName;

    /**
     * 參數類型
     * */
    @Getter
    @Setter
    private Class<?>[] paramTypes;

    /**
     * 參數值
     * */
    @Getter
    @Setter
    private Object[] params;

}

另一個是回覆類

public class RpcResponse {

    /**
     * 請求的ID
     * */
    @Getter
    @Setter
    private Long reqId;

    /**
     * 返回碼
     * */
    @Getter
    @Setter
    private RespCodeEnum code;

    /**
     * 發生錯誤時的錯誤信息
     * */
    @Getter
    @Setter
    private String errorMsg;

    /**
     * 正常返回下返回信息
     * */
    @Getter
    @Setter
    private Object data;

}
  • 工具類

工具類包括對對象和Json字符串/字節數組進行相互轉化的JsonUtil,以及一個用來生成請求ID的算法,算法採用的是Twitter開源的雪花算法,代碼是從網上借鑑來的。Json是用的Jackson做的解析。

public class JsonUtil{

    private static final ObjectMapper mapper = new ObjectMapper();

    /**
     * 對象轉Json字符串
     * */
    public static String parseToJsonStr(Object object) {
        String result = null;
        try {
            result = mapper.writeValueAsString(object);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 字節數組轉Json字符串
     * */
    public static byte[] parseToJsonBytes(Object object) {
        byte[] result = null;
        try {
            result = mapper.writeValueAsBytes(object);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return result;
    }

    /**
     * Json字符串轉爲對象
     * */
    public static <T> T parseToObject(String json,Class<T> clazz){
        T t = null;
        try {
            t = mapper.readValue(json,clazz);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }
        return t;
    }


    /**
     * 字節數組轉爲對象
     * */
    public static <T> T parseToObject(byte[] jsonBytes,Class<T> clazz){
        T t = null;
        try {
            t = mapper.readValue(jsonBytes,clazz);
        }catch (JsonProcessingException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
        return t;
    }

    /**
     * 獲取ObjectMapper
     * */
    public static ObjectMapper getMapper() {
        return mapper;
    }
}

服務註冊和發現

Nacos是阿里的開源的一款中間件,既有服務註冊發現,也有配置中心的作用,在java工程裏這兩個依賴也是獨立的。使用方面,跟着官網文檔走就可以了。

  • 服務註冊

先需要把Nacos的地址,服務名稱,namespace這些配置好。

spring:
  application:
    name: rpc_producer
server:
  port: 7001

rpc:
  heartbeat: 600

app:
  ip: 127.0.0.1
  port: 8001

nacos:
  discovery:
    server-addr: 127.0.0.1:8848
    namespace: 35375ce2-f421-431f-bd2e-89677440dc9f
    register:
      group-name: netty_rpc_provider

接下來就是註冊了,註冊一個實例需要服務名,服務所在的IP和端口。

@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class RpcProducerApplication implements CommandLineRunner {

    @NacosInjected
    private NamingService namingService;

    @Value("${spring.application.name}")
    private String appName;

    @Value("${app.port}")
    private Integer serverPort;

    @Value("${app.ip}")
    private String serverIp;

    public static void main(String[] args) {
        SpringApplication.run(RpcProducerApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        log.info(String.format("Register service name:%s,port:%s",appName,serverPort));
        namingService.registerInstance(appName,serverIp,serverPort);
    }
}
  • 服務發現

    服務發現相比於服務註冊多了一些步驟,因爲服務端是多實例的,所以需要獲取當前服務的所有實例,然後採用一定的負載均衡算法來選擇其中一個實例。這裏本項目暫且使用輪詢算法來做負載均衡。

    • 獲取Nacos中的所有實例,然後將Channel的列表交給專門的連接管理器來維護。ConnectionManager後續在負載均衡的時候用到。
    @Component
    public class RpcServiceDiscovery {
    
        private static final Logger logger = LoggerFactory.getLogger(RpcServiceDiscovery.class);
    
        @NacosInjected
        private NamingService namingService;
    
        @Value("${rpc.provider}")
        private String providerRegisterName;
    
        @Autowired
        private ConnectionManager connectionManager;
    
        private volatile List<ServerAddress> serverList = new ArrayList<>();
    
        @PostConstruct
        public void init(){
            List<Instance> instanceList;
            try {
                instanceList = namingService.getAllInstances(providerRegisterName);
                for (Instance ins : instanceList){
                    ServerAddress addr = new ServerAddress();
                    addr.setIp(ins.getIp());
                    addr.setPort(ins.getPort());
                    serverList.add(addr);
                }
                updateConnection();
            } catch (NacosException e) {
                logger.info(String.format("Nacos 獲取全部實例異常 %s",e));
            }
        }
    
        private void updateConnection(){
            connectionManager.updateConnection(serverList);
        }
    }
    
    

    ServerAddress是自定義類,結構比較簡單,就是IP和端口。

    public class ServerAddress {
    
        @Getter
        @Setter
        private String ip;
    
        @Getter
        @Setter
        private Integer port;
        
    }
    
    
    • 因爲要實現簡單的負載均衡,所以在客戶端發送請求的時候需要從所有的Channel中選出一個來進行通信。下面的ConnectionManager的chooseOneAvailableChannnel就是這個作用。
@Component
public class ConnectionManager {

    private static final Logger logger = LoggerFactory.getLogger(ConnectionManager.class);

    private AtomicInteger channelIndex = new AtomicInteger(0);

    private CopyOnWriteArrayList<Channel> channelList = new CopyOnWriteArrayList<>();

    private Map<SocketChannel,Channel> channelMap = new ConcurrentHashMap<>();

    @Autowired
    private NettyClient client;

    /**
     * 選擇一個可用Channel
     * */
    public Channel chooseOneVariableChannel(){
        if(channelList.size()>0){
            int size = channelList.size();
            //輪詢算法
            int index = (channelIndex.getAndAdd(1)+size)%size;
            return channelList.get(index);
        }else{
            return null;
        }
    }

    /**
     * 更新連接列表
     * */
    public synchronized void updateConnection(List<ServerAddress> serverList)
    {
        if (serverList==null||serverList.size()==0){
            logger.info("沒有可用的服務");
            for (Channel ch : channelList){
                SocketAddress remoteServerAddr = ch.remoteAddress();
                Channel channel = channelMap.get(remoteServerAddr);
                channel.close();
            }
            channelMap.clear();
            channelList.clear();
            return;
        }
        //去重
        HashSet<SocketAddress> serverNodeList = new HashSet<>();
        for(ServerAddress sa : serverList){
            serverNodeList.add(new InetSocketAddress(sa.getIp(),sa.getPort()));
        }

        for (SocketAddress addr : serverNodeList){
            Channel channel = channelMap.get(addr);
            if(channel!=null&&channel.isOpen()){
                logger.info("服務{}已經存在,不需要重新連接",addr);
            }
            //Channel沒打開的情況下,重新連接
            connectToServer(addr);
        }
        //移除無效節點
        for (Channel ch : channelList){
            SocketAddress addr = ch.remoteAddress();
            if(!serverNodeList.contains(addr)){
                logger.info("服務{}無效,自動移除",addr);
                Channel channel = channelMap.get(addr);
                if (channel!=null){
                    channel.close();
                }
                channelList.remove(channel);
                channelMap.remove(addr);
            }
        }
    }
    /**
     * 連接到服務器
     * */
    private void connectToServer(SocketAddress addr){
        try {
            Channel channel = client.connect(addr);
            channelList.add(channel);
            logger.info("成功連接到服務器{}",addr);
        }catch (Exception e){
            logger.info("未能連接到服務器{}",addr);
        }
    }
    /**
     * 移除連接
     * */
    public void removeConnection(Channel channel){
        logger.info("Channel:{}已經被移除",channel.remoteAddress());
        SocketAddress address = channel.remoteAddress();
        channelList.remove(channel);
        channelMap.remove(address);
    }
}

解碼編碼

這裏採用的自定義的消息格式是json類型,客戶端發送一個請求到服務端,也就是將RpcRequest對象轉爲json對象,然後經netty到達客戶端,服務端做出反應之後,將RpcResponse對象以同樣的方式發送到客戶端。這裏用的是Springboot自帶的Jackson。但這裏值得注意的是,這兩個端的編碼解碼器並不是相同的實現。

  • 客戶端解碼和編碼
//解碼
public class JsonDecoder extends LengthFieldBasedFrameDecoder {

    private static final Logger logger = LoggerFactory.getLogger(JsonDecoder.class);

    public JsonDecoder(){
        super(65535,0,4,0,4);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf decode = (ByteBuf) super.decode(ctx, in);
        if (decode==null){
            return null;
        }
        int data_len = decode.readableBytes();
        byte[] bytes = new byte[data_len];
        decode.readBytes(bytes);
        logger.info("JsonDecoder 解碼:{}",new String(bytes));
        Object ret = JsonUtil.parseToObject(bytes,RpcResponse.class);
        return ret;
    }
}
//編碼
public class JsonEncoder extends MessageToMessageEncoder {

    private static final Logger logger = LoggerFactory.getLogger(JsonEncoder.class);
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, List list) throws Exception {
        ByteBuf buf = ByteBufAllocator.DEFAULT.ioBuffer();
        byte[] bytes = JsonUtil.parseToJsonBytes(o);
        logger.info("JsonEncoder 編碼:{}",new String(bytes));
        buf.writeInt(bytes.length);
        buf.writeBytes(bytes);
        list.add(buf);
    }
}

  • 服務端解碼和編碼
//解碼
public class JsonDecoder extends LengthFieldBasedFrameDecoder {

    private static final Logger logger = LoggerFactory.getLogger(JsonDecoder.class);

    public JsonDecoder(){
        super(65535,0,4,0,4);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf decode = (ByteBuf) super.decode(ctx, in);
        if (decode==null){
            return null;
        }
        int data_len = decode.readableBytes();
        byte[] bytes = new byte[data_len];
        decode.readBytes(bytes);
        Object ret = JsonUtil.parseToObject(bytes,RpcRequest.class);
        logger.info("JsonDecoder 解碼 : {}",new String(bytes));
        return ret;
    }
}
//編碼
public class JsonEncoder extends MessageToMessageEncoder {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, List list) throws Exception {
        ByteBuf buf = ByteBufAllocator.DEFAULT.ioBuffer();
        byte[] bytes = JsonUtil.parseToJsonBytes(o);
        buf.writeInt(bytes.length);
        buf.writeBytes(bytes);
        list.add(buf);
    }
}

心跳機制

服務端需要對客戶端進行心跳檢測,約定好一定的心跳上報時間,如果客戶端在這個時間內沒有上報心跳,那麼服務端將與此客戶端之間的Channel關閉。這裏因爲是用的netty做的通信,所以心跳檢測起來並不麻煩。只要在ChannelPipeline裏面加入一個IdleStateHandler就行了。

  • 添加IdleStateHandler進行心跳配置
@Component
@Slf4j
public class NettyClient {

    private EventLoopGroup eventExecutors = new NioEventLoopGroup();

    private Bootstrap bootstrap = new Bootstrap();

    @Value("${rpc.heartbeat}")
    private int heartBeatTime;

    @Autowired
    private NettyClientHandler handler;

    @Autowired
    private ConnectionManager manager;

    public NettyClient(){
        bootstrap.group(eventExecutors)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY,true)
                .option(ChannelOption.SO_KEEPALIVE,true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        ChannelPipeline pipeline = channel.pipeline();
                        pipeline.addLast(new IdleStateHandler(0,0,heartBeatTime))
                                .addLast(new JsonEncoder())
                                .addLast(new JsonDecoder())
                                .addLast("handler",handler);
                    }
                });

    }
	.....
}

同時還有一個需要注意的地方就是實際上這個項目中的請求,也就是RpcRequest是分爲兩種類別的,一種是心跳,一種是Rpc請求,所以上報心跳的時候需要設置RpcRequest對象的種類。本項目中使用一個枚舉類來對這兩種請求進行分類。

public enum RequestTypeEnum {
    /**
     * 正常請求
     * */
    NORMAL,
    /**
     * 心跳請求
     * */
    HEART_BEAT
}
  • 上報心跳

    Netty中連接關閉,開啓,讀寫這些都是基於事件驅動的,所以這裏上報心跳需要實現ChannelInboundHandlerAdapter,至於這裏頭的netty的出站入站細節就不多說了,心跳的上報只需要重寫userEventTriggered就行。

    /**
     * 心跳上報
     * */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx,Object event){

        try {
            if (event instanceof IdleStateEvent){
                IdleStateEvent evt = (IdleStateEvent)event;
                if (evt.state()==IdleState.ALL_IDLE){
                    RpcRequest request = new RpcRequest();
                    request.setId(IDUtil.getRpcRequestId());
                    request.setType(RequestTypeEnum.HEART_BEAT);
                    request.setMethodName("heartBeat");
                    ctx.channel().writeAndFlush(request);
                    logger.info("客戶端{} s發送心跳",heartBeatTime);
                }
            }else{
                super.userEventTriggered(ctx,event);
            }
        }catch (Exception e){
            logger.error("心跳異常 %s ",e);
        }
    }
  • 接收心跳
    /**
     * 檢查心跳
     * */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx,Object event) throws Exception{
        if(event instanceof IdleStateEvent){
            IdleStateEvent idleStateEvent = (IdleStateEvent)event;
            if(idleStateEvent.state()==IdleState.ALL_IDLE){
                logger.info("客戶端{}心跳未上報,連接關閉",ctx.channel().remoteAddress());
                ctx.channel().close();
            }
        }else{
            super.userEventTriggered(ctx,event);
        }
    }

反射,動態代理

爲啥要用到反射呢,因爲服務端從客戶端發來的請求中可以得到請求的接口,方法,方法的參數類型和參數值,那麼問題來了,這些東西知道了,怎麼纔可以調用呢,其實用笨辦法,窮舉不就完事了,寫上若干個case,但是這樣終究是不優雅的,而且一旦項目大了,接口多了,窮舉自然就不行了。那麼這個時候反射就派上用場了。服務端在接收到請求的時候,我們可以通過反射拿到請求對應的服務端接口的類,方法,這樣我們就可以利用反射拿到一個Method對象,通過Method對象的invoke方法就可以實現對這個接口的這個方法的調用呢。

    /**
     * 處理請求
     * */
    private Object handleRequest(RpcRequest req) throws Exception{
        String className = req.getClassName();
        Object serverInstance = serviceDictionary.get(className);

        if(serverInstance!=null){
            Class<?> serviceClass = serverInstance.getClass();
            String methodName = req.getMethodName();
            Class<?>[] paramTypes = req.getParamTypes();
            Object[] params = req.getParams();
            //獲取方法
            Method method = serviceClass.getMethod(methodName,paramTypes);
            method.setAccessible(true);
            return method.invoke(serverInstance,getParamValues(paramTypes,params));
        }else{
            logger.info("沒有找到實例:{},方法名:{}",className,req.getMethodName());
            throw new Exception("沒有找到合適的RPC實例");
        }
    }
        /**
     * 獲取方法參數的值
     * */
    public Object[] getParamValues(Class<?>[] paramTypes,Object[] params){
        if(params==null||paramTypes==null){
            return params;
        }
        Object[] retValues = new Object[params.length];

        for(int i = 0;i < params.length;i++){
            retValues[i] = JsonUtil.parseToObject(String.valueOf(params[i]),paramTypes[i]);
        }
        return retValues;
    }

那客戶端爲啥又要用動態代理呢,首先需要明確一點,客戶端沒有我們請求的接口的實現,也就是說,我們客戶端只申明瞭一個接口罷了,真正的實現在客戶端,比如這裏我有一個UserService的接口,裏面有一個getUserById的方法。

public interface UserService {
    User getUserById(Integer userId);
}

這裏我們用一般寫web的方式來進行調用,也就是需要在Controller層來調用。比如:

@Controller
public class RpcController {

    private static final Logger logger = LoggerFactory.getLogger(RpcController.class);

    @Autowired
    private UserService userService;

    @RequestMapping("/getUserById")
    @ResponseBody
    public String getUserById(@RequestParam("id")Integer id){
        User user = userService.getUserById(id);
        String jsonStr = JsonUtil.parseToJsonStr(user);
        logger.info("getUserById {}",jsonStr);
        return jsonStr;
    }
}

我們知道,如果這個UserService沒有實現類,這個地方肯定是跑不起來的,但是這裏我們又必須這樣子寫啊,因爲需要做到RPC中的“就像調用本地方法一樣”啊,那怎麼辦呢,不要忘了這裏有Spring,Spring裏面不止只有@Autowired這種來初始化一個bean,不然人家MyBatis爲啥只要定義一個接口加一個@Mapper的註解,也不用寫代碼就能實現對數據庫增刪改查呢,我們這裏就借鑑Mybatis這種思想來對客戶端的接口進行實例化,但注意不是進行實現,實現怎麼搞呢,當然就是用動態代理啦,在動態代理內部發送我們的請求,然後拿到返回。不就是相當於,我們在客戶端,像調用本地方法一樣,調用了服務端的方法麼。廢話不多說,看代碼吧。

  • 包掃描

Spring要去初始化bean,需要掃描接口路徑下的接口,對某一個路徑下的類進行掃描,Spring也提供了一些接口。

public class RpcScanner extends ClassPathBeanDefinitionScanner {

    private RpcFactoryBean<?> rpcFactoryBean = new RpcFactoryBean<>();

    private static final Logger logger = LoggerFactory.getLogger(RpcScanner.class);
    /**
     * 註解類
     * */
    @Setter
    private Class<? extends Annotation> annotationClass;

    public RpcScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    public Set<BeanDefinitionHolder> doScan(String... packages){
        Set<BeanDefinitionHolder> beanDefineHolders = super.doScan(packages);

        if(beanDefineHolders.isEmpty()){
            logger.warn("No proper Rpc mapper found in such paths : {}",Arrays.asList(packages));
        }else{
            postProcessBeanDefinitions(beanDefineHolders);
        }
        return beanDefineHolders;
    }

    /**
     * 註冊過濾器
     * */
    public void registerFilters(){
        boolean acceptAllInterfaces = true;
        //如果事先設置了註解類,那麼就只對這個註解類不設置過濾
        if(this.annotationClass!=null){
            addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
            acceptAllInterfaces = false;
        }
        //沒有設置註解類,那麼默認將basePackage下的類都進行掃描
        if(acceptAllInterfaces){
            addIncludeFilter(new TypeFilter() {
                @Override
                public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                    return true;
                }
            });
        }
        addExcludeFilter(new TypeFilter() {
            @Override
            public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                String className = metadataReader.getClassMetadata().getClassName();
                return className.endsWith("package-info");
            }
        });
    }

    /**
     * 配置自定義BeanDefinition的屬性
     */
    private void postProcessBeanDefinitions(Set<BeanDefinitionHolder> holders){
        GenericBeanDefinition definition;
        for (BeanDefinitionHolder holder : holders){
            definition = (GenericBeanDefinition)holder.getBeanDefinition();
            //添加FactoryBean帶參構造函數的參數值
            definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
            definition.setBeanClass(this.rpcFactoryBean.getClass());
            //設置注入模式
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            logger.info("BeanDefinitionHolder:{}",holder);
        }

    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition definition){
        return definition.getMetadata().isInterface()&&definition.getMetadata().isIndependent();
    }
}

這裏的doScan的方法,就是在掃描某個路徑的時候調用的,在掃描之後,需要對BeanDefinition進行一下設置,設置構造方法傳入的參數,這裏需要在對應的FactoryBean裏添加對應的構造方法,設置BeanClass和注入模式,這裏要講還可以講一大堆,建議看不懂的可以瞭解一下FactoryBean的原理和用法。

public class RpcFactoryBean<T> implements FactoryBean<T> {

    private Class<T> rpcInterface;

    private static final Logger logger = LoggerFactory.getLogger(RpcFactoryBean.class);

    @Autowired
    private RpcFactory<T> factory;

    public RpcFactoryBean(){
    }

    public RpcFactoryBean(Class<T> rpcInterface){
        this.rpcInterface = rpcInterface;
    }

    /**
     * 返回對象實例
     * */
    @Override
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(rpcInterface.getClassLoader(),new Class[]{rpcInterface},factory);
    }

    /**
     * Bean的類型
     * */
    @Override
    public Class<?> getObjectType() {
        return this.rpcInterface;
    }

    /**
     * 是否是單例的
     * */
    @Override
    public boolean isSingleton(){
        return true;
    }
 }

最後就是客戶端調用服務端的本質所在了,瞭解過動態代理的人肯定知道InvocationHandler這個接口。

@Component
public class RpcFactory<T> implements InvocationHandler {

    @Autowired
    private NettyClient client;

    private static final Logger logger = LoggerFactory.getLogger(RpcFactory.class);
    /**
     * 發送請求的地方
     * */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RpcRequest request = new RpcRequest();
        request.setType(RequestTypeEnum.NORMAL);
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParams(args);
        request.setParamTypes(method.getParameterTypes());
        request.setId(IDUtil.getRpcRequestId());

        //向服務端發送請求
        Object result = client.send(request);

        Class<?> returnType = method.getReturnType();

        ObjectMapper mapper = new ObjectMapper();

        RpcResponse response = mapper.readValue(String.valueOf(result),RpcResponse.class);

        if(response.getCode()==RespCodeEnum.ERROR){
            throw new Exception(response.getErrorMsg());
        }
        String respData = mapper.writeValueAsString(response.getData());
        if(returnType.isPrimitive()||String.class.isAssignableFrom(returnType)){
            return respData;
        }else if(Collection.class.isAssignableFrom(returnType)){
            CollectionType collectionType = mapper.getTypeFactory().constructCollectionType(Collection.class,Object.class);
            return mapper.readValue(respData,collectionType);
        }else if (Map.class.isAssignableFrom(returnType)){
            MapType mapType = mapper.getTypeFactory().constructMapType(Map.class,Object.class,Object.class);
            return mapper.readValue(respData,mapType);
        }else{
            Object data = response.getData();
            return mapper.readValue(respData,returnType);
        }
    }
}

這裏應該要對Response的返回值做不同類型的校驗和處理,篇幅有限,我之後再加上吧。另外上面還有一個問題,就是RpcFactoryBean這個類中的@Autowired註解,究竟有沒有作用,這段代碼我是參考了網上的,具體請看:

RPC基本原理以及如何用Netty來實現RPC

後來我經過一番查閱和自己debug,這個是起到了作用的,雖然這個類裏沒有@Component的註解,並且初始化是new出來的,不會交給spring來管理,那麼這個類是從哪裏初始化的呢,答案其實還是加載順序的問題,RpcFactory這個類由於加了@Component註解,所以這個bean在Spring容器中是存在的,而並且是單例的,RpcFactoryBean是在什麼時候加載的呢,是在Controller層中的對UserService進行注入的時候,這個時候RpcFactory已經有實例了,並且由於是單例,那自然用@Autowired獲取到的是同一個實例。

試驗一下看效果,還是以Web請求來觸發RPC請求,請求比較簡單,就是根據一個用戶id獲取用戶信息。(http://localhost:6001/getUserById?id=1)

avatar

RPC客戶端的請求日誌

avatar

RPC服務端的日誌

avatar

總結

雖然這個RPC功能很簡陋,但是實現花了我挺多時間,主要還是在查資料上,因爲Spring涉及到源碼層面和FactoryBean這些沒了解過。但是還好磕磕絆絆也實現了出來。但說到底還是有很多缺陷,比如負載均衡算法還可以擴展幾個,比如限流操作,熔斷操作,還有功能上,這裏我默認服務端和客戶端兩個的包名是在一樣的,但是現實中是可能不一樣的,這樣就會出現找不到實現類的出錯的情況,不過這也讓我有動力去重寫這個demo了,最後我把我的代碼實現傳到了我的GitHub,工作之餘我會盡力去完善的。

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