Red5+SpringMVC搭建自己的第一台直播服务器


基本环境

Eclipse

Eclipse Java EE IDE for Web Developers. 
Version: Neon.3 Release (4.6.3)
Build id: 20170314-1500

地址:https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/neon/3/eclipse-jee-neon-3-win32-x86_64.zip

RED5 Server

我这里用的是 Red5 Server 1.0.9
地址:https://github.com/Red5/red5-server/releases

解压server包,得到server目录



此时我们可以双击red5.bat,看看是否可以运行,如果失败,通常问题是提示jvm版本问题。

我这里用的是jdk1.8 64bit

java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b15, mixed mode)

RED5-Eclipse-Plugin

地址:https://github.com/Red5/red5-eclipse-plugin

插件的安装方法就不赘述了

插件有一个问题就是在安装后,创建项目新建server的时候会要求指向server目录,其中自动匹配red5.sh,这里是sh,我们是win平台

sh肯定是运行不了的。手动改成bat会无法进行下一步!我这个IDE是这样的或许你没事呢 偷笑

我们改一下他的插件

1. 导入插件到eclipse
2. 选择  org.leagueplanet.server.glassfish 项目
3. 打开red5.serverdef
4. 搜.sh
5. 把red5-debug.sh red5.-shutdown.sh 改为 .bat 结尾即可

这样下来,在配置server路径的时候我们把 .sh 改为 .bat 就不会有错误提示,也不会无法点下一步了!

开始搭建

项目创建

创建一个Dynamic Web Project 项目
Project name: liveOnline
target runtime 选择 new runtime
Infrared5 下选择 red5 server, next




red5 Runtime 配置

选择jdk1.8 ,把red5目录指向,我们解压的red5 server文件夹



配置red5 server,端口我选的默认,这里看红色框中默认是.sh  我们改为 bat后也依然可以next 大笑



回到创建project页面我们继续进行配置,自定义修改项目配置




勾选red5 application generation 
    
  

点击完成项目创建

看项目列表,我们不仅得到了red5的项目结构,还得到了附赠的client测试端

.

测试RED5 server

我们先去server标签中启动red5服务,先跑一个空服务看看red5 server是否可以正确启动

启动如果报错,说明路径有问题

启动成功后,访问 http://127.0.0.1:5080

下面是red5 启动成功的欢迎界面,如果没有这个界面说明red5 启动报错,仔细查查吧,通常是路径配置问题,如果提示不是有效的win32程序,则是路径配置中没有修改.sh .bat 的主程序指向。

 

red5-web.properties

在eclipse 中我们打开 liveOnline中 red5-web.properties 
webapp.virtualHosts属性表示了访问控制,默认red5给加了一个 192的ip,如果你的内网IP和它不同你可以修改或者直接改为 * (星号)

red5-web.xml

<bean id="web.handler" class="org.red5.core.Application" /> 可以看到这里配置了application.java 来得到red5的各种事件状态,当然这个org.red5.core.application
 已经被自动创建了,我们可以自己修改。web.xml 没什么好说的,暂时不去改它。

基本通讯

下面我们先尝试一下这个 liveOnline 能否完成基本的rtmp通讯

我们将项目add到 red5 server,然后右键 publish

然后debug或start启动

打开http://127.0.0.1:5080/demos/publisher.html ,这是red5 提供的一个测试flex 可以完成推流拉流操作。



我们在location一行中,输入我们的项目名,再点击Connect,观察右侧console

提示
- Connecting to rtmp://localhost/liveOnline
- NetConnection.Connect.Success

证明已经成功通讯
下面我们可以切换到Video标签选择自己的摄像头,然后点start
之后修改name为9800(这个不过是一个rtmp的通讯key ,key key对应则建立推拉的都是一个流),之后选择发布,则已经开始直播了,
直播地址就是 rtmp://{ip}//liveOnlive  ,key 就是输入的9800,当然有的地方叫做 filename
我们可以选择view来观看自己的直播,切换到view界面在name中改为9800,然后点击play即可

关于如何关闭red5 server

