Live555-基类研究二

1 live555源码分析(一)——live555编译与运行 一、live555概述 live555是一个基于C++开发的开源的流媒体服务解决方案,它实现了标准的流媒体传输协议(如RTP/RTCP、RTSP、SIP)。官网地址http://www.live555.com/。 二、live555源码下载 live555可以直接从官网下载,一直在维护更新,可以直接下载最新版http://www.live555.com/liveMedia/public/live555-latest.tar.gz。 三、live555编译 live555是跨平台的,本文以Linux平台进行分 阅读详情


TaskSecheduler类是一个任务调度器,它是整个Live555的任务调度中心,程序在任何时刻的任何动作,均由该类完成。其核心函数是SingleStep。Live555的任务主要分为Delayed Event、Socket Event以及Trigger Event。其类层次结构如下图所示:

 

从类图中可以看到,其类层次结构和第二节中的UsageEnvironment类极其相似。下面让我们对每个类进行一个简单的认识。

TaskSecheduler定义了一些接口,用来对各种任务进行管理。该类中最为重要的一个函数莫过于doEventLoop了,整个任务的调度便是通过调用该函数实现的。每一次循环,该函数都会执行如下的几步:

1.       首先处理IO事件。程序通过select函数选择那些已经准备好的IO文件描述符,对其进行读、写或者异常处理。与该任务相关的接口为setBackgroundHandling、disableBackgroundHandling以及moveSocketHandling。IO事件会在任务中反复执行。

2.       然后将处理触发事件。由于作者采用了一个机器字节中的位来保存触发事件的数量,所以触发事件的数量受机器限制。对X86系统则32个触发事件。这样做的好处是效率高,但缺点就是数量受限制。与该任务相关的接口为createEventTrigger、deleteEventTrigger和triggerEvent。事件一旦被触发后,将会立即删除,避免事件再次触发。

3.       最后将执行延迟任务。延迟任务保存在一个延迟队列中,通过时间来指定何时执行。有关延迟任务,我们放在后面的小节中来专门学习。延迟任务执行后也将从延迟队列事删除。

BasicTaskScheduler0实现了延迟任务和触发事件。触发事件是通过一个机器字长所能表示的位来处理的,在内部被定义为fTriggersAwaitingHandling,是int类型。从最高位开始保存。比如,假设系统是32位机,并且只有一个待触发事件,那么fTriggersAwaitingHandling的值为0x80000000(对应二进制为10000000 0000 0000 0000 0000 0000 0000)。该类中还保存了上一次触发事件的ID以及mask,作为下一次调度的起点。这样保证了调度程序能够有序的执行所有待触发事件,而不是每次都从头开始扫描。它们的关系如下图所示:

至于延迟任务的实现,我们放在单独的小节中来学习。

BasicTaskScheduler实现了最后一个任务,即IO事件和核心调度程序SingleStep。IO任务的实现也很简单,它被定义为一个双向循环链表,链表的节点为HandlerDiscriptor。其实现类为HandlerSet,该类实现了对链表的增删改查操作。与之对应的,作者还定义了一个迭代器类HandlerIterator,用于遍历链表。而调度程序则实现上面所提到的三步操作,来依次执行各类任务。现在给出该函数的实现(为了帮助你的理解,我添加了一些中文注释):

