How tomcat works——13 Host 和 Engine

概述

本章要讨论的2个主题是主机(host)和引擎(Engine)。如果需要在一个 Tomcat部署中部署多个上下文,需要使用一个主机。理论上,当只有一个上下文容器时不需要主机,正如下面 org.apache.catalina.Context 接口中描述:

上下文容器的父容器通常是主机,但是可能有一些其它实现,没有必要的时候也可以忽略

然而在实践中,一个 Tomcat 部署往往需要一个主机。至于为什么,将会在本章后面的“为什么需要Host” 一节中看到。

Engine表示整个 Catalina 的 Servlet 引擎。如果使用的话,它通常位于容器等级的最高层。可以添加到Engine上的子容器通常实现于org.apache.catalina.Host 或org.apache.catalina.Context。在一个 Tomcat 部署中,默认是使用引擎容器的。在该部署中,Engine有一个Host,默认主机。

本章讨论了跟 Host 和 Engine 接口相关的类。首先介绍了 Host 相关的StandardHost、StandardHostMapper(Tomcat4)以及 StandardHostValve 类。接下来是用一个Demo来示范演示了Host作为顶层容器的情况。Engine是本章讨论的第二个主题,介绍了StandardEngine和StandardEngineValve类。接下来是本章的第2个应用Demo,说明了如何将Engine作为顶层容器使用。

13.1 Host接口

主机是用 org.apache.catalina.Host 接口表示的。本接口继承了 Container 接口,如 Listing13.1 所示:

Listing 13.1: The Host interface

package org.apache.catalina;

public interface Host extends Container {


    // ----------------------------------------------------- Manifest Constants


    /**
     * The ContainerEvent event type sent when a new alias is added
     * by <code>addAlias()</code>.
     */
    public static final String ADD_ALIAS_EVENT = "addAlias";


    /**
     * The ContainerEvent event type sent when an old alias is removed
     * by <code>removeAlias()</code>.
     */
    public static final String REMOVE_ALIAS_EVENT = "removeAlias";


    // ------------------------------------------------------------- Properties


    /**
     * Return the application root for this Host.  This can be an absolute
     * pathname, a relative pathname, or a URL.
     */
    public String getAppBase();


    /**
     * Set the application root for this Host.  This can be an absolute
     * pathname, a relative pathname, or a URL.
     *
     * @param appBase The new application root
     */
    public void setAppBase(String appBase);


    /**
     * Return the value of the auto deploy flag.  If true, it indicates that 
     * this host's child webapps should be discovred and automatically 
     * deployed.
     */
    public boolean getAutoDeploy();


    /**
     * Set the auto deploy flag value for this host.
     * 
     * @param autoDeploy The new auto deploy flag
     */
    public void setAutoDeploy(boolean autoDeploy);


    /**
     * Set the DefaultContext
     * for new web applications.
     *
     * @param defaultContext The new DefaultContext
     */
    public void addDefaultContext(DefaultContext defaultContext);


    /**
     * Retrieve the DefaultContext for new web applications.
     */
    public DefaultContext getDefaultContext();


    /**
     * Return the canonical, fully qualified, name of the virtual host
     * this Container represents.
     */
    public String getName();


    /**
     * Set the canonical, fully qualified, name of the virtual host
     * this Container represents.
     *
     * @param name Virtual host name
     *
     * @exception IllegalArgumentException if name is null
     */
    public void setName(String name);


    // --------------------------------------------------------- Public Methods


    /**
     * Import the DefaultContext config into a web application context.
     *
     * @param context web application context to import default context
     */
    public void importDefaultContext(Context context);


    /**
     * Add an alias name that should be mapped to this same Host.
     *
     * @param alias The alias to be added
     */
    public void addAlias(String alias);


    /**
     * Return the set of alias names for this Host.  If none are defined,
     * a zero length array is returned.
     */
    public String[] findAliases();


