12.Linux网络编程-POSIX线程

一:POSIX线程的优点
POSIX(可移植操作系统接口)线程是提高代码响应和性能的有力手段,体现在如下几点:
1)线程拥有并发处理能力
线程类似于进程。如同进程,线程由内核按时间分片进行管理。在单处理器系统中,内核使用时间分片来模拟线程的并发执行,这种方式和进程的相同。而在多处理器系统中,如同多个进程,线程实际上一样可以并发执行。
2)线程间的共享内存机制
对于大多数合作性任务,多线程比多个独立的进程更优越呢?这是因为,线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以,程序中的所有线程都可以读或写声明过的全局变量。如果用fork()创建多个进程,它会带来以下通信问题:如何让多个进程相互通信,这里每个进程都有各自独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信),但它们都遇到两个重要障碍:a.强加了某种形式的额外内核开销,从而降低性能;b.对于大多数情形,IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。
3)线程的开销小
与标准fork()相比,线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的CPU时间,使得线程创建比新进程创建快上十到一百倍。因为这一点,可以大量使用线程而无需太过于担心带来的 CPU或内存不足。使用 fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义,通常就可以创建线程。
4)线程的可移植性
__clone()类似于fork(),同时也有许多线程的特性。__clone调用是特定于Linux平台的,不适用于实现可移植的程序。如果想编写、可移植的、多线程代码,代码可运行于 Solaris、FreeBSD、Linux 和其它平台,POSIX 线程是一种当然之选。

二:POSIX线程函数
POSIX线程库函数绝大多数以“pthread_”打头的;要使用这些函数库,要引入头文件pthread.h;链接这些线程函数库时,要使用编译器命令的"-lpthread"的选项。

1)创建线程函数和线程属性函数
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
@pthread_create:创建线程(实际上就是确定调用该线程函数的入口点),在线程创建以后,就开始运行相关的线程函数;
@int:pthread_create的返回值,表示成功,返回0;表示出错,返回-1;
@thread:为指向线程标识符的指针;
@attr:用来设置线程属性,默认为NULL;
@void *:是线程运行函数的地址;
@arg:是运行函数的参数;

int pthread_attr_init(pthread_attr_t *attr);
@pthread_attr_init:初始化属性
@int:表示成功,返回0;表示出错,返回-1int pthread_attr_destory(pthread_attr_t *attr);
@pthread_attr_destory:销毁属性
@int:表示成功,返回0;表示出错,返回-1int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
@pthread_attr_getdetachstate:获取分离属性;

int pthread_attr_setdetachstate(const pthread_attr_t *attr, int *detachstate);
@pthread_attr_setdetachstate:设置分离属性;

int pthread_attr_setguardsize(const pthread_attr_t *attr, size_t guardsize);
@pthread_attr_setguardsize:设置栈溢出保护区大小

int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t guardsize);
@pthread_attr_getguardsize:获取栈溢出保护区大小

int pthread_attr_getscope(const pthread_attr_t *attr, int* contentionscope);
@pthread_attr_getscope:获取线程竞争范围

int pthread_attr_setscope(pthread_attr_t *attr, int* contentionscope);
@pthread_attr_setscope:设置线程竞争范围

int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int* policy);
@pthread_attr_getschedpolicy:获取线程调度策略

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int* policy);
@pthread_attr_setschedpolicy:设置线程调度策略

int pthread_attr_getinheritsched(const pthread_attr_t *attr, int* policy);
@pthread_attr_getinheritsched:获取继承调度策略

int pthread_attr_setsinheritsched(pthread_attr_t *attr, int* policy);
@pthread_attr_setsinheritsched:设置继承调度策略

int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param* param);
@pthread_attr_getschedparam:获取调度参数

int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param* param);
@pthread_attr_setschedparam:设置调度参数

2)线程合并和分离
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
@pthread_join:子线程和主线程合并,阻塞等待线程结束并回收资源函数;

int pthread_detach(pthread_t thread);
@pthread_detach:子线程和主线程分离,主线程不会等子线程结束再结束;

3)线程退出函数
#include <pthread.h>
void pthread_exit(void *retval);
@pthread_exit:终止调用它的线程并返回一个指向某个对象的指针;
@retval:pthread_exit函数唯一的参数value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr

int pthread_cancel(pthread_t thread);
@pthread_cancel:由其他线程来结束线程运行,而pthread_exit是自杀行为;
@int:函数成功完成之后会返回零,其他任何返回值都表示出现了错误;

