Tomcat 与 Java Web开发技术详解(第三版)第九章 HTTP会话的使用与管理 笔记

第九章 HTTP 会话的使用与管理

Web服务器跟踪客户的状态的 4 种方法:

  • 在 HTML 表单中加入隐藏字段,它包含用于跟踪客户状态的数据。
  • 重写 URL,使它包含跟踪客户状态的数据。
  • 用 Cookie 来传送用于跟踪客户状态的数据。
  • 使用会话(Session) 机制。

9.1 会话简介

HTTP 是无状态协议。无状态,是指当一个个浏览器客户程序与服务器之间多次进行基于 HTTP 请求/响应模式的通信时,HTTP 协议本身没有提供服务器连续跟踪特定浏览器端状态的规范。
例如,有多个客户同时访问bookstore应用时,需要把同一本书加入到他们自己的购物车中,假如,请求数据是完全相同的,当bookstore 应用接收到请求后,怎么判断每个请求是哪个客户发出的,从而把书加入到对应的购物车中呢?需要在 HTTP 请求中加入一些额外的跟踪客户状态的数据。

在 Web 开发中,会话机制是用于跟踪客户状态的普遍解决方案。

会话 指的是在一段时间内,单个客户与 Web 应用的一连串相关交互过程。在一个会话中,客户可能会多次请求访问 Web 应用的同一个网页或多个网页。

Servlet 规范制定了基于 Java 的会话的具体运作机制。在Servlet API 中定义了会话的 javax.servlet.http.HttpSession 接口,Serlvet容器必须实现这一街口。当一个会话开始时,Servlet 容器将创建一个 HttpSession 对象,在 HttpSession 对象总可以存放表示客户状态的信息。Servlet 容器为每个 HttpSession 对象分配一个唯一标识符,称为 SessionID。

会话的运作流程:

  1. 一个浏览器第一次访问支持会话的网页时,Servlet 容器会查看 HTTP 请求中表示 SessionID 的 Cookie,这时还不存在,因此就认为一个新的会话开始了,于是容器会创建一个 HttpSession 对象,为它分配一个唯一的 SessionID,然后把SessionID作为Cookie添加到 HTTP 响应结果中。浏览器接收到 HTTP 响应结果后,会把其中表示 SessionID 的Cookie保存在客户端。
  2. 浏览器进程继续请求访问用用中任意一个支持会话的网页,在本次 HTTP 请求中会包含表示 SesisonID 的 Cookie。Servlet 容器会在 HTTP 请求中查找表示 SessionID 的 Cookie,这时能够获得 Cookie。因此本次请求处于一个会话中,
    Servlet 容器不会创建一个新的 HttpSession 对象,而是从 Cookie 中获取 SessionID,然后根据SessionID 找到内存中的 HttpSsession 对象。
  3. 浏览器进程重复步骤2,直到当前会话被销毁,HttpSession 对象就会结束生命周期。

9.2 HttpSession 的生命周期及会话范围

会话范围: 指浏览器与一个 Web 应用进行一次会话的过程。
在具体实现上,会话范围与 HttpSession 对象的生命周期对应。因此,Web 组件只要共享同一个 HttpSession
对象,也能共享会话范围的共享数据。

HttpSession 接口的以下方法用于向会话范围内存取或删除共享数据:

  • setAttribute(String name, Object object):存数据。
  • getAttribute(String name):返回会话范围内与参数 name 匹配的数据。
  • getAttributeNames():返回共享数据范围内的所有共享数据名。
  • removeAttribute(String name):移除一个共享数据。

其他方法:

  • invalidate():销毁当前的会话,Servlet 容器会释放 HttpSession 对象占用的资源。
  • isNew():判断是否是新建的会话,是,true。
  • setMaxInactiveInterval(int interval):设定一个会话处于不活动状态的最长时间,以 s 为单位。如果超过这个时间,Servlet 容器会自动销毁这个会话。如果参数 interval 设置为负数,表示不限制,即会话永远不会过期。
  • getMaxInactiveInterval():读取当前会话处于不活动状态的最长时间。
  • getServletContext():返回当前 Web 应用的 ServletContext 对象。