    /**
     * Return the Context that would be used to process the specified
     * host-relative request URI, if any; otherwise return <code>null</code>.
     *
     * @param uri Request URI to be mapped
     */
    public Context map(String uri);


    /**
     * Remove the specified alias name from the aliases for this Host.
     *
     * @param alias Alias name to be removed
     */
    public void removeAlias(String alias);


}

特别重要的是map ()方法,它返回合适的上下文来处理请求,该方法的实现可以在StandardHost 类中找到,将在下一小节中讨论。

13.2 StandardHost类

org.apache.catalina.core.StandardHost 类是对 Host 接口的标准实现。该类继承了 org.apache.catalina.core.ContainerBase 类并实现了 Host和Deployer 接口。Deployer 接口将在第 17 章讨论。

跟 StandardContext 和 StandardWrapper 类相似,StandardHost 类的构造函数在它的管道中添加一基础阀门:

public StandardHost() {
    super();
    pipeline.setBasic(new StandardHostValve());
}

如你所见,该基础阀门类型为 org.apache.catalina.core.StandardHostValve。

当启动时,如 start()方法被调用时,StandardHost 上面添加2个阀门:ErrorReportValve 和 ErrorDispatcherValve。它们都在org.apache.catalina.valves 包中。Tomcat4 中 StandardHost 的 start()方法如Listing13.2 所示:

Listing 13.2: The start method of StandardHost

public synchronized void start() throws LifecycleException {
    // Set error report valve
    if ((errorReportValveClass != null)&& (!errorReportValveClass.equals(""))) {
        try {
            Valve valve =(Valve) Class.forName(errorReportValveClass).newInstance();
            addValve(valve);
        }catch (Throwable t) {
            log(sm.getString("StandardHost.invalidErrorReportValveClass",
                errorReportValveClass));
        }
    }
    // Set dispatcher valve
    addValve(new ErrorDispatcherValve());
    super.start();
}

注意:在 Tomcat5 中,start()方法相似,不同点在于包括了构建 JMX 对象的代码,JMX 将在第 20 章讨论。

errorReportValveClass 的值定义如下:

private String errorReportValveClass ="org.apache.catalina.valves.ErrorReportValve";

对于每一个请求,都会调用主机的 invoke()方法。由于 StanardHost 类并没有实现invoke()方法,所以会调用它的父类ContainerBase类的invoke()方法。该invoke()方法会转而调用 StandardHost 的基础阀门 StandardHostValve 的 invoke() 方法。StandardHostValve 阀门的 invoke()方法将在 “StandardHostValve类”小节中讨论。特别是StandardHostValve 的 invoke()方法调用 StandardHost 类的 map()方法来获得一个合适的上下文容器进行请求处理。StandardHost 的 map()方法如 Listing13.3 所示:

Listing 13.3: The map method in the StandardHost class

public Context map(String uri) {

        if (debug > 0)
            log("Mapping request URI '" + uri + "'");
        if (uri == null)
            return (null);

        // Match on the longest possible context path prefix
        if (debug > 1)
            log("  Trying the longest context path prefix");
        Context context = null;
        String mapuri = uri;
        while (true) {
            context = (Context) findChild(mapuri);
            if (context != null)
                break;
            int slash = mapuri.lastIndexOf('/');
            if (slash < 0)
                break;
            mapuri = mapuri.substring(0, slash);
        }

        // If no Context matches, select the default Context
        if (context == null) {
            if (debug > 1)
                log("  Trying the default context");
            context = (Context) findChild("");
        }

        // Complain if no Context has been selected
        if (context == null) {
            log(sm.getString("standardHost.mappingError", uri));
            return (null);
        }

        // Return the mapped Context (if any)
        if (debug > 0)
            log(" Mapped to context '" + context.getPath() + "'");
        return (context);

    }

注意在 Tomcat4 中,ContainerBase 类也声明了一个 map()方法如下签名:

public Container map(Request request, boolean update);