4)线程互斥锁
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
@pthread_mutex_init:以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性;
@int:函数成功完成之后会返回零,其他任何返回值都表示出现了错误;
有两种定义互斥锁方式:
	a.静态定义:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
	b.动态定义:先定义变量:pthread_mutex_t mutex;,然后调用int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 函数进行初始化,通常,互斥锁属性mutexattr为NULL,表示使用默认的属性,即:快速互斥锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex)
@pthread_mutex_destroy:销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态;

int pthread_mutex_lock(pthread_mutex_t *mutex)
@pthread_mutex_lock:如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁.
@int:返回值: 成功则返回0, 出错则返回错误编号。

int pthread_mutex_trylock(pthread_mutex_t *mutex)
@pthread_mutex_trylock:非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态;
@int:返回值: 成功则返回0, 出错则返回错误编号。

int pthread_mutex_unlock(pthread_mutex_t *mutex)
@pthread_mutex_unlock:解锁;
@int:返回值: 成功则返回0, 出错则返回错误编号。

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
@pthread_attr_setstacksize:设置栈大小

int pthread_attr_getstacksize(pthread_attr_t *attr, size_t stacksize);
@pthread_attr_getstacksize:获取栈大小

5)线程信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
@sem_init:sem_init函数是Posix信号量操作中的函数。sem_init() 初始化一个定位在 sem 的匿名信号量;
@int:成功时返回 0;错误时,返回 -1,并把 errno 设置为合适的值
@sem :指向信号量对象
@pshared : 指明信号量的类型。不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享。
@value : 指定信号量值的大小

int sem_wait(sem_t *sem);
@sem_wait:sem_wait是一个函数,也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,将信号量的值将减到1
@int:成功返回0,错误的话信号量的值不改动,返回-1.errno设定来标识错误

int sem_trywait(sem_t *sem);
@sem_trywait:函数sem_trywait()sem_wait()有一点不同,即如果信号量的当前值为0,则返回错误而不是阻塞调用。错误值errno设置为EAGAIN。sem_trywait()其实是sem_wait()的非阻塞版本。
@int:成功返回0,错误的话信号量的值不改动,返回-1.errno设定来标识错误

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
@sem_timedwait:sem_timewait 函数会阻塞当前线程直到拿到锁或超时才会返回。阻塞的实现方式就是休眠当前线程,直到锁释放或者超时后唤醒。
@int:成功返回0,错误的话信号量的值不改动,返回-1.errno设定来标识错误

int sem_post(sem_t *sem);
@sem_post:sem_post是给信号量的值加上一个“1”,它是一个“原子操作”---即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同 时对同一个文件进行读和写操作的两个程序就有可能会引起冲突
@int:sem_post() 成功时返回 0;错误时,信号量的值没有更改,-1 被返回,并设置 errno 来指明错误

6)线程条件变量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
@pthread_cond_init:初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定。调用pthread_cond_init函数时,参数cattr为空指针等价于cattr中的属性为缺省属性,只是前者不需要cattr所占用的内存开销。这个函数返回时,条件变量被存放在参数cv指向的内存中。
@int:函数成功返回0;任何其他返回值都表示错误
有两种定义条件变量方式:
	a.静态定义:pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
	b.动态定义:pthread_cond_init()函数

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
@pthread_cond_wait:阻塞等待;
@int:函数成功返回0;任何其他返回值都表示错误

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
@pthread_cond_timedwait:计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEDOUT,结束等待
@int:函数成功返回0;任何其他返回值都表示错误

int pthread_cond_broadcast(pthread_cond_t *cond);
@pthread_cond_broadcast:唤醒所有正在pthread_cond_wait(&cond1,&mutex1)的线程;

int pthread_cond_signal(pthread_cond_t *cond);
@pthread_cond_signal:发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回;
@int:函数成功返回0;任何其他返回值都表示错误

7)线程ID
pthread_t pthread_self(void);
@pthread_self:显示线程ID;
@pthread_t:成功返回线程ID,失败返回-18)线程特定数据
创建或删除特定的数据;
int pthread_key_create(pthread_key_t* key, void (*destructor)(void*));
int pthread_key_delete(pthread_key_t* key);

9)设置或者获取特定数据中的值;
void* pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void* value);

10)只调用一次
int pthread_once(pthread_once_t* once_control, void(*)(void));
pthread_once_t once_control = PTHREAD_ONCE_INIT;

