hadoop RPC框架源码分析

参考: Hadoop RPC框架_thomas0yang的博客-CSDN博客_hadoop rpc

Hadoop RPC分析

SERVER:

LISTENER: 打开客户端的请求,selector OP_ACCEPT,  
      ServerSocketChannel server = (ServerSocketChannel) key.channel();
       Reader reader = getReader();
        Connection c = connectionManager.register(channel);
        key.attach(c);  // so closeCurrentConnection can get the object
        reader.addConnection(c);


   
READER  数组,  维护了一个pendingConnecitons列表,定期的监听pendingConnecitons的 selector OP_READ, 读取输入的请求

           int size = pendingConnections.size();
            for (int i=size; i>0; i--) {
              Connection conn = pendingConnections.take();
              conn.channel.register(readSelector, SelectionKey.OP_READ, conn);
            }
            readSelector.select();
            
            
            processRpcRequest:
            
             rpcRequest = ReflectionUtils.newInstance(rpcRequestClass, conf);
             rpcRequest.readFields(dis);
             
             产生一个CALL
             Call call = new Call(header.getCallId(), header.getRetryCount(),
          rpcRequest, this, ProtoUtil.convert(header.getRpcKind()),
          header.getClientId().toByteArray(), traceScope, callerContext);
          
          加入callQueue
          callQueue.put(call);  
          
   
HANDLER


   final Call call = callQueue.take(); // pop the queue; maybe blocked here
   执行SERVER端的调用,获取call的结果
   value = call(call.rpcKind, call.connection.protocolName, call.rpcRequest, 
                           call.timestamp);
   注: call的实现是有RpcEngine实现的, Hadoop的RcpEngine有两种,WritableRpcEngine, 一个是ProtobufRpcEngine
   setupResponse(buf, call, returnStatus, detailedErr,
                value, errorClass, error); //设置Response,把要返回的数据准备好一个response
   call.sendResponse();
   -->call.connection.sendResponse
      --> responder.doRespond(call);
   
RESPONDER
    先直接写一部分数据,如果一次没写完,在responsder 注册  OP_WRITE
    channel.register(writeSelector, SelectionKey.OP_WRITE, call);
    后面异步线程慢慢写
    doAsyncWrite(key);
    


客户端