在 Tomcat4 中,StandardHostVavle 的 invoke()方法调用 ContainerBase 的 map()方法,它转而调用 StandardHost 的 map()方法。在 Tomcat5 中,没有映射器组件,适当的上下文将从请求对象中获得。

13.3 StandardHostMapper类

在 Tomcat4 中,StandardHost 的父类 ContainerBase 在start()方法中调用 addDefaultMapper()方法创建一个默认映射器。默认映射器的类型由 mapperClass 属性指定。如下是ContainerBase 的 addDefaulstMapper()方法:

protected void addDefaultMapper(String mapperClass) {
    // Do we need a default Mapper?
    if (mapperClass == null)
        return;
    if (mappers.size() >= 1)
        return;
    // Instantiate and add a default Mapper
    try {
        Class clazz = Class.forName(mapperClass);
        Mapper mapper = (Mapper) clazz.newInstance();
        mapper.setProtocol("http");
        addMapper(mapper);
    }catch (Exception e) {
        log(sm.getString("containerBase.addDefaultMapper", mapperClass),e);
    }
}

StandardHost 定义 mapperClass 变量如下:

private String mapperClass = "org.apache.catalina.core.StandardHostMapper";

另外,StandardHost 类的 start()方法在它的最后调用super.start()来保证创建一个默认的映射器。

注意:Tomcat4 中的 standardContext 使用了略微不同的方法来创建一个默认映射器。它的 start() 方法中并没有调用 super.start()。相反 StandardContext 的start()方法调用 addDefaultMapper()来传递 mapperClass 变量。

StandardHostMapper 中最重要的是 map()方法,下面是它的实现:

public Container map(Request request, boolean update) {
    // Has this request already been mapped?
    if (update && (request.getContext() != null))
        return (request.getContext());
    // Perform mapping on our request URI
    String uri = ((HttpRequest) request).getDecodedRequestURI();
    Context context = host.map(uri);
    // Update the request (if requested) and return the selected Context
    if (update) {
        request.setContext(context);
    if (context != null)
        ((HttpRequest) request).setContextPath(context.getPath());
    else
        ((HttpRequest) request).setContextPath(null);
    }
    return (context);
}

注意,map() 方法仅仅是简单地调用了 Host 的 map()方法。

13.4 StandardHostValve类

org.apache.catalina.core.StandardHostValve 类是 StandardHost 的基础阀门。当有 HTTP 请求时会调用它的 invoke()方法,代码如Listing 13.4:

Listing 13.4: The invoke method of StandardHostValve

public void invoke(Request request, Response response,
                       ValveContext valveContext)
        throws IOException, ServletException {

        // Validate the request and response object types
        if (!(request.getRequest() instanceof HttpServletRequest) ||
            !(response.getResponse() instanceof HttpServletResponse)) {
            return;     // NOTE - Not much else we can do generically
        }

        // Select the Context to be used for this Request
        StandardHost host = (StandardHost) getContainer();
        Context context = (Context) host.map(request, true);
        if (context == null) {
            ((HttpServletResponse) response.getResponse()).sendError
                (HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                 sm.getString("standardHost.noContext"));
            return;
        }

        // Bind the context CL to the current thread
        Thread.currentThread().setContextClassLoader
            (context.getLoader().getClassLoader());

        // Update the session last access time for our session (if any)
        HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
        String sessionId = hreq.getRequestedSessionId();
        if (sessionId != null) {
            Manager manager = context.getManager();
            if (manager != null) {
                Session session = manager.findSession(sessionId);
                if ((session != null) && session.isValid())
                    session.access();
            }
        }

        // Ask this Context to process this request
        context.invoke(request, response);

    }

在 Tomcat4 中invoke()方法中调用 StandardHost 的map()方法来获得一个合适的上下文:

// Select the Context to be used for this Request
StandardHost host = (StandardHost) getContainer();
Context context = (Context) host.map(request, true);