Tomcat 为会话设定的默认保持不活动状态的最长时间为 1800 秒。

当一个会话开始后,如果浏览器突然关闭,Servlet 容器无法立即知道浏览器进程已经被关闭,因此 Servlet 容器端的 HttpSession 对象不会立即结束生命周期。但是,当浏览器关闭后,这个会话就进入了不活动状态,等超过了setMaxInactiveInterval(int interval) 设置的时间后,会话就会因为过期而被 Servlet容器销毁。

会话过期具有以下意义:

  • 销毁长时间处于不活动状态的会话,可以及时释放无效 HttpSession 对象占用的内存空间。
  • 防止未授权的用户访问会话,提高 Web 应用的安全性。

9.3 使用会话的 JSP 范例

maillogin.jsp

<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>maillogin</title></head>

<body bgcolor="#FFFFFF" onLoad="document.loginForm.username.focus()">

<%
    String name = "";
    if (!session.isNew()){
        name = (String) session.getAttribute("name");
        if (name == null){
            name = "";
        }
    }

%>
<p>欢迎光临邮件系统</p>
<p>Session ID:<%=session.getId()%></p>
<table width="500" border="0">
    <tr>
        <td>
            <table width="500" border="0" >
                <form name="loginForm" method="post" action="mailcheck.jsp">
                    <tr>
                        <td width="401"><div align="right">User Name:&nbsp;</div></td>
                        <td width="399"><input type="text" name="username" value="<%=name%>" ></td>
                    </tr>
                    <tr>
                        <td width="401"><div align="right">Password:&nbsp;</div></td>
                        <td width="399"><input type="password" name="password"></td>
                    </tr>
                    <tr>
                        <td width="401">&nbsp;</td>
                        <td width="399"><br><input type="Submit" name="Submit"  value="提交"></td>
                    </tr>
                </form>
            </table>
        </td>
    </tr>
</table>

</body></html>

mailcheck.jsp

<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>mailcheck</title></head>
<body>



<%
String name=null;
name=request.getParameter("username");
if(name!=null)
  session.setAttribute("username",name);
else{
  name=(String)session.getAttribute("username");
  if(name==null){
    response.sendRedirect("maillogin.jsp");    
  }
}
%>

<a href="maillogin.jsp">登录</a>&nbsp;&nbsp;&nbsp;
<a href="maillogout.jsp">注销</a>
<p>当前用户为:<%=name%> </P>
<P>你的信箱中有100封邮件</P>

</body></html>

mailloginout.jsp

<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>maillogout</title></head>
<body>

<%
String name=(String)session.getAttribute("username");
session.invalidate(); 
%>

<%=name%>,再见!
<p>
<p>
<a href="maillogin.jsp">重新登录邮件系统</a>&nbsp;&nbsp;&nbsp;

</body></html>

9.4 使用会话的 Servlet 范例

JSP文件默认情况下是支持会话的,而 HttpServlet 类默认情况下是不支持会话的。这是 JSP 与 HttpServlet 的一个小区别。

Servlet 容器调用 HttpServlet 类的服务方法时,会传递一个 HttpServletRequest 类型的参数,HttpServlet 可以通过 HttpServletRequest 对象来获取 HttpSession 对象。

  • getSession():使得当前 HttpServlet 支持会话。假如会话存在,就返回相应的 HttpSession 对象,否则就创建一个新会话,并将新建的 HttpSession 对象返回。该方法等价于调用 HttpServletReuqest 的 getSession(ture)方法。
  • getSession(boolean create):如果为 true,等价于 getSession() 方法。为 false,那么假如会话存在,返回 HttpSession对象,否则返回 null。
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String[] itemNames = {"糖果","收音机","练习簿"};
        //获取 HttpSession 对象
        HttpSession session = req.getSession(true);
        //获取会话范围内的 ShoppingCart 对象
        ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
        if (cart == null) {
            cart = new ShoppingCart();
            session.setAttribute("cart", cart);
        }
        
    }