void BasicTaskScheduler::SingleStep(unsigned maxDelayTime) {
	fd_set readSet = fReadSet;
	fd_set writeSet = fWriteSet;
	fd_set exceptionSet = fExceptionSet;

	DelayInterval const& timeToDelay = fDelayQueue.timeToNextAlarm();
	struct timeval tv_timeToDelay;

	tv_timeToDelay.tv_sec = timeToDelay.seconds();
	tv_timeToDelay.tv_usec = timeToDelay.useconds();

	/**
	 * 一个太大的tv_sec将导致select函数失败
	 * 因此,确保它不大于一万秒,即11.5天
	 */
	const long MAX_TV_SEC = MILLION;

	if (tv_timeToDelay.tv_sec > MAX_TV_SEC) {
		tv_timeToDelay.tv_sec = MAX_TV_SEC;
	}

	/**
	 * 检查最大延迟时间是否大于一万秒
	 * 以及微秒数是否大于一百万微秒,即1秒
	 */
	if (maxDelayTime > 0 && (tv_timeToDelay.tv_sec > (long) maxDelayTime / MILLION
			|| (tv_timeToDelay.tv_sec == (long) maxDelayTime / MILLION
					&& tv_timeToDelay.tv_usec > (long) maxDelayTime % MILLION))) {
		tv_timeToDelay.tv_sec = maxDelayTime / MILLION;
		tv_timeToDelay.tv_usec = maxDelayTime % MILLION;
	}

	int selectResult = select(fMaxNumSockets, &readSet, &writeSet,
			&exceptionSet, &tv_timeToDelay);
	if (selectResult < 0) {
		if (errno != EINTR && errno != EAGAIN) {
			/**
			 * 哭了,错误
			 */
			internalError();
		}
	}

	/**
	 * 迭代器
	 */
	HandlerIterator iter(*fHandlers);
	HandlerDescriptor* handler;

	/**
	 * 找到上次执行后的处理程序队列中的下一个
	 * 这里先找到上次执行时的socket号
	 */
	if (fLastHandledSocketNum >= 0) {
		while ((handler = iter.next()) != NULL) {
			if (handler->socketNum == fLastHandledSocketNum)
				break;
		}
		/**
		 * 没有找到,可能已经被移除,重置延时队列
		 */
		if (handler == NULL) {
			fLastHandledSocketNum = -1;
			iter.reset();
		}
	}

	/**
	 * 从找到的handler开始,执行其下一个,不管其状态是什么,皆执行
	 * 当然,也可能是从队列头开始执行的
	 */
	while ((handler = iter.next()) != NULL) {
		int sock = handler->socketNum;
		int resultConditionSet = 0;

		/**
		 * 检查
		 */
		if (FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet))
			resultConditionSet |= SOCKET_READABLE;

		if (FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet))
			resultConditionSet |= SOCKET_WRITABLE;

		if (FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet))
			resultConditionSet |= SOCKET_EXCEPTION;

		if ((resultConditionSet & handler->conditionSet) != 0
				&& handler->handlerProc != NULL) {
			/**
			 * 保存sock号,调度程序下次将从该位置继续执行,下同
			 */
			fLastHandledSocketNum = sock;

			/**
			 * 调用事件处理函数
			 */
			(*handler->handlerProc)(handler->clientData, resultConditionSet);
			break;
		}
	}

	/**
	 * 我们没有调用处理程序
	 * 因此,从重再来一次
	 * 造成这样的原因可能是从上一次执行处理程序的位置向后没有找到任何可执行的处理程序了
	 * 于是从头开始寻找处理程序
	 */
	if (handler == NULL && fLastHandledSocketNum >= 0) {
		iter.reset();

		while ((handler = iter.next()) != NULL) {
			int sock = handler->socketNum;
			int resultConditionSet = 0;

			if (FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet))
				resultConditionSet |= SOCKET_READABLE;

			if (FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet))
				resultConditionSet |= SOCKET_WRITABLE;

			if (FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet))
				resultConditionSet |= SOCKET_EXCEPTION;

			if ((resultConditionSet & handler->conditionSet) != 0
					&& handler->handlerProc != NULL) {
				fLastHandledSocketNum = sock;

				(*handler->handlerProc)(handler->clientData, resultConditionSet);
				break;
			}
		}

		/**
		 * 依然没有找到任务何执行的handler
		 * 将其值置为-1
		 * 以告诉处理程序,下次应该从头开始寻找handler
		 */
		if (handler == NULL)
			fLastHandledSocketNum = -1;
	}

	/**
	 * 响应新触发的事件
	 */
	if (fTriggersAwaitingHandling != 0) {
		/**
		 * 首先检查是否只有一个待触发事件
		 */
		if (fTriggersAwaitingHandling == fLastUsedTriggerMask) {
			fTriggersAwaitingHandling = 0;
			if (fTriggeredEventHandlers[fLastUsedTriggerNum] != NULL) {
				/**
				 * 执行事件处理函数
				 */
				(*fTriggeredEventHandlers[fLastUsedTriggerNum])(
						fTriggeredEventClientDatas[fLastUsedTriggerNum]);
			}
		} else {
			/**
			 * 寻找待执行的触发事件
			 */
			unsigned i = fLastUsedTriggerNum;
			EventTriggerId mask = fLastUsedTriggerMask;

			do {
				i = (i + 1) % MAX_NUM_EVENT_TRIGGERS;
				mask >>= 1;
				if (mask == 0)
					mask = 0x80000000;

				if ((fTriggersAwaitingHandling & mask) != 0) {
					fTriggersAwaitingHandling &= ~mask;
					if (fTriggeredEventHandlers[i] != NULL) {
						/**
						 * 响应事件
						 */
						(*fTriggeredEventHandlers[i])(
								fTriggeredEventClientDatas[i]);
					}

					fLastUsedTriggerMask = mask;
					fLastUsedTriggerNum = i;
					break;
				}
			} while (i != fLastUsedTriggerNum);
		}
	}

	/**
	 * 执行一个最迫切的延迟任务
	 */
	fDelayQueue.handleAlarm();
}