注意:在得到上下对象的时候需要一个往返过程。map() 方法接收2个参数,该方法是在 ContainerBase 中的。然后 ContainerBase 类又在它的子对象(这里是StandardHost对象)中查找合适的映射器并调用它的 map()方法。

invoke()方法解析得到一个 Session 对象并调用它的 access()方法更新它的最后进入访问时间。如下是org.apache.catalina.session.StandardSession 类中的 access() 方法实现:

public void access() {
    this.isNew = false;
    this.lastAccessedTime = this.thisAccessedTime;
    this.thisAccessedTime = System.currentTimeMillis();
}

最后,invoke()方法调用上下文容器的 invoke()方法,让上下文来处理请求。

13.5为什么需要Host

一Tomcat 部署必须有一个Host,如果该Context使用 ContextConfig 来配置。原因如下:

ContextConfig 需要应用文件 web.xml 的位置,它在applicationConfig()方法中尝试打开该文件,下面是该方法的片段:

synchronized (webDigester) {
    try {
        URL url =
        servletContext.getResource(Constants.ApplicationWebXml);
        InputSource is = new InputSource(url.toExternalForm());
        is.setByteStream(stream);
        ...
        webDigester.parse(is);
        ...

Constants.ApplicationWebXml 定义的是 /WEB-INF/web.xml文件的相对地址,servletContext是一个 org.apache.catalina.core.ApplicationContext 类型的对象(它实现了javax.servlet.ServletContext)。

下面是 ApplicationContext 类中的 getResource()方法:

public URL getResource(String path)throws MalformedURLException {
    DirContext resources = context.getResources();
    if (resources != null) {
        String fullPath = context.getName() + path;
        // this is the problem. Host must not be null
        String hostName = context.getParent().getName();

最后一行清楚表明context的父容器(host)是必须的,如果使用 ContextConfig来配置的话。在第 15 章中将会介绍如何解析 web.xml 文件。简单的说,除非自己写 ContextConfig 类,否则你必须有一个主机。

13.6 应用Demo1

本章第一个应用Demo演示了如何将一个主机作为顶层容器使用。该程序有2个类组成:ex13.pyrmont.core.SimpleContextConfig 和ex13.pyrmont.startup.Bootstrap1 类。SimpleContextConfig 类跟第 11 章中的相同,Boostrap2 类如 Listing13.5 所示:

Listing 13.5: The Bootstrap1 Class

package ex13.pyrmont.startup;

//explain Host
import ex13.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappLoader;


public final class Bootstrap1 {
  public static void main(String[] args) {
    //invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern
    System.setProperty("catalina.base", System.getProperty("user.dir"));
    Connector connector = new HttpConnector();

    Wrapper wrapper1 = new StandardWrapper();
    wrapper1.setName("Primitive");
    wrapper1.setServletClass("PrimitiveServlet");
    Wrapper wrapper2 = new StandardWrapper();
    wrapper2.setName("Modern");
    wrapper2.setServletClass("ModernServlet");

    Context context = new StandardContext();
    // StandardContext's start method adds a default mapper
    context.setPath("/app1");
    context.setDocBase("app1");

    context.addChild(wrapper1);
    context.addChild(wrapper2);

    LifecycleListener listener = new SimpleContextConfig();
    ((Lifecycle) context).addLifecycleListener(listener);

    Host host = new StandardHost();
    host.addChild(context);
    host.setName("localhost");
    host.setAppBase("webapps");

    Loader loader = new WebappLoader();
    context.setLoader(loader);
    // context.addServletMapping(pattern, name);
    context.addServletMapping("/Primitive", "Primitive");
    context.addServletMapping("/Modern", "Modern");

    connector.setContainer(host);
    try {
      connector.initialize();
      ((Lifecycle) connector).start();
      ((Lifecycle) host).start();

      // make the application wait until we press a key.
      System.in.read();
      ((Lifecycle) host).stop();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

13.6.1 运行Demo

在 Windows 下面可以在工作目录下输入如下命令运行该程序:

java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./lib/commons—digester.jar;./
ex13.pyrmont.startup.Bootstrap1

在 Linux 下面需要使用分号来分隔开两个库

java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./lib/commons-digester.jar:./
ex13.pyrmont.startup.Bootstrap1

使用如下 URL 可以调用 PrimitiveServlet和ModernServlet:

http://localhost:8080/app1/Primitive
http://localhost:8080/app1/Modern

13.7 Engine接口

org.apache.catalina.Engine 接口用来表示一个引擎。Engine表示整个 Catalina的 Servlet 引擎。当我们想要支持多个虚拟主机时,需要一个引擎,实际上Tomcat 部署正是使用了引擎。Engine 接口如 Listing 13.6 所示:

Listing 13.6: The Engine Interface

public interface Engine extends Container {


    // ------------------------------------------------------------- Properties


    /**
     * Return the default hostname for this Engine.
     */
    public String getDefaultHost();


    /**
     * Set the default hostname for this Engine.
     *
     * @param defaultHost The new default host
     */
    public void setDefaultHost(String defaultHost);


    /**
     * Retrieve the JvmRouteId for this engine.
     */
    public String getJvmRoute();


    /**
     * Set the JvmRouteId for this engine.
     *
     * @param jvmRouteId the (new) JVM Route ID. Each Engine within a cluster
     *        must have a unique JVM Route ID.
     */
    public void setJvmRoute(String jvmRouteId);


    /**
     * Return the <code>Service</code> with which we are associated (if any).
     */
    public Service getService();


    /**
     * Set the <code>Service</code> with which we are associated (if any).
     *
     * @param service The service that owns this Engine
     */
    public void setService(Service service);


    /**
     * Set the DefaultContext
     * for new web applications.
     *
     * @param defaultContext The new DefaultContext
     */
    public void addDefaultContext(DefaultContext defaultContext);


    /**
     * Retrieve the DefaultContext for new web applications.
     */
    public DefaultContext getDefaultContext();


    // --------------------------------------------------------- Public Methods


    /**
     * Import the DefaultContext config into a web application context.
     *
     * @param context web application context to import default context
     */
    public void importDefaultContext(Context context);


}

可以给引擎设置默认主机或上下文。注意引擎也跟服务(service)相关联。将在第 14 章介绍Services 相关内容。

13.8 StandardEngine类

类 org.apache.catalina.core.StandardEngine 是 Engine 接口的标准实现,跟StandardContext 和 StandardHost 相比,StandardEngine 类相对较小。初始化时,StandardEngine 类需要添加一个基础阀门,下面是该类构造函数:

public StandardEngine() {
    super();
    pipeline.setBasic(new StandardEngineValve());
}

作为顶层容器,StandardEngine 可以有子容器,它的子容器必须是主机(host)。如果你尝试给它添加一个非主机容器,会产生异常。如下是StandardEngine 类的 addChile()方法:

public void addChild(Container child) {
    if (!(child instanceof Host))
        throw new IllegalArgumentException(sm.getString("StandardEngine.notHost"));
    super.addChild(child);
}

由于位于容器顶层,所以引擎不能有父容器,当你尝试给引擎设置父容器时会产生异常,下面是 StandardEngine 类的 setParent()方法:

public void setParent(Container container) {
    throw new IllegalArgumentException(sm.getString("standardEngine.notParent"));
}

13.9 StandardEngineValve类

org.apache.catalina.core.StandardEngineValve 是StandardEngine 的基础阀门,它的 invoke() 方法如 Listing13.7。

Listing 13.7: The invoke method of StandardEngineValve

public void invoke(Request request, Response response,
                       ValveContext valveContext)
        throws IOException, ServletException {
        // Validate the request and response object types
        if (!(request.getRequest() instanceof HttpServletRequest) ||
            !(response.getResponse() instanceof HttpServletResponse)) {
            return;     // NOTE - Not much else we can do generically
        }

        // Validate that any HTTP/1.1 request included a host header
        HttpServletRequest hrequest = (HttpServletRequest) request;
        if ("HTTP/1.1".equals(hrequest.getProtocol()) &&
            (hrequest.getServerName() == null)) {
            ((HttpServletResponse) response.getResponse()).sendError
                (HttpServletResponse.SC_BAD_REQUEST,
                 sm.getString("standardEngine.noHostHeader",
                              request.getRequest().getServerName()));
            return;
        }

        // Select the Host to be used for this Request
        StandardEngine engine = (StandardEngine) getContainer();
        Host host = (Host) engine.map(request, true);
        if (host == null) {
            ((HttpServletResponse) response.getResponse()).sendError
                (HttpServletResponse.SC_BAD_REQUEST,
                 sm.getString("standardEngine.noHost",
                              request.getRequest().getServerName()));
            return;
        }

        // Ask this Host to process this request
        host.invoke(request, response);

    }

在验证了请求对象和响应对象之后,invoke()方法获得一个 Host 实例来处理请求。是通过调用引擎的 map()方法得到主机。一旦获得了一个主机,它的 invoke()方法将会被调用。

13.10 应用Demo2

本章的第二个应用Demo用于演示引擎作为顶层容器。该Demo使用了2个类,ex13.pyrmont.core.SimpleContextConfig 和ex13.pyrmont.startup.Bootstrap2 类。Bootstrap2 类如 Listing13.8 所示。

Listing 13.8: The Bootstrap2 class

package ex13.pyrmont.startup;

//Use engine
import ex13.pyrmont.core.SimpleContextConfig;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappLoader;


public final class Bootstrap2 {
  public static void main(String[] args) {
    //invoke: http://localhost:8080/app1/Primitive or http://localhost:8080/app1/Modern
    System.setProperty("catalina.base", System.getProperty("user.dir"));
    Connector connector = new HttpConnector();

    Wrapper wrapper1 = new StandardWrapper();
    wrapper1.setName("Primitive");
    wrapper1.setServletClass("PrimitiveServlet");
    Wrapper wrapper2 = new StandardWrapper();
    wrapper2.setName("Modern");
    wrapper2.setServletClass("ModernServlet");

    Context context = new StandardContext();
    // StandardContext's start method adds a default mapper
    context.setPath("/app1");
    context.setDocBase("app1");

    context.addChild(wrapper1);
    context.addChild(wrapper2);

    LifecycleListener listener = new SimpleContextConfig();
    ((Lifecycle) context).addLifecycleListener(listener);

    Host host = new StandardHost();
    host.addChild(context);
    host.setName("localhost");
    host.setAppBase("webapps");

    Loader loader = new WebappLoader();
    context.setLoader(loader);
    // context.addServletMapping(pattern, name);
    context.addServletMapping("/Primitive", "Primitive");
    context.addServletMapping("/Modern", "Modern");

    Engine engine = new StandardEngine();
    engine.addChild(host);
    engine.setDefaultHost("localhost");

    connector.setContainer(engine);
    try {
      connector.initialize();
      ((Lifecycle) connector).start();
      ((Lifecycle) engine).start();

      // make the application wait until we press a key.
      System.in.read();
      ((Lifecycle) engine).stop();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

13.10.1 运行Demo

在 Windows 下面可以在工作目录下输入如下命令运行该程序:

java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./lib/commons—digester.jar;./
ex13.pyrmont.startup.Bootstrap2

在 Linux 下面需要使用分号来分隔开两个库

java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./lib/commons-digester.jar:./
ex13.pyrmont.startup.Bootstrap2

使用如下 URL 可以调用 PrimitiveServlet和ModernServlet:

http://localhost:8080/app1/Primitive
http://localhost:8080/app1/Modern

13.11小结

在本章中,我们讨论了两种类型的容器:主机(Host)和引擎(Engine)。本章还介绍了这两种容器的相关类。并且用2个Demo演示了如何让这两种容器作为顶层容器来工作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值