9.5 通过重写 URL 来跟踪会话

当浏览器禁用 Cookie,Servlet 容器无法向客户端存放 SessionID,Servlet 容器就无法跟踪会话。因此,每次客户请求访问支持会话的 JSP 页面时,Servlet 容器就会创建一个新的会话,这样就无法把多个业务逻辑上相关的客户请求放在同一个会话中。

Servlet 规范提供了另一种跟踪会话的方案,如果浏览器不支持Cookie,Servlet 容器可以重写 Web 组件的URL,把 SessionID 添加到 URL 信息中。 HttpServletResponse 接口提供了重写 URL 的方法:

public java.lang.String encodeURL(String url);

对于9.4节中的 maillogin.jsp 的 from 表单提交地址修改

<--!修改前-->
<form name="loginForm" method="post" action="mailcheck.jsp">
......
</form>

<--!修改后-->
<form name="loginForm" method="post" action="<%=response.encodeURL('mailcheck.jsp')%>">
.....
<form>

response.encodeURL(‘mailcheck.jsp’) 运行流程如下:
(1)判断mailcheck.jsp 是否支持会话,如果不支持,那么直接返回参数指定的URL mailcheck.jsp。
(2)判断浏览器是否支持Cookie。支持,直接返回URL。不支持Cookie,就在参数指定的 URL中加入当前 SessionID 的信息,然后返回修改后的 URL。

<form name="loginForm" method="post" action="mailcheck.jsp?jsessionid=954166...">
......
</form>

由此可见,只有当当前 Web 组件支持会话,浏览器不支持 Cookie的情况下,encodeURL(String url) 方法才会重写 URL,否则,直接返回参数指定的原始 URL。

9.6 会话的持久化

把内存中的 HttpSession 对象保存到文件系统或数据库中,这一过程称为会话的持久化。
会话的持久化有以下两个好处:

  1. 节约内存空间。假定有一万个客户同时访问某个 Web 应用,Servlet 容器中会生成一万个 HttpSession 对象。如果把这些对象都放在内存中,将消耗大量的内存资源,显然是不可取的。因此,把处于不活动状态的 HttpSession 对象转移到文件系统或数据库中,这样可以提高对内存资源的利用率。
  2. 确保服务器重启或单个Web应用重启后,能恢复重启前的会话。

在持久化会话时,Servlet 容器不仅会持久化 HttpSession 对象,还会对其所有可序列化的属性进行持久化,从而确保存放在会话范围内的共享数据不会丢失。可序列化属性就是指属性所属的类实现了 java.io.Serializable 接口。

会话在其生命周期中,可能会在运行时状态和持久化状态之间转换:

  • 运行时状态: 主要特征就是 HttpSession 对象位于内存中。运行时状态还包含两个子状态:不活动状态和活动状态
      不活动状态: 指在一段时间内,处于会话中的客户端一直没有向 Web 应用发送任何请求。
      活动状态: 指在一段时间内,处于会话中的客户端频繁的向 Web 应用发送各种 HTTP请求。
  • 持久化状态: 主要特征就是 HttpSession 对象位于永久性存储设备中。

会话从运行时状态变为持久化状态的过程称为搁置(或持久化)。在一下情况下会话会被搁置:

  • 服务器终止或单个 Web 应用终止, Web 应用中的会话会被搁置。
  • 会话处于不活动状态时间太长,达到了特定的限制值。
  • Web 应用中处于运行时状态的会话数目太多,达到了特定的限制值,部分会话会被搁置。

会话从持久化状态变为运行时状态的过程称为激活(或加载)。在一下情况下会话会被激活:

  • 服务器或单个 Web 应用重启。
  • 处于会话中的客户端向 Web 应用发出 HTTP请求,相应的会话会被激活。

Java Servlet API 没有为会话的持久化提供标准的接口。会话的持久化完全依赖于 Servlet 容器的具体实现。