三:死锁
a.出现的环境:发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生
b.避免方法:
对共享资源操作前一定要获得锁;完成操作以后一定要释放锁;尽量短时间地占用锁;如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC;线程错误返回时应该释放它所获得的锁.
注意:pthread_mutex_trylock()与pthread_mutex_lock()类似,但是在锁已经被占据时返回EBUSY而不是挂起等待

四:创建线程和线程属性
线程创建后,默认是PTHREAD_CREATE_JOINABLE状态,即不可分离的,需要调用pthread_join()函数避免僵进程。因此可能出现主线程结束,但子线程还没有执行完。

//编译命令:g++ -o pthread pthreadCreate.cpp -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

void* thread_routine( void *ptr )
{
	for (int i=0; i<20; i++)
	{
		printf("B");
		fflush(stdout);
	}
}

int main(int argc, char** argv)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_routine, NULL) !=0)
		ERR_EXIT("pthread_create");
	
	//获取分离属性
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	int state;
	pthread_attr_getdetachstate(&attr, &state);
	if (state == PTHREAD_CREATE_JOINABLE)
		printf("detachstate:PTHREAD_CREATE_JOINABLE\n");
	else if (state == PTHREAD_CREATE_DETACHED)
		printf("detachstate:PTHREAD_CREATE_DETACHED\n");
	
	//获取栈大小
	size_t size;
	pthread_attr_getstacksize(&attr, &size);
	printf("stacksize:%d\n", size);
	
	//获取栈溢出保护区大小
	pthread_attr_getguardsize(&attr, &size);
	printf("guardsize:%d\n", size);
	
	for (int i=0; i<20; i++)
	{
		printf("A");
		fflush(stdout);
	}
	
	return 0;
}
//执行结果,发现没有打印出'B',那是因为主线程已经执行完毕了,程序就退出了,导致子线程没有被执行。
root@ubuntu:/home/share/eclipse_workspace/demo# ./pthread 
detachstate:PTHREAD_CREATE_JOINABLE
stacksize:8388608
guardsize:4096
AAAAAAAAAAAAAAAAAAAAroot@ubuntu:/home/share/eclipse_workspace/demo#

五:pthread_join
pthread_join() 将两个线程合并为一个线程。pthread_join() 的第一个参数是 tid mythread。第二个参数是指向 void 指针的指针。如果 void 指针不为 NULL,pthread_join 将线程的 void*返回值放置在指定的位置上。由于我们不必理会 thread_function() 的返回值,所以将其设为 NULL。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

void* thread_routine( void *ptr )
{
	for (int i=0; i<20; i++)
	{
		printf("B");
		fflush(stdout);
	}
}

int main(int argc, char** argv)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_routine, NULL) !=0)
		ERR_EXIT("pthread_create");
	for (int i=0; i<20; i++)
	{
		printf("A");
		fflush(stdout);
	}

	if (pthread_join(tid, NULL) !=0)
		ERR_EXIT("pthread_join");
	return 0;
}

//输出结果
root@ubuntu:/home/share/eclipse_workspace/demo# ./pthread 
AAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBroot@ubuntu:/home/share/eclipse_workspace/demo# 

六:pthread_detach
pthread_detach() 将子线程和主线程分离;

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

void* thread_routine( void *ptr )
{
	for (int i=0; i<20; i++)
	{
		printf("B");
		fflush(stdout);
	}
}

int main(int argc, char** argv)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_routine, NULL) !=0)
		ERR_EXIT("pthread_create");
	if (pthread_detach(tid) !=0)
		ERR_EXIT("pthread_detach");
	for (int i=0; i<20; i++)
	{
		printf("A");
		fflush(stdout);
	}
	return 0;
}

//输出结果
root@ubuntu:/home/share/eclipse_workspace/demo# ./pthread 
AAAAAAAAAAAAAAAAAAAAroot@ubuntu:/home/share/eclipse_workspace/demo# 

七:pthread_exit
pthread_exit函数唯一的参数value_ptr是函数的返回代码,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

void* thread_routine(void* ptr)
{
	const char* msgExit = "thread all done";
	pthread_exit ((void*)msgExit); // 重点看 pthread_exit() 的参数,是一个字串,这个参数的指针可以通过
}

int main()
{
	pthread_t tid;
	void *result;
	if (pthread_create(&tid, NULL, thread_routine, NULL) !=0)
		ERR_EXIT("pthread_create");

	if (pthread_join(tid, &result) !=0)
		ERR_EXIT("pthread_join");

	printf("pthread_join returns: %s\n",(char *)result);
	return 0;
}