从eclipse server标签中关闭要stop好多次才可以成功,取个巧的办法是从任务管理器中删除java.exe 进程

完善red5项目

我们先建立一下文件目录在liveOnline
修改red5-web.properties 中webapp.virtualHosts 为 * 
修改red5-web.xml 中 <bean id="web.handler" class="com.service.Application" /> 

[java] view plain copy
  1. package com.state;  
  2. /** 
  3.  * 临时容器 
  4.  * @author Allen 2017年3月31日 
  5.  * 
  6.  */  
  7.   
  8. import java.util.HashMap;  
  9.   
  10. import com.state.room.RoomVo;  
  11.   
  12. public class Ram {  
  13.   
  14.     public static HashMap<String, RoomVo> roomHm = new HashMap<>();  
  15.       
  16. }  
[java] view plain copy
  1. package com.state.user;  
  2.   
  3. public class UserVo implements java.io.Serializable {  
  4.     /** 
  5.      *  
  6.      */  
  7.     private static final long serialVersionUID = -6628674875994109212L;  
  8.     private String red5Id;  
  9.     private String red5Name;  
  10.     private Long red5CreateTime;  
  11.   
  12.     public String getRed5Id() {  
  13.         return red5Id;  
  14.     }  
  15.   
  16.     public void setRed5Id(String red5Id) {  
  17.         this.red5Id = red5Id;  
  18.     }  
  19.   
  20.     public String getRed5Name() {  
  21.         return red5Name;  
  22.     }  
  23.   
  24.     public void setRed5Name(String red5Name) {  
  25.         this.red5Name = red5Name;  
  26.     }  
  27.   
  28.     public Long getRed5CreateTime() {  
  29.         return red5CreateTime;  
  30.     }  
  31.   
  32.     public void setRed5CreateTime(Long red5CreateTime) {  
  33.         this.red5CreateTime = red5CreateTime;  
  34.     }  
  35.   
  36. }  
[java] view plain copy
  1. package com.state.user;  
  2.   
  3. import java.text.SimpleDateFormat;  
  4. import java.util.Date;  
  5. import java.util.Iterator;  
  6. import java.util.List;  
  7.   
  8. import com.state.Ram;  
  9.   
  10. /** 
  11.  * 用户操作 
  12.  *  
  13.  * @author Allen 2017年3月31日 
  14.  * 
  15.  */  
  16. public class UserState {  
  17.     /** 
  18.      * 插入用户到房间中 
  19.      * @param red5Id 
  20.      * @param red5Name 
  21.      * @param red5CreateTime 
  22.      * @param roomKey 
  23.      * @return 
  24.      */  
  25.     public boolean insert(String red5Id, String red5Name, Long red5CreateTime, String roomKey) {  
  26.         try {  
  27.             if (Ram.roomHm.containsKey(roomKey)) {  
  28.                 UserVo uvo = new UserVo();  
  29.                 uvo.setRed5Id(red5Id);  
  30.                 uvo.setRed5Name(red5Name);  
  31.                 uvo.setRed5CreateTime(red5CreateTime);  
  32.                 Ram.roomHm.get(roomKey).getUserList().add(uvo);  
  33.                 return true;  
  34.             }  
  35.             selectAll(roomKey);  
  36.         } catch (Exception e) {  
  37.             e.printStackTrace();  
  38.         }  
  39.         return false;  
  40.     }  
  41.     /** 
  42.      * 获取房间中全部用户 
  43.      * @param roomKey 
  44.      * @return 
  45.      */  
  46.     public List<UserVo> selectAll(String roomKey) {  
  47.   
  48.         try {  
  49.             if (Ram.roomHm.containsKey(roomKey)) {  
  50.                 Iterator<UserVo> it = Ram.roomHm.get(roomKey).getUserList().iterator();  
  51.                 System.out.println("================================================");  
  52.                 while (it.hasNext()) {  
  53.                     UserVo temp = it.next();  
  54.                     System.out.println(temp.getRed5Id() + "," + temp.getRed5Name() + ","  
  55.                             + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(temp.getRed5CreateTime())));  
  56.                 }  
  57.                 System.out.println("================================================");  
  58.             }  
  59.         } catch (Exception e) {  
  60.             e.printStackTrace();  
  61.         }  
  62.         return null;  
  63.     }  
  64.     /** 
  65.      * 删除房间中某用户 
  66.      * @param redId 
  67.      * @param roomKey 
  68.      * @return 
  69.      */  
  70.     public boolean delete(String redId, String roomKey) {  
  71.   
  72.         try {  
  73.             if (Ram.roomHm.containsKey(roomKey)) {  
  74.                 Iterator<UserVo> it = Ram.roomHm.get(roomKey).getUserList().iterator();  
  75.                 while (it.hasNext()) {  
  76.                     if (it.next().getRed5Id().equals(redId))  
  77.                     {  
  78.                         it.remove();  
  79.                         break;  
  80.                     }   
  81.                 }  
  82.             }  
  83.             return true;  
  84.   
  85.         } catch (Exception e) {  
  86.             e.printStackTrace();  
  87.         }  
  88.         return false;  
  89.     }  
  90.     /** 
  91.      * 统计房间中用户数 
  92.      * @param roomKey 
  93.      * @return 
  94.      */  
  95.     public int count(String roomKey) {  
  96.         return Ram.roomHm.containsKey(roomKey) ? Ram.roomHm.get(roomKey).getUserList().size() : 0;  
  97.     }  
  98.   
  99. }  