好了,到目前为止,我们已经了解了这三个类的作用及其工作原理,下面让我们来写一段测试代码,分别对三类任务进行测试。下面给出我的实现:

/*
 * TestTaskScheduler.cpp
 *
 *  Created on: 2012-4-10
 *      Author: lovey599
 *
 *  通过以下测试程序,我们可以看到:
 *  	除了IO任务以外的其它任务
 *  	执行后将从队列中删除
 *  	不再执行
 *  通过以下测试代码,你是否对doEventLoop函数有了更加清晰的认识?
 *  对整个框架的启动机制有了清晰的了解?
 *  同时,我们也可以看到,调度中心就是一个永真循环,并未对多线程提供更多的支持
 */

#include <iostream>

#include "BasicUsageEnvironment.hh"

using namespace std;

/**
 * 后台IO任务处理函数(包括socket)
 * 此处将数据输出到控制台
 */
void taskFun(void* clientData, int mask) {
	/**
	 * 检查
	 */
	do {
		if (mask & SOCKET_EXCEPTION) {
			cout << "IO Event:Oh,my god!" << endl;
			break;
		}

		if (mask & SOCKET_WRITABLE) {
			cout << "IO Event(Writable):" <<  (char*) clientData << endl;
		}

		if (mask & SOCKET_READABLE) {
			cout << "IO Event(Readable):" <<  (char*) clientData << endl;
		}

		sleep(1);
	} while (0);
}

/**
 * 触发事件回调函数
 */
void eventFun(void* clientData) {
	cout << "Event Trigger:" << (char*) clientData << endl;
	sleep(1);
}

/**
 * 延迟任务回调函数
 */
void delayedFun(void* clientData) {
	cout << "Delayed Task:" << (char*) clientData << endl;
	sleep(1);
}

int main(int argc, char* argv[]) {
	TaskScheduler* scheduler = BasicTaskScheduler::createNew();
	UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);

	char taskFunData[] = "do task...";

	/**
	 * 触发事件测试
	 */
	EventTriggerId id = scheduler->createEventTrigger(&eventFun);

	if ((id & ~0) == 0) {
		cout << "Create Event Trigger Failed.\n";
	} else {
		/**
		 * 将其加入调度队列
		 * 不要被名字迷惑了
		 * 这里仅仅是使其成为可调度状态,并不是立即执行
		 * 以便doEventLoop()执行它
		 */
		scheduler->triggerEvent(id, (void*) taskFunData);
	}

	/**
	 * 测试后台IO任务
	 */
	scheduler->setBackgroundHandling(STDOUT_FILENO, SOCKET_WRITABLE, &taskFun,
			(void*) taskFunData);

	/**
	 * 延迟任务测试
	 * 延迟5秒后执行
	 */
	scheduler->scheduleDelayedTask(5000000, delayedFun, (void*) taskFunData);

	/**
	 * 启动任务调度
	 */
	env->taskScheduler().doEventLoop();

	return 0;
}


