libevent (hello-world代码源码)----event_base_new

libevent (hello-world代码源码)----evconnlistener、event中看了evconnlistener、event的定义,现在我们知道evconnlistener是用来描述tcp服务端的结构体,event结构里有3个event_callback,分别对应超时、io和信号事件。在libevent (hello-world代码源码)----event_base中可以知道event是event_base结构体管理的事件单位。

event_base管理一堆超时、io和信号触发的event事件。

sockaddr_in 

让我们来看第四行:

struct sockaddr_in sin;

创建一个 描述网络连接参数的结构体,里面包含ip、端口、协议信息。

struct sockaddr_in
 
{
 
   short sin_family;/*Address family一般来说AF_INET(地址族)PF_INET(协议族)*/
 
   unsigned short sin_port;/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/
 
   struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/
 
   unsigned char sin_zero[8];/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/
 
};

WSAStartup 

然后下面是:

#ifdef _WIN32
	WSADATA wsa_data;
	WSAStartup(0x0201, &wsa_data);
#endif

其中#ifdef _WIN32由编译器(ml.exe/ml64.exe)内部定义的。具体描述是:

  _WIN32:Defined for applications for Win32 and Win64. Always defined.

  _WIN64:Defined for applications for Win64.

windows平台上总是会有这种宏,所以用来判断是否是windows平台。

WSAStartup(0x0201, &wsa_data);

当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。为了在应用程序当中调用任何一个Winsock API,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务初始化。

所以这几行的意思就是如果是windows系统,初始化网络服务。(linux不需要)

event_base_new

接下来是创建新的event_base:

	base = event_base_new();
	if (!base) {
		fprintf(stderr, "Could not initialize libevent!\n");
		return 1;
	}

event.h

event_base_new:

/**
 * Create and return a new event_base to use with the rest of Libevent.
 *
 * @return a new event_base on success, or NULL on failure.
 *
 * @see event_base_free(), event_base_new_with_config()
创建并返回一个新的event_ base以与Libevent的其余部分一起使用。
@return一个新的event_ base,成功时返回,失败时返回NULL。
@see event_base_free(), event_base_new_with_config()
 */
EVENT2_EXPORT_SYMBOL
struct event_base *event_base_new(void);

失败时返回NULL,所以后面就是如果创建失败直接return 1,因为是main函数所以直接退出程序。

event_config:

/**
 * Configuration for an event_base.
 *
 * There are many options that can be used to alter the behavior and
 * implementation of an event_base.  To avoid having to pass them all in a
 * complex many-argument constructor, we provide an abstract data type
 * where you set up configuration information before passing it to
 * event_base_new_with_config().
 *
 * @see event_config_new(), event_config_free(), event_base_new_with_config(),
 *   event_config_avoid_method(), event_config_require_features(),
 *   event_config_set_flag(), event_config_set_num_cpus_hint()
 */
struct event_config
#ifdef EVENT_IN_DOXYGEN_
{/*Empty body so that doxygen will generate documentation here.*/}
#endif
;

event_base的配置:

有许多选项可用于更改event_ base的行为和实现。为了避免在复杂的多参数构造函数中传递它们,我们提供了一种抽象数据类型,您可以在其中设置配置信息,然后再将其传递给event_base_new_with_config()。

 * @see event_config_new(), event_config_free(), event_base_new_with_config(),
 *   event_config_avoid_method(), event_config_require_features(),
 *   event_config_set_flag(), event_config_set_num_cpus_hint()

相关函数有点多,先不看了,应该都是改配置相关的吧。

event-internal.h

event_config:

/** Internal structure: describes the configuration we want for an event_base
 * that we're about to allocate. */