[java] view plain copy
  1. package com.state.room;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import com.state.user.UserVo;  
  7.   
  8. /** 
  9.  * 房间VO 
  10.  *  
  11.  * @author Allen 2017年3月31日 
  12.  * 
  13.  */  
  14. public class RoomVo {  
  15.   
  16.     private String roomKey;// 房间key  
  17.     private String roomName;// 房间名  
  18.     private List<UserVo> userList=new ArrayList<>();// 房间内用户列表  
  19.   
  20.     public List<UserVo> getUserList() {  
  21.         return userList;  
  22.     }  
  23.   
  24.     public void setUserList(List<UserVo> userList) {  
  25.         this.userList = userList;  
  26.     }  
  27.   
  28.     public String getRoomKey() {  
  29.         return roomKey;  
  30.     }  
  31.   
  32.     public void setRoomKey(String roomKey) {  
  33.         this.roomKey = roomKey;  
  34.     }  
  35.   
  36.     public String getRoomName() {  
  37.         return roomName;  
  38.     }  
  39.   
  40.     public void setRoomName(String roomName) {  
  41.         this.roomName = roomName;  
  42.     }  
  43.   
  44. }  
[java] view plain copy
  1. package com.state.room;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.Iterator;  
  5. import java.util.List;  
  6. import java.util.Map.Entry;  
  7.   
  8. import com.state.Ram;  
  9.    
  10.   
  11. /** 
  12.  * 房间操作 
  13.  *  
  14.  * @author Allen 2017年3月31日 
  15.  * 
  16.  */  
  17. public class RoomState {  
  18.   
  19.     /** 
  20.      * 创建一个房间信息 
  21.      *  
  22.      * @param roomKey 
  23.      * @param roomName 
  24.      * @return 
  25.      */  
  26.     public boolean insert(String roomKey, String roomName) {  
  27.   
  28.        
  29.         try {  
  30.             if (!Ram.roomHm.containsKey(roomKey)) {  
  31.                 RoomVo rVo = new RoomVo();  
  32.                 rVo.setRoomName(roomName);  
  33.                 Ram.roomHm.put(roomKey, rVo);  
  34.                 return true;  
  35.             }  
  36.         } catch (Exception e) {  
  37.             e.printStackTrace();  
  38.         }  
  39.         return false;  
  40.     }  
  41.   
  42.     /** 
  43.      * 返回所有房间信息 
  44.      *  
  45.      * @return 
  46.      */  
  47.     public List<RoomVo> selectAll() {  
  48.   
  49.         try {  
  50.             List<RoomVo> resultList = new ArrayList<>();  
  51.             Iterator<Entry<String, RoomVo>> it = Ram.roomHm.entrySet().iterator();  
  52.             while (it.hasNext()) {  
  53.                 Entry<String, RoomVo> entry = it.next();  
  54.                 resultList.add(entry.getValue());  
  55.             }  
  56.             return resultList;  
  57.         } catch (Exception e) {  
  58.             e.printStackTrace();  
  59.         }  
  60.         return null;  
  61.     }  
  62.   
  63.     /** 
  64.      * 删除一个房间信息 
  65.      *  
  66.      * @param red5Id 
  67.      * @return 
  68.      */  
  69.     public boolean delete(String roomKey) {  
  70.         try {  
  71.             Ram.roomHm.remove(roomKey);  
  72.             return true;  
  73.         } catch (Exception e) {  
  74.             e.printStackTrace();  
  75.         }  
  76.   
  77.         return false;  
  78.     }  
  79.   
  80.     /** 
  81.      * 统计房间总数 
  82.      *  
  83.      * @return 
  84.      */  
  85.     public int count() {  
  86.         return Ram.roomHm.size();  
  87.     }  
  88.   
  89. }  