live555 的学习使用 1. 下载及编译 参考:http://www.cnblogs.com/skyseraph/archive/2012/04/11/2442840.html 本文在VS2010中创建4个lib工程,编译。要点: (1) 4个工程都要添加C/C++包含目录: BasicUsageEnvironment\include; groupsock\include; UsageEnvironment\ 阅读详情

相关推荐

FFmpeg入门详解之92:Live555学习之(一)-------Live555的基本介绍

LIVE555下包含LiveMedia、UsageEnvironment、BasicUsageEnvironment、GroupSock库,MediaServer简单服务器程序以及其他多个测试demo。

福优学苑@音视频+流媒体 2942

Live555-SingleStep函数分析

Live555-SingleStep函数分析

xiangxiang2017的博客 917

1、Linux下搭建研究live555的开发环境

最近一样项目需要用到rtsp拉流,总体对比下来,还是觉得live555比较轻量级,所以决定使用live555作为rtsp服务。 live555众所周知,只能以文件的形式拉流,不能实时的塞进去buf数据,不能实时的直播。 但是可以改 就是为了改,决定研究一番。

TrueDei 1391

live555学习笔记【5】---消息循环

如果你真的有认真阅读live555的源码,你会发现,无论是它的服务器还是客户端,最终都会走到一个env->taskScheduler().doEventLoop()函数内,看名字应该就可以看出来,这是个消息循环处理函数。实际上,无论是客户端还是服务器,我们可以通过watchVariable变量来控制服务器或客户端的正常退出,否则它们永远都不会退出。 void BasicTaskSch...

王木木 1128

QT实现串口通信

要实现串口通信,需要知道串口通信需要的信息 主要参数有:波特率、校验位、数据位、停止位、控制流 主要操作有:串口的打开和关闭、刷新设备串口、接发数据、开关显示灯等。 实现效果如图: 最后附赠完整源码。 第一步:在头文件中引入 QtSerialPort 类的两个头文件(必须引入) // 引入串口通信的两个头文件(第一步) #include <QtSerialPort/QSerialPort> // 提供访问串口的功能 #include <QtSerialPo

m0_49456900的博客 6万+

Solidworks曲面建模学习(二)——3D草图

本文主要是演示有一些零件在solidworks中通过3D草来绘制会更加方便。

东东不熬夜的博客 9497

live555 singleStep 分析

最近看了看live555代码,看到这部分随手写下来做个记录 1. live555是用的select模型 2. 在处理函数里做了多种操作 处理各个socket上事件,回调函数         处理延迟队列,和队列上的事件 处理triggerEvent 个人见解: 感觉这个模型太累了,都是在一个线程里干,效率必定低 void BasicTaskScheduler::Single

occupy8的专栏 1642

AI 自动生成商业计划书:AIGC 如何助力创业者?

在创业的早期阶段,商业计划书(Business Plan)是至关重要的。它不仅用于吸引投资者、明确发展路径,还能帮助团队对业务模式、市场定位和财务预测有清晰认知。然而,撰写一份高质量的商业计划书往往需要投入大量时间和精力。如今,AIGC(AI Generated Content,人工智能生成内容)技术正在改变这一现状。借助 AIGC,创业者可以更快速、高效地创建商业计划书,并通过 AI 进行优化和数据分析。

2401_82808034的博客 3779

live555学习笔记3

三 消息循环 看服端的主体:live555MediaServer.cpp中的main()函数,可见其创建一个RTSPServer类实例后,即进入一个函数env-&gt;taskScheduler().doEventLoop()中,看名字很明显是一个消息循坏,执行到里面后不停地转圈,生名不息,转圈不止。那么在这个人生的圈圈中如何实现RTSP服务和RTP传输呢?别想那么远了,还是先看这个圈圈中实现了...

wodamazi 239

采购订单定价策略 超详细