struct event_config {
//这个队列中存放的是config中需要避免的IO多路复用模型
	TAILQ_HEAD(event_configq, event_config_entry) entries;
//CPU的个数,仅仅在win下有用
	int n_cpus_hint;

// 检查两次新事件之间的最大时间间隔
	struct timeval max_dispatch_interval;
// 两次检查新事件之间的执行回调函数个数的最大个数
	int max_dispatch_callbacks;
//任务的优先级限制
	int limit_callbacks_after_prio;
	enum event_method_feature require_features;//多路IO复用函数应该满足哪些特征
	enum event_base_config_flag flags;
};

 内部结构:描述我们要分配的事件库的配置。其中flags:

  • EVENT_BASE_FLAG_NOLOCK:不要为event_base分配锁。设置这个选项可以为event_base节省一点加锁和解锁的时间,但是当多个线程访问event_base会变得不安全
  • EVENT_BASE_FLAG_IGNORE_ENV:选择多路IO复用函数时,不检测EVENT_*环境变量。使用这个标志要考虑清楚:因为这会使得用户更难调试程序与Libevent之间的交互
  • EVENT_BASE_FLAG_STARTUP_IOCP:仅用于Windows。这使得Libevent在启动时就启用任何必需的IOCP分发逻辑,而不是按需启用。如果设置了这个宏,那么evconn_listener_new和bufferevent_socket_new函数的内部将使用IOCP
  • EVENT_BASE_FLAG_NO_CACHE_TIME:在执行event_base_loop的时候没有cache时间。该函数的while循环会经常取系统时间,如果cache时间,那么就取cache的。如果没有的话,就只能通过系统提供的函数来获取系统时间。这将更耗时
  • EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST:告知Libevent,如果决定使用epoll这个多路IO复用函数,可以安全地使用更快的基于changelist 的多路IO复用函数:epoll-changelist多路IO复用可以在多路IO复用函数调用之间,同样的fd 多次修改其状态的情况下,避免不必要的系统调用。但是如果传递任何使用dup()或者其变体克隆的fd给Libevent,epoll-changelist多路IO复用函数会触发一个内核bug,导致不正确的结果。在不使用epoll这个多路IO复用函数的情况下,这个标志是没有效果的。也可以通过设置EVENT_EPOLL_USE_CHANGELIST 环境变量来打开epoll-changelist选项

event.c

event_base_new:

struct event_base *
event_base_new(void)
{
	struct event_base *base = NULL;
	struct event_config *cfg = event_config_new();
	if (cfg) {
		base = event_base_new_with_config(cfg);
		event_config_free(cfg);
	}
	return base;
}

可以看出先通过event_config_new()创建配置文件,(如果配置文件创建成功)通过配置文件创建event_base,然后释放配置配置文件内存。所以重点应该是通过配置文件创建event_base的event_base_new_with_config函数,不过这个函数有点长,还是先看看配置文件怎么创建的吧

event_config_new:

struct event_config *
event_config_new(void)
{//申请内存,并全置1
	struct event_config *cfg = mm_calloc(1, sizeof(*cfg));

	if (cfg == NULL)
		return (NULL);//内存申请失败,返回NULL
    //tailqueue最重要的特点就是在head中增加了一个指向末尾的指针,所以它能够直接在链表的尾部插入数据
	TAILQ_INIT(&cfg->entries);//初始化一个尾部队列
	cfg->max_dispatch_interval.tv_sec = -1;//检查两次新事件之间的最大时间间隔
	cfg->max_dispatch_callbacks = INT_MAX;//两次检查新事件之间的执行回调函数个数的最大个数
	cfg->limit_callbacks_after_prio = 1;//任务优先级限制,限制低优先级的任务执行

	return (cfg);
}

 然后,开始new一个event_base,event_base_new_with_config:

struct event_base *
event_base_new_with_config(const struct event_config *cfg)
{
	int i;
	struct event_base *base;
	int should_check_environment;
//如果是debug event_debug_mode_too_late 置1 干嘛的,不知道
#ifndef EVENT__DISABLE_DEBUG_MODE
	event_debug_mode_too_late = 1;
#endif
	//申请内存,自定义的内存管理函数,总之就是申请内存空间并初始化
	if ((base = mm_calloc(1, sizeof(struct event_base))) == NULL) {
		event_warn("%s: calloc", __func__);//打印错误日志
		return NULL;
	}

	if (cfg)
		base->flags = cfg->flags;//配置文件赋值
	//检查环境时间:可能系统时间发生了变化
	//先把flags和EVENT_BASE_FLAG_IGNORE_ENV按位与 然后和 cfg 与
	//EVENT_BASE_FLAG_IGNORE_ENV:选择多路IO复用函数时,不检测EVENT_*环境变量。
	//所以就是 当cfg合法并且 设置了EVENT_BASE_FLAG_IGNORE_ENV时 should_check_environment 置为0。
	should_check_environment =
	    !(cfg && (cfg->flags & EVENT_BASE_FLAG_IGNORE_ENV));

	{
		struct timeval tmp;

    // EVENT_BASE_FLAG_PRECISE_TIMER:通常情况下我们使用的是更快的计数器,如果设置了这个标志的话我们
    // 会使用更精确的时间
	//默认情况下flags采用的是EV_MONOT_PRECISE
	//设置了EVENT_BASE_FLAG_PRECISE_TIMER precise_time置1
		int precise_time =
		    cfg && (cfg->flags & EVENT_BASE_FLAG_PRECISE_TIMER);
		int flags;
		if (should_check_environment && !precise_time) {
			//获取环境变量的值
			precise_time = evutil_getenv_("EVENT_PRECISE_TIMER") != NULL;
			if (precise_time) {
				base->flags |= EVENT_BASE_FLAG_PRECISE_TIMER;
			}
		}
		flags = precise_time ? EV_MONOT_PRECISE : 0;
	// POSIX clock_gettime接口提供获得monotonic时间的方式。CLOCK_MONOTONIC基本上都是支持的;
    // linux也提供CLOCK_MONOTONIC_COARSE模式,大约1-4毫秒的准确性。
    // 所有平台上,CLOCK_MONOTONIC实际上是单调递增的。
	// 此函数最终的结果是获取CLOCK_MONOTONIC模式的时间,并将event_base的时钟模式设置为
    // CLOCK_MONOTONIC模式
	// flags默认为1
		evutil_configure_monotonic_time_(&base->monotonic_timer, flags);
	// 根据“base”将“tp”设置为当前时间。我们必须把锁锁在“base”上。如果存在缓存时间,则返回它。
	// 否则,使用clock_gettime或gettimeofday(视情况而定)查找正确的时间。
	// 成功时返回0,失败时返回1。
		gettime(base, &tmp);
	}

	//-----初始化参数-----

	//最小堆最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子节点和右子节点的值。
	//libevent的最小堆是通过数组的形式实现的,索引从0开始
	min_heap_ctor_(&base->timeheap);

	//signal.c中默认信号处理实现的数据结构
	//用于从信号处理程序发送通知的Socketpair
	base->sig.ev_signal_pair[0] = -1;
	base->sig.ev_signal_pair[1] = -1;
	//一种宽度足以容纳“socket()”或“accept()”输出的类型。在Windows上,这是一个intptr_;在其他地方,它是一个int。
	//一些th_ notify函数用来唤醒主线程的socketpair。
	base->th_notify_fd[0] = -1;
	base->th_notify_fd[1] = -1;
	//active_later_queue:尾部队列函数event_callbacks的列表,下次处理事件时应该激活,但这次不会。
	//初始化active_later_queue
	TAILQ_INIT(&base->active_later_queue);

	//io:从文件描述符到已启用(添加)事件的映射
	//初始化事件_映射以供使用
	evmap_io_initmap_(&base->io);
	//sigmap:从信号号到启用(添加)事件的映射
	//初始化sigmap
	evmap_signal_initmap_(&base->sigmap);
	//changelist:在下一次调度时告知后端的更改列表。仅由O(1)后端使用。
	//自上次调用eventop.dispatch以来的“更改”列表。仅在后端使用变更集时维护。
	//初始化changelist
	event_changelist_init_(&base->changelist);
	//指向后端特定数据的指针。
	base->evbase = NULL;

	//根据cfg配置初始化参数
	if (cfg) {
		//检查两次新事件之间的最大时间间隔
		memcpy(&base->max_dispatch_time,
		    &cfg->max_dispatch_interval, sizeof(struct timeval));
		//任务优先级限制,限制低优先级的任务执行
		base->limit_callbacks_after_prio =
		    cfg->limit_callbacks_after_prio;
	} else {
		base->max_dispatch_time.tv_sec = -1;
		base->limit_callbacks_after_prio = 1;
	}
	if (cfg && cfg->max_dispatch_callbacks >= 0) {
		//两次检查新事件之间的执行回调函数个数的最大个数
		base->max_dispatch_callbacks = cfg->max_dispatch_callbacks;
	} else {
		base->max_dispatch_callbacks = INT_MAX;
	}
	if (base->max_dispatch_callbacks == INT_MAX &&
	    base->max_dispatch_time.tv_sec == -1)
	    //如果执行回调函数个数的最大个数为INT_MAX,而且最大时间间隔为-1  ,任务优先级置为INT_MAX
		base->limit_callbacks_after_prio = INT_MAX;

	//按优先顺序排列的后端数组,确定系统环境
	//选择IO复用结构体
	for (i = 0; eventops[i] && !base->evbase; i++) {
		if (cfg != NULL) {
			/* determine if this backend should be avoided */
			//确定是否应避免使用此后端(IO复用)
			if (event_config_is_avoided_method(cfg,
				eventops[i]->name))//禁用哪种多路复用
				continue;
			if ((eventops[i]->features & cfg->require_features)
			    != cfg->require_features)//是否满足设置的特征
				continue;
		}

		/* also obey the environment variables */
		//也 服从环境变量
		if (should_check_environment &&
		    event_is_method_disabled(eventops[i]->name))
			continue;
		//找到一个满足条件的IO多路复用
		//函数指针和其他数据来描述这个event_base的后端(系统环境)
		base->evsel = eventops[i];
		//指向后端特定数据的指针
		//初始化ev_base,并且会对信号监听的处理也进行初始化
		base->evbase = base->evsel->init(base);
	}

	if (base->evbase == NULL) {
		event_warnx("%s: no event mechanism available",
		    __func__);
		base->evsel = NULL;
		event_base_free(base);
		return NULL;
	}

	if (evutil_getenv_("EVENT_SHOW_METHOD"))
		event_msgx("libevent using: %s", base->evsel->name);

	/* allocate a single active event queue */
	//llocate单个活动事件队列
	if (event_base_priority_init(base, 1) < 0) {
		event_base_free(base);
		return NULL;
	}

	/* prepare for threading */
	//准备线程
/*
libevent关于多线程的使用需要在所有的初始化之前加evthread_use_pthreads()函数的原因:
evthread_use_pthreads()定义在evthread_pthread.c里面。
在这个函数里,初始化了一个evthread_lock_callbacks对象 cbs,
然后调用evthread_set_lock_callbacks(&cbs);来的对cbs这个evthread_lock_callbacks对象做操作。
evthread_set_lock_callbacks定义在evthread.c里面。
在这个函数里,其实就是将cbs的值赋值给了全局变量_evthread_lock_fns。

在定义了_EVENT_DISABLE_THREAD_SUPPORT的情况下
在add_event函数里面,libevent调用了EVBASE_ACQUIRE_LOCK这个宏。
这个宏定义在evthread-internal.h, 同时EVBASE_ACQUIRE_LOCK这个宏又调用了EVLOCK_LOCK,
EVLOCK_LOCK又调用了全局变量_evthread_lock_fns的lock成员。这个_evthread_lock_fns就是之前说过的那个。
所以其实就是调用了evthread_use_pthreads()函数设置的_evthread_lock_fns这个结构体的lock成员。
而这个lock成员函数,根据evthread_use_pthreads()函数里面设置的值,就是evthread_posix_lock函数,其中参数mode是0,
参数_lock是base.th_base_lock。所以其实就是pthread_mutex_lock(base.th_base_lock)

在没有定义_EVENT_DISABLE_THREAD_SUPPORT的情况下
在add_event函数里面,libevent调用了EVBASE_ACQUIRE_LOCK这个宏。这个宏定义在evthread-internal.h,
 同时EVBASE_ACQUIRE_LOCK这个宏又调用了EVLOCK_LOCK,EVLOCK_LOCK又调用了函数_evthreadimpl_lock_lock(),
 参数mode是0,参数lock是base.th_base_lock。_evthreadimpl_lock_lock定义在evthread.c里面。
 在_evthreadimpl_lock_lock函数里面,会先判断全局变量_evthread_lock_fns的lock存不存在。
 如果存在就调用_evthread_lock_fns的lock成员,相当于就是调用evthread_posix_lock函数了,
 就和定义了_EVENT_DISABLE_THREAD_SUPPORT的情况一样了。如果不存在就什么都不干,返回0。
因为我的环境里面是没有定义_EVENT_DISABLE_THREAD_SUPPORT的,所以如果不在开始的时候调用evthread_use_pthreads(),
那么全局变量_evthread_lock_fns就没有被赋值,他的lock成员自然也就是NULL了。所以,EVBASE_ACQUIRE_LOCK宏其实什么都没干,
也就没有加锁,所以在多个线程里面add_event会乱掉
*/
#if !defined(EVENT__DISABLE_THREAD_SUPPORT) && !defined(EVENT__DISABLE_DEBUG_MODE)
	event_debug_created_threadable_ctx_ = 1;
#endif
//如果定义了EVENT__DISABLE_THREAD_SUPPORT 设置base->th_base_lock base->current_event_cond
#ifndef EVENT__DISABLE_THREAD_SUPPORT
	if (EVTHREAD_LOCKING_ENABLED() &&
	    (!cfg || !(cfg->flags & EVENT_BASE_FLAG_NOLOCK))) {
		int r;
		EVTHREAD_ALLOC_LOCK(base->th_base_lock, 0);
		EVTHREAD_ALLOC_COND(base->current_event_cond);
		r = evthread_make_base_notifiable(base);
		if (r<0) {
			event_warnx("%s: Unable to make base notifiable.", __func__);
			event_base_free(base);
			return NULL;
		}
	}
#endif

#ifdef _WIN32
	if (cfg && (cfg->flags & EVENT_BASE_FLAG_STARTUP_IOCP))
		event_base_start_iocp_(base, cfg->n_cpus_hint);
#endif

	return (base);
}

 似懂非懂,迷迷糊糊

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。著名分布式缓存软件memcached也是libevent based,而且libevent在使用上可以做到跨平台,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能。 编辑本段 详细   编译库代码,编译脚本会判断OS支持哪种类型的事件机制(select、epoll或kqueue),然后条件编译相应代码,供上层使用的接口仍然是保持统一的(否则也不能所谓的跨平台了)。在linux redhat as 4 u 2 上编译相当容易,configure以后make,make install就可以了,windows上编译似乎有点小麻烦,不过稍微改点东西也就通过了。   从代码中看,libevent支持用户使用三种类型的事件,分别是网络IO、定时器、信号三种,在定时器的实现上使用了RB tree的数据结构,以达到高效查找、排序、删除定时器的目的,网络IO上,主要关注了一下linux上的epoll(因为目前的开发主要在linux平台),结果发现libevent的epoll居然用的EPOLLLT,水平触发的方式用起来比较方便,不容易出错,但是在效率上可能比EPOLLET要低一些。   跟网络无关的,libevent也有一些缓冲区管理的函数,而且是c风格的函数,实用性不是太大。libevent没有提供缓存的函数。   虽然libevent实用上的价值不大,但它提供的接口形式还是不错的,实现类似的lib的时候仍然是可以参考的。   Libevent定时器的数据结构自version 1.4起已由红黑树改为最小堆(Min Heap),以提高效率;网络IO和信号的数据结构采用了双向链表(TAILQ)。在实现上主要有3种链表: EVLIST_INSERTED, EVLIST_ACTIVE, EVLIST_TIMEOUT,一个ev在这3种链表之间被插入或删除,处于EVLIST_ACTIVE链表中的ev最后将会被调度执行。   Libevent提供了DNS,HTTP Server,RPC等组件,HTTP Server可以说是Libevent的经典应用。从http.c可看到Libevent的很多标准写法。写非阻塞式的HTTP Server很容易将socket处理与HTTP协议处理纠缠在一起,Libevent在这点上似乎也有值得推敲的地方。
### 回答1: 《libevent参考手册(中文版)_libevent-2.1.5.pdf》是libevent事件驱动库的参考手册的中文翻译版本,主要是为中国的开发者提供方便和参考。libevent是一个开源的高性能网络库,广泛应用于服务器程序的开发中。 该手册详细介绍了libevent库的使用方法和相关的API函数,帮助开发者们更好地理解和使用这个库。手册的内容包括库的基本概述、事件循环、定时器、信号处理、缓冲区管理等各个方面。通过阅读手册,开发者可以学习如何使用libevent库来构建高效可靠的网络应用程序。 手册中的每个功能点都有详细的说明和示例代码,让开发者可以更快地上手使用libevent库。无论是初学者还是有一定经验的开发者,都可以通过这份参考手册快速入门和掌握libevent库的使用。 此外,手册还包含了常见问题的解答与技巧,帮助开发者在使用libevent时遇到问题时能够解决,提高开发效率。 总之,《libevent参考手册(中文版)_libevent-2.1.5.pdf》为开发者们提供了掌握libevent库的必备工具和参考资料。通过学习手册中的内容,开发者们可以更好地利用libevent库来实现高性能、高可靠性的网络应用程序。 ### 回答2: libevent是一个事件驱动的编程库,用于开发高效的网络通信应用程序。它提供了一个简单而灵活的接口,能够实现异步的、非阻塞的事件处理机制。 libevent参考手册中文版_libevent-2.1.5.pdf是libevent官方提供的参考手册的中文翻译版本。这个手册详细介绍了libevent库的各个方面,包括核心概念、函数接口、使用方法等。通过阅读这个手册,用户可以了解到如何使用libevent开发高效的网络应用程序。 这个手册首先介绍了libevent的设计理念和特点,包括事件驱动的编程模型、事件循环机制等。然后详细介绍了libevent库中的各个功能模块,包括事件基、事件、缓冲区、定时器等。对于每个功能模块,手册提供了对应的API接口说明和使用示例。 除了功能模块的介绍外,手册还包括了一些高级话题的讨论,例如多线程编程、信号处理、SSL支持等。这些内容对于开发复杂的网络应用程序非常有用。 总的来说,libevent参考手册中文版_libevent-2.1.5.pdf是libevent库的官方文档的中文翻译版本。通过阅读这个手册,用户可以全面了解libevent库的使用方法和原理,从而能够更好地利用libevent开发高效的网络通信应用程序。 ### 回答3: libevent是一个事件驱动的网络编程库,它简化了网络应用程序的开发过程。libevent支持多种操作系统平台,包括Windows、Linux和Unix,并提供了跨平台的API接口。 libevent参考手册中文版_libevent-2.1.5.pdf是libevent库的中文参考手册,它详细介绍了libevent库的各个功能和使用方法,对于开发人员来说是非常有用的工具。 这个参考手册主要包含以下几个部分: 1. 简介:介绍了libevent库的概要和功能特点,以及它在网络编程中的作用。 2. 安装和配置:详细说明了如何安装和配置libevent库,包括下载源代码、编译安装和设置环境变量等步骤。 3. API接口:列举了libevent库提供的各种API接口,并对每个接口进行了详细的说明。开发人员可以根据自己的需求选择合适的接口来实现网络应用程序。 4. 示例代码:提供了一些实用的示例代码,演示了如何使用libevent库来开发各种类型的网络应用程序,如服务器、客户端和代理等。 5. 常见问题解答:收集了一些常见的问题和解答,帮助开发人员在使用libevent库过程中遇到问题时能够及时获取帮助。 通过学习和参考libevent参考手册,开发人员可以更好地理解和掌握libevent库的使用方法,提高网络应用程序的开发效率。同时,中文版的参考手册方便了不擅长英文阅读的开发人员使用libevent库进行开发工作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值