Tomcat 的会话管理器有两种:

  • org.apache.catalina.session.StandarManager 类:标准会话管理器。
  • org.apache.catalina.session.PersistentManager 类:提供了更多的管理会话的功能。

9.6.1 标准会话管理器 StandarManager

默认的会话管理器。
它的实现机制为:
当 Tomcat 服务器或打个 Web 应用终止时,会对被终止的Web应用的 HttpSession 对象持久化。把它们保存到文件系统中。
默认的文件为:

	<CATALINA_HOME>/work/Catalina/[hostname]/[application]/SESSIONS.ser

当 Tomcat 或单个 Web 应用重启时,会激活已经被持久化的 HttpSession 对象。

9.6.2 持久化会话管理器 PersistentManager

PersistentManager 把存放 HttpSession 对象的永久性存储设备成为会话Store。 PersistentManager具有以下功能:

  • 当 Tomcat 服务器或单个 Web 应用关闭或重启,会对 Web 应用的 HttpSession 对象进行持久化,把他们保存到会话 Store中。
  • 具有容错功能,及时把 HttpSession 对象备份到会话 Store 中。
  • 可以灵活控制在内存中的 Httpsession 对象的数目,将部分 HttpSession 转移到会话 Store 中。

Tomcat 中会话 Store 的接口为 org.apache.Catalina.Store,提供了两个实现这一接口的类:

  • org.apache.Catalina.FileStore:把 HttpSession 对象保存到一个文件中。
  • org.apache.Catalina.JDBCStore:把 HttpSession 对对象保存到数据库的一张表中。

1、配置 FileStore

FileStore 将 HttpSession 对象保存到文件中,默认的目录是 /work/Catalina/[hostname]/[applicationname]。每个 HttpSession 对象都会对应一个文件,它以 SessionID 作为文件名,扩展名为 .session。
配置会话管理器:

<Context  reloadable="true" >
  <Manager className="org.apache.catalina.session.PersistentManager"
           saveOnRestart="true"
           maxActiveSession="10"
           minIdleSwap="60"
           maxIdleInterval="300"
           maxIdleBackup="10"
           maxInactiveInterval="300">
    <!--directory 指定会话的存放目录-->
    <Store className="org.apache.catalina.session.FileStore"
           directory="mydir"/>
  </Manager>
</Context>

<Manager>元素的属性

   方法                    描述
className指定会话管理的类名
saveOnRestarttrue,表示当Web应用终止时,会把内存中所有的 HttpSession 对象都保存到会话 Store 中。当 Web 应用重启时,会重新加载这些会话对象
maxActiveSessions设定处于运行时状态的会话最大数目,如果超过这一数目,Tomcat 将把一些 HttpSession 对象转移到会话 Store 中。为 -1 表示不限制处于运行时状态的会话数目
maxIdleSwap指定会话处于不活动状态的最短时间(秒)超过这一时间,Tocmat 有可能把这个 HttpSession 转移到会话 Store 中。为 -1 表示不限制
maxIdleSwap指定会话处于不活动状态的最长时间(秒),超过这一时间,Tocmat 必须把这个 HttpSession 对象保存到会话 Store中。为 -1 表示不限制
maxxIdleBackup指定会话处于不活动状态的最长时间(秒),超过这时间,Tomcat 将为这个 HttpSession 对象在会话 Store 中备份。与 maxIdleSwap不同,这个 HttpSession 对象仍然存在于内存中
maxInactiveInterval指定会话处于不活动状态的最长时间(秒),超过这一时间,Tocmat会使这个会话过期

2、配置 JDBCStore