CLIENT

    call方法的摘要
       final Call call = createCall(rpcKind, rpcRequest);
       Connection connection = getConnection(remoteId, call, serviceClass,fallbackToSimpleAuth);
          --> connection.setupIOstreams(fallbackToSimpleAuth);
                  setupConnection();
                       this.socket = socketFactory.createSocket();
                       SocketChannel ch = socket.getChannel();
                       int ret = selector.select((SelectableChannel)channel, 
                                  SelectionKey.OP_CONNECT, timeoutLeft);
                  InputStream inStream = NetUtils.getInputStream(socket);
                  OutputStream outStream = NetUtils.getOutputStream(socket);
                  writeConnectionHeader(outStream);
                       
      
      connection.sendRpcRequest(call);    
          //准备写入的数据,buffer
          final DataOutputBuffer d = new DataOutputBuffer();
          RpcRequestHeaderProto header = ProtoUtil.makeRpcRequestHeader(
          call.rpcKind, OperationProto.RPC_FINAL_PACKET, call.id, call.retry,
          clientId);
          header.writeDelimitedTo(d);
          call.rpcRequest.write(d); 
          异步并发的写入 connection的IOSTREAM,这里用并发提高处理请求的速度
          Future<?> senderFuture = sendParamsExecutor.submit(new Runnable(){....})
                byte[] data = d.getData();
                int totalLength = d.getLength();
                out.writeInt(totalLength); // Total Length
                out.write(data, 0, totalLength);// RpcRequestHeader + RpcRequest
                out.flush();
         senderFuture.get(); 等待写入完成,
         
         
      等待返回结果
      synchronized (call) {
      while (!call.done) {
        try {
          call.wait();                           // wait for the result
        } catch (InterruptedException ie) {
          Thread.currentThread().interrupt();
          throw new InterruptedIOException("Call interrupted");
        }
      }
      没有报错就返回:
      return call.getRpcResponse();
         
         
          
        返回的response如何处理呢? 
        
        由Connection 异步的处理,一个线程,不停的监听
        
        private class Connection extends Thread   
        
        Connection.run
                    try {
                    while (waitForWork()) {//wait here for work - read or close connection
                      receiveRpcResponse();
                    }
              
              waitForWork 解读:
                 如果conneciton.calls 队列为空,并且等待超过timeout的时间,就会被关闭掉,return false
                 如果connection需要关闭,就会被关闭掉,return false
                 只有当calls不为空,并且shouldconnectionclose 标记为false,并且是running, 就继续 true
              
              receiveRpcResponse的解读:
              
                  int totalLen = in.readInt(); //这一个调用会把线程阻塞,直到有数据返回, 所以每一个conneciton只能同时处理一个call.
                  
                  当返回的状态为SUCCESS之后,
                  就
                  Writable value = ReflectionUtils.newInstance(valueClass, conf);
                          value.readFields(in);                 // read value
                          calls.remove(callId);
                          call.setRpcResponse(value);
                      callComplete();
                          this.done = true;
                                              notify(); // 这里处理完call, 就会notfiy wait的call.
                                              
                                             


根据ClientRMService 分析一下具体Protocal的实现原理

ClientRMService implemeng ApplicationClientProtocal

在serviceStart方法里会启动一个server
    YarnRPC rpc = YarnRPC.create(conf);
    this.server =   
      rpc.getServer(ApplicationClientProtocol.class, this,
            clientBindAddress,
            conf, this.rmDTSecretManager,
            conf.getInt(YarnConfiguration.RM_CLIENT_THREAD_COUNT, 
                YarnConfiguration.DEFAULT_RM_CLIENT_THREAD_COUNT));
                                              
    
    YarnRPC的实现类:org.apache.hadoop.yarn.ipc.HadoopYarnProtoRPC
    HadoopYarnProtoRPC.getServer
        RpcFactoryProvider.getServerFactory(conf).getServer(protocol, 
            instance, addr, conf, secretManager, numHandlers, portRangeConfig);
        
          org.apache.hadoop.yarn.factories.impl.pb.RpcServerFactoryPBImpl.gerServer
            获取
              Constructor<?> constructor = serviceCache.get(protocol);
              pbServiceImplClazz = localConf
            .getClassByName(getPbServiceImplClassName(protocol)); 
                    String srcPackagePart = getPackageName(clazz);
                                    String srcClassName = getClassName(clazz);
                                    String destPackagePart = srcPackagePart + "." + impl.pb.service; 
                                    String destClassPart = srcClassName + PB_IMPL_CLASS_SUFFIX;
                                    return destPackagePart + "." + PBServiceImpl;
                
                 注:org.apache.hadoop.yarn.api.impl.pb.service.ApplicationClientProtocolPBServiceImpl
                 这个类是一个包装类,并没有直接实现  ApplicationClientProtocal 接口,而是包装了一个 实现了ApplicationClientProtocal接口的 ClientRMService, 所有服务转发给clientRMService处理的
                 这个类是RPC框架中的一个连接器类,连接了RPCServer接受到请求之后,转发给真实实现了协议接口的服务处理。
                 这个类有固定的一些包名字和类名字,后面在RPC框架里找到协议实现的时候根据命名规范找到具体的协议连接器类
                 
         这个类实现的是 ApplicationClientProtocolPB 接口,
         public class ApplicationClientProtocolPBServiceImpl implements ApplicationClientProtocolPB {
         ApplicationClientProtocolPB 接口实现了protobuf生成的接口
         public interface ApplicationClientProtocolPB extends ApplicationClientProtocolService.BlockingInterface {
         ApplicationClientProtocolPBServiceImpl 这个类就是protobuf 接口和 ClientRMService之间的连接器
         
         constructor = pbServiceImplClazz.getConstructor(protocol);
         constructor.setAccessible(true);
         serviceCache.putIfAbsent(protocol, constructor);      
            
      
        这里调用ApplicationClientProtocolPBServiceImpl 的构造器,创建出这里调用ApplicationClientProtocolPBServiceImpl 对象,把 ClientRMService 的一个实例作为参数传入
            Object service = null;
            try {
              service = constructor.newInstance(instance);
            } 
            
            获取服务实现的接口,它实现了ApplicationClientProtocolPB 接口
            Class<?> pbProtocol = service.getClass().getInterfaces()[0];
            Method method = protoCache.get(protocol);
            
            if (method == null) {
          Class<?> protoClazz = null;
          try {
            protoClazz = localConf.getClassByName(getProtoClassName(protocol));
                                org.apache.hadoop.yarn.proto.ApplicationClientProtocol$ApplicationClientProtocolService
                                由下面的proto文件生成
                                hadoop-yarn-project/hadoop-yarn/hadoop-yarn-api/src/main/proto/applicationclient_protocol.proto 
                                在proto文件里定义了包路径以及生成的class的名称
                                option java_package = "org.apache.hadoop.yarn.proto";
                                        option java_outer_classname = "ApplicationClientProtocol";
          } catch (ClassNotFoundException e) {
            throw new YarnRuntimeException("Failed to load class: ["
                + getProtoClassName(protocol) + "]", e);
          }
          
          //在这个类里 org.apache.hadoop.yarn.proto.ApplicationClientProtocol$ApplicationClientProtocolService
          //会有一个方法: newReflectiveBlockingService
          try {
            method = protoClazz.getMethod("newReflectiveBlockingService",
                pbProtocol.getInterfaces()[0]);
            method.setAccessible(true);
            protoCache.putIfAbsent(protocol, method);
          } catch (NoSuchMethodException e) {
            throw new YarnRuntimeException(e);
          }
         }
         
         //newReflectiveBlockingService 给的一个参数就是是实现了协议的服务,里面会 根据方法的index,调用方法
         //ProtobufRpcEngine 里的Server的call方法代理到blockingService 里的方法调用
         
         return createServer(pbProtocol, addr, conf, secretManager, numHandlers,
          (BlockingService)method.invoke(null, service), portRangeConfig);
          //设置engine为 ProtobufRpcEngine
          RPC.setProtocolEngine(conf, pbProtocol, ProtobufRpcEngine.class); 
                RPC.Server server = new RPC.Builder(conf).setProtocol(pbProtocol)
                    .setInstance(blockingService).setBindAddress(addr.getHostName())
                    .setPort(addr.getPort()).setNumHandlers(numHandlers).setVerbose(false)
                    .setSecretManager(secretManager).setPortRangeConfig(portRangeConfig)
                    .build();
                LOG.info("Adding protocol "+pbProtocol.getCanonicalName()+" to the server");
                server.addProtocol(RPC.RpcKind.RPC_PROTOCOL_BUFFER, pbProtocol, blockingService);
                return server;
         
         
         RPC.Server的实现
          @Override
            public Writable call(RPC.RpcKind rpcKind, String protocol,
                Writable rpcRequest, long receiveTime) throws Exception {
              return getRpcInvoker(rpcKind).call(this, protocol, rpcRequest,
                  receiveTime);
            }
            
            public static RpcInvoker  getRpcInvoker(RPC.RpcKind rpcKind) {
                RpcKindMapValue val = rpcKindMap.get(rpcKind);
                return (val == null) ? null : val.rpcInvoker; 
              }
         
         //在registerProtocalEngine的时候,就把rpcKindMap 里的rpcInvoker设置了
         public static void registerProtocolEngine(RPC.RpcKind rpcKind, 
              Class<? extends Writable> rpcRequestWrapperClass,
              RpcInvoker rpcInvoker) {
            RpcKindMapValue  old = 
                rpcKindMap.put(rpcKind, new RpcKindMapValue(rpcRequestWrapperClass, rpcInvoker));
            if (old != null) {
              rpcKindMap.put(rpcKind, old);
              throw new IllegalArgumentException("ReRegistration of rpcKind: " +
                  rpcKind);      
            }
            LOG.debug("rpcKind=" + rpcKind + 
                ", rpcRequestWrapperClass=" + rpcRequestWrapperClass + 
                ", rpcInvoker=" + rpcInvoker);
          }
          
          //在类ProtobufRpcEngine 初始化的时候,调用了registerProtocolEngine, 注册好了
          public class ProtobufRpcEngine implements RpcEngine {
              public static final Log LOG = LogFactory.getLog(ProtobufRpcEngine.class);
              
              static { // Register the rpcRequest deserializer for WritableRpcEngine 
                org.apache.hadoop.ipc.Server.registerProtocolEngine(
                    RPC.RpcKind.RPC_PROTOCOL_BUFFER, RpcRequestWrapper.class,
                    new Server.ProtoBufRpcInvoker());
              }
              
            ProtoBufRpcInvoker 是ProtobufRpcEngine 的内部类,实现了RpcInvoker
          static class ProtoBufRpcInvoker implements RpcInvoker
        
        最终服务端的call就是通过该类实现的,分析一下
        
        public Writable call(RPC.Server server, String protocol,
          Writable writableRequest, long receiveTime) throws Exception {
         
        //RpcRequestWrapper 是在registerProtocolEngine 的时候注册的
        注:这里复习一下Server.Reader 线程异步读取Client发送过来的数据:
         **************************************************************************************
          一下代码引用自Reader里的processRpcRequest 方法里的片段,这里的rpcRequestClass 就是
          Class<? extends Writable> rpcRequestClass = getRpcRequestWrapper(header.getRpcKind());
                          if (rpcRequestClass != null)
                                   return rpcRequestClass;
                                RpcKindMapValue val = rpcKindMap.get(ProtoUtil.convert(rpcKind));
          
          Writable rpcRequest;
              try { //Read the rpc request
                rpcRequest = ReflectionUtils.newInstance(rpcRequestClass, conf);
                rpcRequest.readFields(dis);
              }
          ************************************************************************************
          
          
        RpcRequestWrapper request = (RpcRequestWrapper) writableRequest;
        RequestHeaderProto rpcRequest = request.requestHeader;
        String methodName = rpcRequest.getMethodName();
        String protoName = rpcRequest.getDeclaringClassProtocolName();
        long clientVersion = rpcRequest.getClientProtocolVersion();
        if (server.verbose)
          LOG.info("Call: protocol=" + protocol + ", method=" + methodName);
        
        ************************************************************************************
        创建ProtobufRpcEngine.Server的时候,注册RpcKind和 协议的类以及实现
        registerProtocolAndImpl(RPC.RpcKind.RPC_PROTOCOL_BUFFER, protocolClass,
          protocolImpl);
                
                String protocolName = RPC.getProtocolName(protocolClass);
                          ProtocolInfo anno = protocol.getAnnotation(ProtocolInfo.class);
                                                return  (anno == null) ? protocol.getName() : anno.protocolName();
                           version = RPC.getProtocolVersion(protocolClass);
                                               ProtocolInfo anno = protocol.getAnnotation(ProtocolInfo.class);
                                                if (anno != null) {
                                                  version = anno.protocolVersion();
                                                  if (version != -1)
                                                    return version;
                                                }
                                                try {
                                                  Field versionField = protocol.getField("versionID");
                                                  versionField.setAccessible(true);
                                                  return versionField.getLong(protocol);
                                                }

                      getProtocolImplMap(rpcKind).put(new ProtoNameVer(protocolName, version), new ProtoClassProtoImpl(protocolClass, protocolImpl)); 
        
        ************************************************************************************

        
        ProtoClassProtoImpl protocolImpl = getProtocolImpl(server, protoName,
            clientVersion);
            
        //protocolImpl就是创建Server的时候给的 连接器类  ApplicationClientProtocolPBServiceImpl
        BlockingService service = (BlockingService) protocolImpl.protocolImpl;
        MethodDescriptor methodDescriptor = service.getDescriptorForType()
            .findMethodByName(methodName);
        if (methodDescriptor == null) {
          String msg = "Unknown method " + methodName + " called on " + protocol
              + " protocol.";
          LOG.warn(msg);
          throw new RpcNoSuchMethodException(msg);
        }
        Message prototype = service.getRequestPrototype(methodDescriptor);
        Message param = prototype.newBuilderForType()
            .mergeFrom(request.theRequestRead).build();
        
        Message result;
        long startTime = Time.now();
        int qTime = (int) (startTime - receiveTime);
        Exception exception = null;
        try {
          server.rpcDetailedMetrics.init(protocolImpl.protocolClass);
          result = service.callBlockingMethod(methodDescriptor, null, param);
        } catch (ServiceException e) {
          exception = (Exception) e.getCause();
          throw (Exception) e.getCause();
        } catch (Exception e) {
          exception = e;
          throw e;
        } finally {
          int processingTime = (int) (Time.now() - startTime);
          if (LOG.isDebugEnabled()) {
            String msg = "Served: " + methodName + " queueTime= " + qTime +
                " procesingTime= " + processingTime;
            if (exception != null) {
              msg += " exception= " + exception.getClass().getSimpleName();
            }
            LOG.debug(msg);
          }
          String detailedMetricsName = (exception == null) ?
              methodName :
              exception.getClass().getSimpleName();
          server.rpcMetrics.addRpcQueueTime(qTime);
          server.rpcMetrics.addRpcProcessingTime(processingTime);
          server.rpcDetailedMetrics.addProcessingTime(detailedMetricsName,
              processingTime);
          if (server.isLogSlowRPC()) {
            server.logSlowRpcCalls(methodName, processingTime);
          }
        }
        return new RpcResponseWrapper(result);
      }
        
         
      现在来看一下客户端调用的实现
      
      YarnClient.createYarnClient();
      YarnClientImpl.rmClient 是一个实现了ApplicationClientProtocol 接口的客户端代理
      YarnClientImpl.serviceStart
          rmClient = ClientRMProxy.createRMProxy(getConfig(),ApplicationClientProtocol.class);
                  T proxy = RMProxy.<T>getProxy(conf, protocol, rmAddress);
                       YarnRPC.create(conf).getProxy(protocol, rmAddress, conf);
                       org.apache.hadoop.yarn.ipc.HadoopYarnProtoRPC.getProxy()
                               RpcFactoryProvider.getClientFactory(conf).getClient(protocol, 1,addr, conf);
                               org.apache.hadoop.yarn.factories.impl.pb.RpcClientFactoryPBImpl.getClient(protocol, 1,addr, conf); //和上面的获取SERVER的 对应org.apache.hadoop.yarn.factories.impl.pb.RpcServerFactoryPBImpl.gerServer
                                                   org.apache.hadoop.yarn.api.impl.pb.client.ApplicationClientProtocolPBClientImpl
                               //对应Server的实现:org.apache.hadoop.yarn.api.impl.pb.service.ApplicationClientProtocolPBServiceImpl
                               
                               Class<?> pbClazz = null;
                              try {
                                pbClazz = localConf.getClassByName(getPBImplClassName(protocol));
                                //org.apache.hadoop.yarn.api.impl.pb.client.ApplicationClientProtocolPBClientImpl
                              } catch (ClassNotFoundException e) {
                                throw new YarnRuntimeException("Failed to load class: ["
                                    + getPBImplClassName(protocol) + "]", e);
                              }
                              try {
                                constructor = pbClazz.getConstructor(Long.TYPE, InetSocketAddress.class, Configuration.class);
                                
                                                        public ApplicationClientProtocolPBClientImpl(long clientVersion,
                                                                  InetSocketAddress addr, Configuration conf) throws IOException {
                                                                RPC.setProtocolEngine(conf, ApplicationClientProtocolPB.class,
                                                                  ProtobufRpcEngine.class);
                                                                proxy = RPC.getProxy(ApplicationClientProtocolPB.class, clientVersion, addr, conf);
                                                              }
                                                              
                                                              RPC.getProxy()
                                                                      RPC.getProtocolProxy(protocol, clientVersion, addr, conf).getProxy();
                                                                      
                                                                      ProtobufRpcEngine.getProxy()
                                                                      @Override
                                                                      public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
                                                                          InetSocketAddress addr, UserGroupInformation ticket, Configuration conf,
                                                                          SocketFactory factory, int rpcTimeout, RetryPolicy connectionRetryPolicy
                                                                          ) throws IOException {
                                                                        return getProxy(protocol, clientVersion, addr, ticket, conf, factory,
                                                                          rpcTimeout, connectionRetryPolicy, null);
                                                                      }
                                                                      
                                                                      ProtobufRpcEngine.getProxy(...)
                                                                              final Invoker invoker = new Invoker(protocol, addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy, fallbackToSimpleAuth);
                                                                          new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(protocol.getClassLoader(), new Class[]{protocol}, invoker), false);
                                                                  
                                                                  最终是由Java的代理类来实现的,最终实现调用是由Invoker处理的
                                                                  
                                
                                
                                constructor.setAccessible(true);
                                cache.putIfAbsent(protocol, constructor);
                              } catch (NoSuchMethodException e) {
                                throw new YarnRuntimeException("Could not find constructor with params: " + Long.TYPE + ", " + InetSocketAddress.class + ", " + Configuration.class, e);
                              }
                            }
                            try {
                              Object retObject = constructor.newInstance(clientVersion, addr, conf);
                              return retObject;
                            } catch (InvocationTargetException e) {
                              throw new YarnRuntimeException(e);
                            } catch (IllegalAccessException e) {
                              throw new YarnRuntimeException(e);
                            } catch (InstantiationException e) {
                              throw new YarnRuntimeException(e);
                            }
                               
                                       
                                       
                           ProtobufRpcEngine.Invoker.invoke方法:
                                       
                        @Override
                            public Object invoke(Object proxy, Method method, Object[] args)
                                throws ServiceException {
                              long startTime = 0;
                              if (LOG.isDebugEnabled()) {
                                startTime = Time.now();
                              }
                              
                              if (args.length != 2) { // RpcController + Message
                                throw new ServiceException("Too many parameters for request. Method: ["
                                    + method.getName() + "]" + ", Expected: 2, Actual: "
                                    + args.length);
                              }
                              if (args[1] == null) {
                                throw new ServiceException("null param while calling Method: ["
                                    + method.getName() + "]");
                              }
                        
                              // if Tracing is on then start a new span for this rpc.
                              // guard it in the if statement to make sure there isn't
                              // any extra string manipulation.
                              Tracer tracer = Tracer.curThreadTracer();
                              TraceScope traceScope = null;
                              if (tracer != null) {
                                traceScope = tracer.newScope(RpcClientUtil.methodToTraceString(method));
                              }
                        
                              RequestHeaderProto rpcRequestHeader = constructRpcRequestHeader(method);
                              
                              if (LOG.isTraceEnabled()) {
                                LOG.trace(Thread.currentThread().getId() + ": Call -> " +
                                    remoteId + ": " + method.getName() +
                                    " {" + TextFormat.shortDebugString((Message) args[1]) + "}");
                              }
                        
                        
                              Message theRequest = (Message) args[1];
                              final RpcResponseWrapper val;
                              try {
                              //最终调用Client的call方法调用远程的服务,Client在上面已经分析过了,到此hadoop RPC分析完毕
                                val = (RpcResponseWrapper) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,
                                    new RpcRequestWrapper(rpcRequestHeader, theRequest), remoteId,
                                    fallbackToSimpleAuth);
                        
                              } catch (Throwable e) {
                                if (LOG.isTraceEnabled()) {
                                  LOG.trace(Thread.currentThread().getId() + ": Exception <- " +
                                      remoteId + ": " + method.getName() +
                                        " {" + e + "}");
                                }
                                if (traceScope != null) {
                                  traceScope.addTimelineAnnotation("Call got exception: " +
                                      e.getMessage());
                                }
                                throw new ServiceException(e);
                              } finally {
                                if (traceScope != null) traceScope.close();
                              }
                        
                              if (LOG.isDebugEnabled()) {
                                long callTime = Time.now() - startTime;
                                LOG.debug("Call: " + method.getName() + " took " + callTime + "ms");
                              }
                              
                              Message prototype = null;
                              try {
                                prototype = getReturnProtoType(method);
                              } catch (Exception e) {
                                throw new ServiceException(e);
                              }
                              Message returnMessage;
                              try {
                                returnMessage = prototype.newBuilderForType()
                                    .mergeFrom(val.theResponseRead).build();
                        
                                if (LOG.isTraceEnabled()) {
                                  LOG.trace(Thread.currentThread().getId() + ": Response <- " +
                                      remoteId + ": " + method.getName() +
                                        " {" + TextFormat.shortDebugString(returnMessage) + "}");
                                }
                        
                              } catch (Throwable e) {
                                throw new ServiceException(e);
                              }
                              return returnMessage;
                            }
                                                  
          
            
       
       
            
            
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值