//结果
root@ubuntu:/home/share/eclipse_workspace/demo# ./pthread 
pthread_join returns: thread all done

八:利用线程实现点对点的多客户端回射服务器
有客户端连接进来后,新开一个线程与客户端交互;

//server.cpp
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

void* echo_srv(void* ptr)
{
	int conn = *((int*)ptr);
    char recvbuf[1024];
	while (true)
	{
		memset(recvbuf, 0, sizeof(recvbuf));
		int ret = read(conn, recvbuf, sizeof(recvbuf));
		if (ret == 0)
			break;
		else if (ret == -1)
			ERR_EXIT("read");
		fputs(recvbuf, stdout);
		fflush(stdout);
		write(conn, recvbuf, ret);
	}
	close(conn);
	printf("client close, tid=%ld exit\n", pthread_self());
	pthread_exit(NULL);
}


int main(int argc, char** argv)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");

    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);

    while (true)
    {
    	int conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
        if (conn < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
        pthread_t tid;
        if (pthread_create(&tid, NULL, echo_srv, (void*)&conn) !=0)
        	ERR_EXIT("pthread_create");
    }
    close(listenfd);
    return 0;
}


//client.cpp
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);


void echo_cli(int sock)
{
    char sendbuf[1024] = {0};
	char recvbuf[1024] ={0};
	while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
	{
		write(sock, sendbuf, strlen(sendbuf));
		read(sock, recvbuf, sizeof(recvbuf));

		fputs(recvbuf, stdout);
		memset(sendbuf, 0, sizeof(sendbuf));
		memset(recvbuf, 0, sizeof(recvbuf));
	}
	close(sock);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    echo_cli(sock);

    return 0;
}

//结果:
root@ubuntu:/home/share/eclipse_workspace/demo# ./client 
dfdfdf
dfdfdf
^C
root@ubuntu:/home/share/eclipse_workspace/demo# 

root@ubuntu:/home/share/eclipse_workspace/demo# ./server 
ip=127.0.0.1 port=35428
hello
client close, tid=140405590841088 exit
ip=127.0.0.1 port=35430
dfdfdf
client close, tid=140405580261120 exit
^C
root@ubuntu:/home/share/eclipse_workspace/demo# 

九:线程特定数据
在多线程中,由于数据空间是共享的,因此全局变量也为所有线程所共有,但是当有必要提供线程私有的全局变量的需求时,应该怎么处理呢。POSIX线程库通过维护一定的数据结构来解决这个问题,这些数据被称为TSD(Thread-specific Data)。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define ERR_EXIT(m) do{ perror(m); exit(EXIT_FAILURE);}while(0);

pthread_key_t key_tsd;
typedef struct tsd
{
	pthread_t tid;
	char *str;
}tsd_t;


void* thread_routine(void *argv)
{
	tsd_t *value = (tsd_t*)malloc(sizeof(tsd_t));
	value->tid = pthread_self();
	value->str = (char*)argv;

	//设置特定的数据
	pthread_setspecific(key_tsd, value);
	printf("%s setpecific %p\n", (char*)argv, value);

	//取出特定数据里面的值
	value = (tsd_t *)pthread_getspecific(key_tsd);
	printf("tid=0x%x str=%s\n", (int)value->tid, value->str);

	//睡眠2秒,查看是否该线程的特定数据值被其他线程更改;
	sleep(2);
	value = (tsd_t *)pthread_getspecific(key_tsd);
	printf("tid=0x%x str=%s\n", (int)value->tid, value->str);
}

void destroy_routine(void *value)
{
	printf("tid=%ld destory...\n", pthread_self());
	if (value != NULL)
	{
		free(value);
		value = NULL;
	}
}

int main(int argc, char** argv)
{
	//线程退出会调用destory_routine函数
	pthread_key_create(&key_tsd, destroy_routine);

	pthread_t tid1, tid2;
	if (pthread_create(&tid1, NULL, thread_routine, (void*)"thread1") !=0)
		ERR_EXIT("pthread_create");
	if (pthread_create(&tid2, NULL, thread_routine, (void*)"thread2") !=0)
		ERR_EXIT("pthread_create");

	if (pthread_join(tid1, NULL) !=0 )
		ERR_EXIT("pthread_join");

	if (pthread_join(tid2, NULL) !=0 )
		ERR_EXIT("pthread_join");

	pthread_key_delete(key_tsd);
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值