CREATE TABLE `tomcat_sessions` (
  `session_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表示 SessionID',
  `valid_session` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表示会话是否有效',
  `max_inactive` int NOT NULL COMMENT '表示会话可以处于不活动状态的最长时间',
  `last_access` bigint NOT NULL COMMENT '表示最近一次访问时间',
  `app_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '表示会话所属的 Web 应用名称',
  `session_data` mediumblob COMMENT '表示 HttpSession 对象的序列化数据',
  PRIMARY KEY (`session_id`),
  KEY `kapp_name` (`app_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

配置

<Context  reloadable="true">
  <Manager className="org.apache.catalina.session.PersistentManager" 
    saveOnRestart="true"
    maxActiveSessions="10"
    minIdleSwap="60"
    maxIdleSwap="120"
    maxIdleBackup="180"
    maxInactiveInterval="300">
	<--! com.mysql.jdbc.Driver 驱动的jar包我是放在tomcat  lib 下才识别到的
	不然一直报 ClassNotFoundException 找不到驱动 -->
   <Store className="org.apache.catalina.session.JDBCStore"
     driverName="com.mysql.jdbc.Driver"
     connectionURL="jdbc:mysql://localhost/tomcatsessionDB?user=dbuser&amp;password=1234&amp;useSSL=false"
     sessionTable="tomcat_sessions"
     sessionIdCol="session_id"
     sessionDataCol="session_data"
     sessionValidCol="valid_session"
     sessionMaxInactiveCol="max_inactive"
     sessionLastAccessedCol="last_access"
     sessionAppCol="app_name"
     checkInterval="60" />
  
  </Manager>
</Context>

<Store>子元素的属性

      方法                     描述
className设定会话 Store 的类名
driverName设定数据库驱动程序的类名
connectionURL设定数据库访问的URL,在URL中应该包含访问数据库的用户名和密码
sessionTable存放HttpSession 对象的表名
sessinIdCol表示 SessionID 字段
sessionDataCol表示 HttpSession 对象序列化数据的字段名
sessionAppCol表示 Web应用的名字的字段
sessionVaild表示会话是否有效的字段
sessionMaxInactiveCol表示会话可以处于不活动状态的最长时间
sessionLastAcdessCol表示最近一次访问会话的时间
checkInterval表示Tomcat 定期检查会话状态的字段的名字

9.7 会话的监听

Servlet API 定义了四个用于监听会话中各种事件的接口。
(1)HttpSessionListener 接口:监听会话以及销毁会话的事件。有两个方法:

  • sessionCreated(HttpSessionEvent event):当 Servlet 容器创建了一个会话后调用此方法。
  • sessionDestory(HttpSessionEvent event):当 Servlet 容器将要销毁一个会话之前调用此方法。

(2)HttpSessionAttributeListener 接口: 监听会话中加入属性、替换属性和删除属性的事件,有三个方法:

  • attributeAdded(HttpSessionBindingEvent event):当Web 应用向一个会话中加入了一个新属性,Servlet 容器会调用此方法。
  • attributeRemoved(HttpSessionBindingEvent event):删除一个会话,容器调用此方法。
  • attributeReplaced(HttpSessionBindingEvent event):替换了会话中已存在的属性值,容器调用此方法。

(3)HttpSessionBindingListener 接口: 监听会话与一个属性绑定或解除绑定的时间。

  • valueBound(HttpSessionBindingEvent event):当 Web 应用把一个属性与会话绑定后,容器调用。
  • valueUnBound(HttpSessionBindingEvent event):当 Web 应用把一个属性与会话解除绑定前,容器调用。

**(4)HttpSessionActivationListener(HttpSessionEvent event) 接口:**监听会话被激活或被搁置的事件。

  • sessionDidActiveate(HttpSessionEvent event):当 Servlet 容器把一个会话激活后,容器调用。
  • sessionWiillPasssivate(HttpSessionEvent event):当 Servlet 容器把一个会话搁置之前,容器调用。

HttpSessionLitener 和 HttpSessionAtrributeLitener 需要在 web.xml 文件中通过 <litener> 配置,向 Servlet 容器中注册。

public class MySessionLifeListener implements HttpSessionListener,HttpSessionAttributeListener{ 
  public void sessionCreated(HttpSessionEvent event) { 
    System.out.println("A new session is created. id = " + event.getSession().getId());
  } 

  public void sessionDestroyed(HttpSessionEvent event) {

    System.out.println("A new session is to be destroyed. id = " + event.getSession().getId());
  } 
  

  public void attributeAdded(HttpSessionBindingEvent event){
    System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is added into a session.");
  } 
   
  public void attributeRemoved(HttpSessionBindingEvent event){
    System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is removed from a session.");
  } 
   
  public void attributeReplaced(HttpSessionBindingEvent event){
    System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is replaced in a session.");
  } 

} 

web.xml

	<listener>
        <listener-class>com.mypack.MySessionLifeListener</listener-class>
    </listener>

HttpSessionBindingLitener 和 HttpSessionActivationListener 则是通过会话的属性来实现。
如下:

public class MyData implements HttpSessionBindingListener,HttpSessionActivationListener,Serializable{
  private int data;

  public MyData(){}

  public MyData(int data){
    this.data=data;
  }
  
  public int getData(){
    return data;
  }
  
  public void setData(){
    this.data=data;
  }
 
  public void valueBound(HttpSessionBindingEvent event){
    System.out.println("MyData is bound with a session.");
  }

  public void valueUnbound(HttpSessionBindingEvent event){
    System.out.println("MyData is unbound with a session.");
  }

  public void sessionDidActivate(HttpSessionEvent se){
    System.out.println("A session is activate.");
  } 

  public void sessionWillPassivate(HttpSessionEvent se){
    System.out.println("A session will be passivate.");
  }  

}

使用会话属性(需要添加到会话中的数据)的类来实现 HttpSessionBindingLitener 和 HttpSessionActivationListener 接口,进行相关操作时,就会调用响应的方法。

9.7.1 用 HttpSessionLitener 统计在线用户人数

一个用户登入 Web 应用就会创建一个会话,当这个会话被销毁,就意味着用户离开的 web 应用。

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class OnlineCounterListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        //创建一个会话后调用
        HttpSession session = se.getSession();
        ServletContext context = session.getServletContext();
        
        Integer count = (Integer) context.getAttribute("count");
        // 把 count 放入应用范围内
        if (count == null){
            session.setAttribute("count", 1);
        } else {
            session.setAttribute("count", count + 1);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // 会话被销毁前调用
        HttpSession session = se.getSession();
        ServletContext context = session.getServletContext();
        Integer count = (Integer) context.getAttribute("count");
        if (count == null){
            return;
        } else {
            context.setAttribute("count", count - 1);
        }
        
    }
}

将其注册到容器中即可。

9.7.2 用 HttpSessionBindingListener 统计用户在线人数

OnlineUsers 表示在线用户的名单

public class OnlineUsers{
  private static final OnlineUsers onlineUsers=new OnlineUsers();
  private List<String> users=new ArrayList<String>();

  public void add(String name){
    users.add(name);
  }
  
  public void remove(String name){
    users.remove(name);
  }

  public List getUsers(){
    return users;
  }
  
  public int getCount(){
    return users.size();
  }
  public static OnlineUsers getInstance(){
    return onlineUsers; 
  }
}

User 表示用户,实现了 HttpSessionBindingListener 监听器

public class User implements HttpSessionBindingListener,Serializable{
  private OnlineUsers onlineUsers=OnlineUsers.getInstance();
  private String name=null;

  public User(String name){
    this.name=name;
  }
  
  public void setName(String name){
    this.name=name;
  }
   
  public String getName(){
    return name;
  }
  
  public void valueBound(HttpSessionBindingEvent event){
    onlineUsers.add(name);
    System.out.println(name+" is bound with a session");
  }

  public void valueUnbound(HttpSessionBindingEvent event){
    onlineUsers.remove(name); 
    System.out.println(name+" is unbound with a session");
  }
}

通过上面两个类,当用户登入 Web 应用时,在 Web 应用中将用户的信息(User)和 HttpSession 进行绑定,就会调用 valueBound() 方法,从而记录下用户信息。当用户退出应用时,应用将 用户和会话进行解绑,则会调用 valueUnbound()方法。从而实现记录用户在线人数以及用户具体信息的功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值