‘需求’字段里填写的是一个例程编号(例程是一段判断程序),如果我们在需求中维护了例程编号,那只有这个例程条件被满足时,这个条件表才会被系统自动读取。一部分的条件表只有在凭证项目满足一定的条件时才会被读取。 ‘排斥的’字段如果别勾中的话,那表明如果这个条件表被系统成功读取到数据后,系统将停止这个存取顺序的执行,否则,系统将继续执行下去。 注意:a、如果我们给某个条件类型设定了有效期的话,那我们必须给这个条件类型分配一个存取顺序。 b、不能给一个抬头条件分配存取顺序。 是...

竹大的博客 1万+

香橙派5使用RK3588S内置NPU加速yolov5推理,实时识别数字达到50fps

香橙派5采用了RK3588S,内置的NPU达到了6Tops的算力,博主这里记录一下自己的踩坑过程,好不容易做出来的不能以后忘记了(手动狗头)。这里博主还在B站上发布了效果视频,大家感兴趣的话可以看一下。其中解决了先验框标注位置不对,或者出现非常多的框的问题

weixin_51651698的博客 2万+

x86_64平台SingleStep单步调试原理和示例

先看一个程序: // simple.c int value = 0; int main(int argc, char **argv) { value ++; value ++; value ++; value ++; value ++; value ++; } 现在想单步调试它,跟踪value的变化,如何来做? 用gdb即可。但是如果想要理解背后发生了什么,还是要手工来一遍效果才更佳。 这就需要理解单步跟踪的本质(Linux为例): x86_64体系结构,FLAGS寄存器有一个TF标志位来使

TCP/IP 4970

Halcon 形态学 开发案例 (开运算opening(先腐蚀再膨胀)、闭运算closing(先膨胀再腐蚀))

算子方法:功能:使用用户自定义的结构元素对区域进行开运算参数: Region(输入参数) :输入区域 StructElement(输入参数) :结构元素(区域) RegionOpening(输出参数) :开运算后的区域功能:使用圆形结构元素对区域进行开运算参数: Region(输入参数) :输入区域 RegionOpening(输出参数) :开运算后的区域 Radius(输入参数) :圆形结构元素的半径功能:使用矩形结构元素对区域进行开运算。

程序员,他们想的是什么?他们想的永远都是技术,他们崇尚的也永远都是技术。 1618

live555开发笔记(二):live555创建RTSP服务器源码剖析,创建rtsp服务器的基本流程总结

基于Live555的流媒体服务器方案源码剖析,了解基本的代码搭建步骤。

长沙红胖子 Qt・西安|省级科研团队从业背景,深耕双一流体系高精尖技术配套研发|剥离高精尖机理,沉淀高端特种行业领域 Qt 工程化落地经验|博文仅分享通用技术总结,输出公开普适工程思路。 1万+

Linux ARM平台开发系列讲解(GPIO子系统) 2.3.2 GPIO子系统常用函数解析