[java] view plain copy
  1. package com.service;  
  2.   
  3. import java.text.SimpleDateFormat;  
  4. import java.util.Date;  
  5.   
  6. import org.red5.server.adapter.MultiThreadedApplicationAdapter;  
  7. import org.red5.server.api.IClient;  
  8. import org.red5.server.api.IConnection;  
  9. import org.red5.server.api.scope.IScope;  
  10. import org.red5.server.api.stream.IBroadcastStream;  
  11. import org.red5.server.api.stream.ISubscriberStream;  
  12.   
  13. import com.state.room.RoomState;  
  14. import com.state.user.UserState;  
  15. /** 
  16.  *  
  17.  * @author Allen 2017年4月7日 
  18.  * 
  19.  */  
  20. public class Application extends MultiThreadedApplicationAdapter {  
  21.        
  22.    
  23.     @Override  
  24.     public boolean connect(IConnection conn) {  
  25.         System.out.println("connect");  
  26.         return super.connect(conn);  
  27.     }  
  28.   
  29.     @Override  
  30.     public void disconnect(IConnection arg0, IScope arg1) {  
  31.         System.out.println("disconnect");   
  32.         new UserState().delete(arg0.getSessionId(), arg0.getAttribute(arg0.getSessionId()).toString());  
  33.         super.disconnect(arg0, arg1);  
  34.     }  
  35.     /** 
  36.      * 开始发布直播 
  37.      */  
  38.     @Override  
  39.     public void streamPublishStart(IBroadcastStream stream) {  
  40.         System.out.println("[streamPublishStart]********** ");  
  41.         System.out.println("发布Key: " + stream.getPublishedName());  
  42.         RoomState room = new RoomState();  
  43.         room.insert(stream.getPublishedName(), "房间" + stream.getPublishedName());  
  44.         System.out.println(  
  45.                 "发布时间:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(stream.getCreationTime())));  
  46.         System.out.println("****************************** ");  
  47.     }  
  48.   
  49.     /** 
  50.      * 流结束 
  51.      */  
  52.     @Override  
  53.     public void streamBroadcastClose(IBroadcastStream arg0) {  
  54.         RoomState room = new RoomState();  
  55.         room.delete(arg0.getPublishedName());  
  56.         super.streamBroadcastClose(arg0);  
  57.     }  
  58.   
  59.     /** 
  60.      * 用户断开播放 
  61.      */  
  62.     @Override  
  63.     public void streamSubscriberClose(ISubscriberStream arg0) {  
  64.         new UserState().delete(arg0.getConnection().getSessionId(), arg0.getBroadcastStreamPublishName());  
  65.         super.streamSubscriberClose(arg0);  
  66.     }  
  67.   
  68.     /** 
  69.      * 链接rtmp服务器 
  70.      */  
  71.     @Override  
  72.     public boolean appConnect(IConnection arg0, Object[] arg1) {  
  73.         // TODO Auto-generated method stub  
  74.         System.out.println("[appConnect]********** ");  
  75.         System.out.println("请求域:" + arg0.getScope().getContextPath());  
  76.         System.out.println("id:" + arg0.getClient().getId());  
  77.         System.out.println("name:" + arg0.getClient().getId());  
  78.         System.out.println("**************** ");  
  79.         return super.appConnect(arg0, arg1);  
  80.     }  
  81.   
  82.     /** 
  83.      * 加入了rtmp服务器 
  84.      */  
  85.     @Override  
  86.     public boolean join(IClient arg0, IScope arg1) {  
  87.         // TODO Auto-generated method stub  
  88.         return super.join(arg0, arg1);  
  89.     }  
  90.   
  91.     /** 
  92.      * 开始播放流 
  93.      */  
  94.     @Override  
  95.     public void streamSubscriberStart(ISubscriberStream stream) {  
  96.         System.out.println("[streamSubscriberStart]********** ");  
  97.         System.out.println("播放域:" + stream.getScope().getContextPath());  
  98.         System.out.println("播放Key:" + stream.getBroadcastStreamPublishName());  
  99.         System.out.println("********************************* ");  
  100.         String sessionId = stream.getConnection().getSessionId();  
  101.         stream.getConnection().setAttribute(nullnull);    
  102.         new UserState().insert(sessionId, sessionId, stream.getCreationTime(), stream.getBroadcastStreamPublishName());  
  103.         super.streamSubscriberStart(stream);  
  104.     }  
  105.   
  106.     /** 
  107.      * 离开了rtmp服务器 
  108.      */  
  109.     @Override  
  110.     public void leave(IClient arg0, IScope arg1) {  
  111.         System.out.println("leave");  
  112.         super.leave(arg0, arg1);  
  113.     }  
  114.    
  115. }

重新发布启动后,如果新的application中的console没有打印,则可能是新版本发布失败。
我们删除  red5 server/ webapp / liveOnline 和  red5 server/ webapp /webapp  / liveOnline  文件夹后再次在red5 server 中右键清理和发布

服务启动后我们再次使用 http://127.0.0.1:5080/demos/publisher.html 选择链接rtmp liveOnline服务

在这一步 eclipse console 如果提示 Scope not found 那就是web.handler 中class路径配错了

正确的话不仅会返回succss,而且eclipse console 会打印我们新建的application中的 System.out.print 信息

[html] view plain copy
  1. [appConnect]**********   
  2. 请求域:/liveOnline  
  3. id:1  
  4. name:1  
  5. ****************   

SpringMVC

下面我们在red5项目中增加SpringMVC支持,来提供通过HTTP访问red5服务器内房间信息

注意一下:Spring jar包我们放到WEB-INF/libs 中,然后引用它,这里如果使用默认的lib目录,最后发布red5+springMVC的时候red5的rtmp会无法访问,会抛出Scope not found!!!!虽然这个问题在google百度github都没有解释,但是我最后还是找到的解决方案!如果你用lib没有问题,那么只能说是系统环境了。。我用的是win7 64bit。linux或者mac或许没问题吧

首先我们把WEB-INF 下面的lib改成libs,如果没有lib新建一个libs即可

我们将spring的jar包拷贝到libs,为什么选择4.3.6因为我的red5 server下lib中的spring就是4.3.6的保持版本与red5一致!

Add JARS到项目

web.xml

我们在web.xml中增加
[html] view plain copy
  1. <!-- ********************** Spring配置 ********************** -->  
  2.     <!-- 配置DispatchcerServlet -->  
  3.     <servlet>  
  4.         <servlet-name>springDispatcherServlet</servlet-name>  
  5.         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  6.         <!-- 配置Spring mvc下的配置文件的位置和名称 -->  
  7.         <init-param>  
  8.             <param-name>contextConfigLocation</param-name>  
  9.             <param-value>/WEB-INF/springmvc.xml</param-value>  
  10.         </init-param>  
  11.         <load-on-startup>2</load-on-startup>  
  12.     </servlet>  
  13.     <servlet-mapping>  
  14.         <servlet-name>springDispatcherServlet</servlet-name>  
  15.         <url-pattern>/*</url-pattern>  
  16.     </servlet-mapping>  
  17.     <!-- 静态资源处理交给默认servlet -->  
  18.     <servlet-mapping>  
  19.         <servlet-name>default</servlet-name>  
  20.         <url-pattern>*.css</url-pattern>  
  21.     </servlet-mapping>  
  22.   
  23.     <servlet-mapping>  
  24.         <servlet-name>default</servlet-name>  
  25.         <url-pattern>*.gif</url-pattern>  
  26.     </servlet-mapping>  
  27.   
  28.     <servlet-mapping>  
  29.         <servlet-name>default</servlet-name>  
  30.         <url-pattern>*.jpg</url-pattern>  
  31.     </servlet-mapping>  
  32.   
  33.     <servlet-mapping>  
  34.         <servlet-name>default</servlet-name>  
  35.         <url-pattern>*.js</url-pattern>  
  36.     </servlet-mapping>  
  37.   
  38.     <servlet-mapping>  
  39.         <servlet-name>default</servlet-name>  
  40.         <url-pattern>*.html</url-pattern>  
  41.     </servlet-mapping>   

springmvc.xml

新建一个springmvc.xml在WEB-INF目录下
[html] view plain copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"  
  4.     xmlns:mvc="http://www.springframework.org/schema/mvc"  
  5.     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
  6.         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd  
  7.         http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">  
  8.     <!-- 配置@ResponseBody 保证返回值为UTF-8 -->  
  9.     <!-- 因为StringHttpMessageConverter默认是ISO8859-1 -->   
  10.     <bean id="utf8Charset" class="java.nio.charset.Charset"  
  11.         factory-method="forName">  
  12.         <constructor-arg value="UTF-8" />  
  13.     </bean>  
  14.     <mvc:annotation-driven>  
  15.         <mvc:message-converters>  
  16.             <bean class="org.springframework.http.converter.StringHttpMessageConverter">  
  17.                 <constructor-arg ref="utf8Charset" />  
  18.             </bean>  
  19.         </mvc:message-converters>  
  20.     </mvc:annotation-driven>  
  21.     <!-- 配置自动扫描的包 -->  
  22.     <context:component-scan base-package="com.action"></context:component-scan>  
  23.     <!-- 配置视图解析器 如何把handler 方法返回值解析为实际的物理视图 -->  
  24.     <bean  
  25.         class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  26.         <property name="prefix" value="/WEB-INF/views/"></property>  
  27.         <property name="suffix" value=".jsp"></property>  
  28.     </bean>  
  29. </beans>  

测试类

[java] view plain copy
  1. package com.action.room;  
  2. import org.springframework.stereotype.Controller;  
  3. import org.springframework.web.bind.annotation.RequestMapping;  
  4. import org.springframework.web.bind.annotation.ResponseBody;  
  5.   
  6. import com.state.Ram;  
  7.    
  8. /** 
  9.  *  
  10.  * @author Allen 2017年4月10日 
  11.  * 
  12.  */  
  13. @Controller  
  14. public class room {  
  15.   
  16.    
  17.     @ResponseBody  
  18.     @RequestMapping("/roomsize")  
  19.     public String hello() {   
  20.         return "当前房间数: "+Ram.roomHm.size() ;  
  21.     }  
  22.   
  23. }  

测试

 重新发布重新启动red5 server
访问:http://127.0.0.1:5080/liveOnline/roomsize
返回结果

再访问 http://127.0.0.1:5080/demos/publisher.html
按照上面描述的方法,开启一个rtmp直播在liveOnline


我们再刷新roomsize发现房间数显示为1,然后我们关闭直播再刷新发现房间数返回为0。

结束

原文地址:https://blog.youkuaiyun.com/crazyzxljing0621/article/details/69568339

如果用ffpeg去解析生成的flv文件和用手机端直播的可以参考 : https://blog.youkuaiyun.com/qq_33730348/article/details/79931703

swfobject参数详解:https://blog.youkuaiyun.com/qq_33730348/article/details/79931773

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值