1. 概述 该章节会记录一年常用的GPIO子系统常用函数,会更具情况不断更新 static inline int gpio_get_value(unsigned int gpio) 功能 :用于获取某个GPIO引脚上的值 gpio : gpio引脚号 返回值:非负值,得到的 GPIO 值;负值,获取失败。 static inline void gpio_set_value(unsigned int gpio, int value) static inline int gpio_cansleep(unsi

DSMGUOGUO的博客 668

大唐杯历届省赛押题训练(6)

考试-TD-LTE无线网络设备介绍-TD-LTE网络架构一、单选题1TD-LTE的接入网E-UTRAN由下列哪些网元组成( )。0分/1分A、RNCB、BTSC、eNode BD、Node B提交答案:B正确答案:C考点TD-LTE网络架构解析暂无试题解析二、多选题1关于LTE网络整体结构,哪些说法是正确的( )0分/2分A、E-UTRAN用E-NodeB替代原有的RNC-NodeB结构B、E-NodeB间的接口为S1接口C、各网络节点之间的接口使用IP传输。

nimo观察者 633

live555搭建流媒体服务器1--live555概述

live555搭建流媒体服务器1--源码概述

TRTK 3491

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

LIVE555研究之三:LIVE555基础

LIVE555基础      LIVE555是为流媒体提供解决方案的跨平台C++开源项目。从今天起我们将正式开始深入LIVE555代码。 一、各库简要介绍     LIVE555下包含LiveMedia、UsageEnvironment、BasicUsageEnvironment、GroupSock库,MediaServer简单服务器程序以及其他多个测试demo。     LiveMedia库:包

ithzhang 1万+

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

开源流媒体项目 live555 简介

live555 是一个 C++ 开发的流媒体项目,它主要由几个用于多媒体流的库组成,其官方网站地址为 http://www.live555.com/。live555 使用开放的标准协议 (RTP/RTCP,RTSP,SIP),方便与其它标准的流媒体组件互操作。这些库可以为 Unix-like(包括 Linux 和 Mac OS X),Windows,和 QNX (及其它 POSIX 兼容系统)等系...

whatday的专栏 4533

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

live555学习(一)通读Makefile编译live555

live555学习(一)通读Makefile编译live555live555编译live555学习开源live555学习(一)通读Makefile编译live555前言live555简介下载live555live的编译生成MakefileMake的生成目标前言拿到一份开源代码,我们应该怎么学习呢,第一次接触的时候面对海量的源码,会有种找不着北的感觉吧。这里我会写下自己的学习过程,以及学习中遇...

weixin_30394633的博客 1439

系统学习live555

live555,音视频同步,RTSP,h.264,h.265

qq_45865950的博客 1889

基于live555的视频直播

基于live555的视频直播 一直很想做流媒体的直播,最近花时间看了有关live555的有关代码,这里隆重的推荐两篇:    http://blog.youkuaiyun.com/nkmnkm(道长的文章,分析的很不错)    http://blog.youkuaiyun.com/gavinr(这里面的文章容易让人理清思路) 其实刚入门的时候,看这两篇帖子也比较吃力,我们先看看live

海阔天空的专栏 8789

Live555研究之一 源代码编译

Live555学习之一 源码编译       Live555 是一个为流媒体提供解决方案的跨平台的C++开源项目,它实现了对标准流媒体传输协议如RTP/RTCP、RTSP、SIP等的支持。Live555实现了对多种音视频编码格式的音视频数据的流化、接收和处理等支持,包括MPEG、H.263+、DV、JPEG视频和多种音频编码。同时由于良好的设计,Live555非常容易扩展对其他格式的支持。目前,L

ithzhang 1万+

Live555研究之一mediaServer主循环

mediaServer工程中的live555MediaServer.cpp提供一个main函数,里面创建了一个DynamicRTSPServer对象,DynamicRTSPServer从RTSPServerSupportingHTTPStreaming派生下来,RTSPServerSupportingHTTPStreaming从RTSPServer派生下来,RTSPServerSupporting

鉴于的专栏 1339

live555介绍

Live555是一个开源的跨平台流媒体处理库,主要用于构建流媒体服务器、客户端和代理服务。它支持RTSP/RTP等主流协议及H.264、AAC等多种媒体格式,可实现媒体文件的流化传输、实时流采集转发和协议转换。其核心优势在于模块化设计、协议兼容性和高效性能,被广泛应用于网络摄像头、视频监控、在线直播等场景,同时作为流媒体技术研究和开发的基础平台。典型应用包括搭建RTSP点播服务器、openRTSP客户端录制及嵌入式流媒体解决方案等。

xgbing 655

浅析live555媒体库之入门编译使用

简介          Live555 是一个为流媒体提供解决方案的跨平台的C++开源项目,它实现了对标准流媒体传输是一个为流媒体提供解决方案的跨平台的C++开源项目,它实现了对标准流媒体传输协议如RTP/RTCP、RTSP、SIP等的支持。Live555实现了对多种音视频编码格式的音视频数据的流化、接收和处理等支持,包括MPEG、H.263+、DV、JPEG视频和多种音频编码。同时由于良好的设计

努力奋斗 3660
上一篇: WinSocket的select函数的用法(windows套接字比较研究)
下一篇: Java之多线程断点下载的实现
BIGBurning
博客等级 码龄12年 625粉丝 59原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值