套接字选项、getsockopt()、setsockpt()、ioctl()、fcntl()

一、套接字选项

1、getsockopt()、setsockopt()

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s,int level,int optname,void* optval,socklen_t* optlen);//获得套接字选项设置情况
int setsockopt(int s,int level,int optname,const void* optval,socklen_t optlen);//设置套接字选项
  • s:操作的套接字描述符,通过socket()获得
  • level:选项所在协议层
  • optname:选项名
  • optval:操作的内存缓冲区。getsockopt()指向获取返回选项值的缓冲区;setsockopt()指向设置的参数缓冲区
  • optlen():缓冲区大小

当对套接字选项进行操作时,必须给出选项所处的层和选项的名称。函数执行成功时返回0,错误时返回-1,errno如下:
在这里插入图片描述

2、套接字选项
按照参数选项级别的level不同,套接字选项大致可以分为3类:

  • 通用套接字选项:参数level值为SOL_SOCKET,用于设置或者获取通用的一些参数,例如接收和发送的缓冲区大小、地址重用等。
  • IP选项:参数level值为IPPROTO_IP,用于设置或者获取IP层的参数。例如选项名IP_HDRINCL表示在数据中包含IP头部数据,IP_TOS表示服务类型、IP_TTL表示存活时间等。
  • TCP选项:参数level的值为IPPROTO_TCP,用于获得或者设置TCP协议层的一些参数。例如选项名TCP_MAXRT对最大重传时间进行操作、选项名TCP_MAXSEG对最大分片大小进行操作、选项名TCP_KEEPALIVE对保持连接时间进行操作。

具体如下表:
套接字选项
3、示例

#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
/*结构保存获取结果*/
union Optval {
	int 			val;			/*整型值*/
	struct linger 	linger;		/*linger结构*/
	struct timeval 	tv;			/*时间结构*/
	unsigned char 	str[16];	/*字符串*/
};
static Optval optval;				/*用于保存数值*/
/*数值类型*/
enum Valtype{
	VALINT,				/*int类型*/
	VALLINGER,			/*struct linger类型*/
	VALTIMEVAL,			/*struct timeval类型*/
	VALUCHAR,			/*字符串*/
	VALMAX				/*错误类型*/
};
/*用于保存套接字选项的结构*/
struct sopts{
	int level;				/*套接字选项级别*/
	int optname;			/*套接字选项名称*/
	char *name;				/*套接字名称*/
	Valtype valtype;		/*套接字返回参数类型*/
};

sopts sockopts[] = {
	{SOL_SOCKET, 	SO_BROADCAST,		"SO_BROADCAST",			VALINT},
	{SOL_SOCKET, 	SO_DEBUG,			"SO_DEBUG",				VALINT},
	{SOL_SOCKET, 	SO_DONTROUTE,		"SO_DONTROUTE",			VALINT},
	{SOL_SOCKET, 	SO_ERROR,			"SO_ERROR",				VALINT},
	{SOL_SOCKET, 	SO_KEEPALIVE,		"SO_KEEPALIVE",			VALINT},
	{SOL_SOCKET, 	SO_LINGER,			"SO_LINGER",			VALINT},
	{SOL_SOCKET, 	SO_OOBINLINE,		"SO_OOBINLINE",		VALINT},
	{SOL_SOCKET, 	SO_RCVBUF,			"SO_RCVBUF",		VALINT},
	{SOL_SOCKET, 	SO_RCVLOWAT,		"SO_RCVLOWAT",		VALINT},
	{SOL_SOCKET, 	SO_RCVTIMEO,		"SO_RCVTIMEO",		VALTIMEVAL},
	{SOL_SOCKET, 	SO_SNDTIMEO,		"SO_SNDTIMEO",		VALTIMEVAL},
	{SOL_SOCKET, 	SO_TYPE,			"SO_TYPE",			VALINT},
	{IPPROTO_IP, 	IP_HDRINCL,			"IP_HDRINCL",		VALINT},
	{IPPROTO_IP, 	IP_OPTIONS,			"IP_OPTIONS",		VALINT},
	{IPPROTO_IP, 	IP_TOS,				"IP_TOS",			VALINT},
	{IPPROTO_IP, 	IP_TTL,				"IP_TTL",			VALINT},
	{IPPROTO_IP, 	IP_MULTICAST_TTL,	"IP_MULTICAST_TTL",	VALUCHAR},
	{IPPROTO_IP, 	IP_MULTICAST_LOOP,	"IP_MULTICAST_LOOP",VALUCHAR},
	{IPPROTO_TCP, 	TCP_KEEPCNT,		"TCP_KEEPCNT",		VALINT},
	{IPPROTO_TCP, 	TCP_MAXSEG,			"TCP_MAXSEG",		VALINT},
	{IPPROTO_TCP, 	TCP_NODELAY,		"TCP_NODELAY",		VALINT},
	/*结尾,主程序中判断VALMAX*/
	{0,				0,					NULL,				VALMAX}
};
/*显示查询结果*/
static void disp_outcome(sopts *sockopt, int len, int err)
{
	if(err == -1){							/*错误*/
		printf("optname %s NOT support\n",sockopt->name);
		return;	
	}
	
	switch(sockopt->valtype){				/*根据不同的类型进行信息打印*/
		case VALINT:						/*整型*/
			printf("optname %s: default is %d\n",sockopt->name,
			optval.val);
			break;
		case VALLINGER:/*struct linger*/
			printf("optname %s: default is  %d(ON/OFF), %d to linger\n",
				sockopt->name,			/*名称*/
				optval.linger.l_onoff,		/*linger打开*/
				optval.linger.l_linger);	/*延时时间*/
			break;
		case VALTIMEVAL:					/*struct timeval结构*/
			printf("optname %s: default is %.06f\n",
				sockopt->name,			/*名称*/
										/*浮点型结果*/
				((((double)optval.tv.tv_sec*100000+(double)optval.tv.tv_usec))/(double)1000000));
			break;
		case VALUCHAR:					/*字符串类型,循环打印*/
			{
				int i = 0;
				printf("optname %s: default is ",sockopt->name);
				/*选项名称*/
				for(i = 0; i < len; i++){
					printf("%02x ", optval.str[i]);			
				}
				printf("\n");					
			}
			break;
		default:
			break;
	}	
}
int main(int argc, char *argv[])
{
	int i = 0;
	int s = socket(AF_INET, SOCK_STREAM, 0);	/*建立一个流式套接字*/
	while(sockopts[i].valtype != VALMAX){		/*判断是否结尾,否则轮询执行*/
		socklen_t len = sizeof(sopts);					/*计算结构长度*/
		int err = getsockopt(s, sockopts->level, sockopts->optname, &optval,&len);									/*获取选项状态*/
		
		disp_outcome(&sockopts[i], len, err);	/*显示结果*/
		i++;/*递增*/
	}
	close(s);
	return 0;	
}

4、SOL_SOCKET协议族部分选项

  1. SO_BROADCAST广播选项
    用于进行广播设置,默认情况下系统的广播是禁止的。广播使用UDP套接字,此选项输入参数是int变量。输入为0时表示禁止广播,其他值表示允许广播。
#define YES 1
#define NO 0
int s = socket(AF_INET,SOCK_DGRAM,0);//建立套接字
int optval = YES;//选项
int err = setsockopt(s, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval));//设置选项
if(err)
	perror("setsockopt");
  1. SO_KEEPALIVE保持连接选项
    用于设置TCP连接的保持,设置此项后,连接会测试连接的状态。常用与可能长时间没有数据交流的连接,通常在服务端设置。如果2h内没有数据通信,TCP会自动发送一个活动探测数据报文,对方必须进行响应,通常有如下3种情况:

    • TCP的连接正常,发送ack响应。
    • 对方发送RST响应,对方在2h内进行了重启或崩溃,之前的连接已经失效,套接字收到一个ECONNRESET错误,之前套接字已经关闭。
    • 对方没有任何响应,则本机会发送另外8个活动探测报文,时间间隔为75s。仍未有任何响应,则放弃探测,套接字错误类型设置为ETIMEOUT,并关闭套接字连接。若收到ICMP控制报文响应(一般为主机不可达ICMP报文),也关闭连接,此时套接字错误类型设置为EHOSTUNREACH。

    不能实时地探测连接情况,可以探测空闲的连接,然后自动关闭服务器连接。

  2. SO_LINGER缓冲区处理方式选项
    用于设置TCP连接关闭时的方式,即关闭流式连接时, 发送缓冲区中的数据如何处理。

    Linux内核的默认处理方式是调用close()时,函数会立刻返回。在可能的情况下尽量发送缓冲区的数据,但不一定保证会发送剩余的数据。
    使用选项SO_LINGER可以阻塞close()函数直至剩余数据全部发送到对方。SO_LINGER的操作是通过结构linger来进行的结构定义如下:

struct linger
{
	int l_onoff;	//是否设置延时关闭
	int l_linger;	//超时时间
};
  • l_onoff的值为0,这时l_linger将会被忽略,即使用系统默认的关闭行为。
  • l_onoff的值为1,此时l_linger表示关闭连接的超时时间,非0时表示超时的秒数,会在超时之前发送所有未发送的数据。
  • l_onoff的值为1,l_linger的值设置为0,表示立刻关闭,发送缓冲区里面的剩余数据将被丢弃。
  1. SO_RCVBUF和SO_SNDBUF缓冲区大小选项
    用于操作发送缓冲区和接收缓冲区的大小,这两个选项在TCP和UDP连接中的含义有所不同。
    • 在UDP连接中,发送缓冲区在数据通过网络设备发送后就可以丢弃,不用保存。而接收缓冲区则需要保存数据直到应用程序读取。
    • 在TCP连接中,接收缓冲区大小就是滑动窗口大小。设置TCP接收缓冲区大小的时机很重要,是在建立连接时通过SYN获得的。对于客户端,接收缓冲区的大小要在调用connect()之前进行设置。而对于服务器程序要在listen()之前进行设置。

5、设置获取缓冲区大小示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
int main(int argc,char **argv)
{
	int err = -1;					/*返回值*/
	int s = -1;						/*socket描述符*/
	int snd_size = 0;   				/*发送缓冲区大小*/
	int rcv_size = 0;				/*接收缓冲区大小*/
	socklen_t optlen;				/*选项值长度*/
	/*
	 * 建立一个TCP套接字
	 */
	s = socket(PF_INET,SOCK_STREAM,0);
	if( s == -1){
		printf("建立套接字错误\n");
		return -1;	
	}
	
	/*
	 * 先读取缓冲区设置的情况
	 * 获得原始发送缓冲区大小
	 */
	optlen = sizeof(snd_size);
	err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen);
	if(err){
		printf("获取发送缓冲区大小错误\n");
	}  
		/*
	 * 获得原始接收缓冲区大小
	 */
	optlen = sizeof(rcv_size);
	err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
	if(err){
		printf("获取接收缓冲区大小错误\n");
	}
	/*
	 * 打印原始缓冲区设置情况
	 */
	printf(" 发送缓冲区原始大小为: %d 字节\n",snd_size);
	printf(" 接收缓冲区原始大小为: %d 字节\n",rcv_size);
	/*
	 * 设置发送缓冲区大小
	 */
	snd_size = 4096;					/*发送缓冲区大小为8K*/
	optlen = sizeof(snd_size);
	err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen);
	if(err){
		printf("设置发送缓冲区大小错误\n");
	}
	/*
	 * 设置接收缓冲区大小
	 */
	rcv_size = 8192;					/*接收缓冲区大小为8K*/
	optlen = sizeof(rcv_size);
	err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, &rcv_size, optlen);
	if(err){
		printf("设置接收缓冲区大小错误\n");
	}
	/*
	 * 检查上述缓冲区设置的情况
	 * 获得修改后发送缓冲区大小
	 */
	optlen = sizeof(snd_size);
	err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen);
	if(err){
		printf("获取发送缓冲区大小错误\n");
	}  
	/*
	 * 获得修改后接收缓冲区大小
	 */
	optlen = sizeof(rcv_size);
	err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
	if(err){
		printf("获取接收缓冲区大小错误\n");
	}
	/*
	 * 打印结果
	 */
	printf(" 发送缓冲区大小为: %d 字节\n",snd_size);
	printf(" 接收缓冲区大小为: %d 字节\n",rcv_size);
	close(s);
	return 0;
}

二、ioctl()

使用ioctl()函数与linux内核中的网络协议栈进行交互。

int ioctl(int d, int request, ...); 

1、命令选项
主要包含对套接字、文件、网络接口、地址解析协议(ARP)和路由等操作请求。
在这里插入图片描述在这里插入图片描述
2、ioctl()函数的IO请求
6个,第三个参数要求为一个执行整形数据的指针。

3、ioctl()函数的文件请求
请求命令都是FIOxxx类型,除了可以处理套接字外,对通用的文件系统也同样适用。
4、ioctl()函数的网络请求
网络接口常用的数据结构:

struct ifreq
{
#define IFHWADDRLEN 6	//网络接口硬件结构长度,即MAC长度
 union
 {
  char ifrn_name[IFNAMSIZ];  //网络接口名称,如eth0
 } ifr_ifrn;
 
 union {
  struct sockaddr ifru_addr;	//本地IP地址
  struct sockaddr ifru_dstaddr;	//目标IP地址
  struct sockaddr ifru_broadaddr;//广播IP地址
  struct sockaddr ifru_netmask;	//本地子网掩码地址
  struct  sockaddr ifru_hwaddr;	//本地MAC地址
  short ifru_flags;				//网络接口标记	
  int ifru_ivalue;				//值,不同请求含义不同
  int ifru_mtu;					//最大传输单元,mtu
  struct ifmap ifru_map;		//网卡地址映射
  char ifru_slave[IFNAMSIZ]; 	//占位符
  char ifru_newname[IFNAMSIZ];	//新名称
  void __user * ifru_data;		//用户数据
  struct if_settings ifru_settings;//设备协议设置
 } ifr_ifru;
};

#define ifr_name ifr_ifrn.ifrn_name 	//接口名称
#define ifr_hwaddr ifr_ifru.ifru_hwaddr 	//MAC地址
#define ifr_addr ifr_ifru.ifru_addr 	//本地IP地址
#define ifr_dstaddr ifr_ifru.ifru_dstaddr 	//p2p地址
#define ifr_broadaddr ifr_ifru.ifru_broadaddr 	//广播IP
#define ifr_netmask ifr_ifru.ifru_netmask 	//子网掩码
#define ifr_flags ifr_ifru.ifru_flags 		//标志
#define ifr_metric ifr_ifru.ifru_ivalue 	//接口侧度		
#define ifr_mtu  ifr_ifru.ifru_mtu 			//最大传输单元
#define ifr_map  ifr_ifru.ifru_map 			//设备地址映射
#define ifr_slave ifr_ifru.ifru_slave 		//副设备
#define ifr_data ifr_ifru.ifru_data 		//接口使用
#define ifr_ifindex ifr_ifru.ifru_ivalue 	//网络接口序号
#define ifr_bandwidth ifr_ifru.ifru_ivalue   //连接带宽
#define ifr_qlen ifr_ifru.ifru_ivalue 		//传输单元长度
#define ifr_newname ifr_ifru.ifru_newname 	//新名称
#define ifr_settings ifr_ifru.ifru_settings //设备协议设置

ifmap是网卡设备的映射属性:

struct ifmap
{
	unsigned long mem_start;	//开始地址
	unsigned long mem_end;		//结束地址
	unsigned short base_addr;	//基地址
	unsigned char irq;			//中断号
	unsigned char dma;			//DMA
	unsigned char port;			//端口
	//3字节空闲
};

网络的配置结构体 一块缓冲区,可以转换为结构ifreq来方便操作,用于读取网络接口的配置情况。

struct ifconf
{
	int ifc_len;	//缓冲区ifr_buf大小
	union
	{
		char __user *ifcu_buf;//缓冲区指针
		struct ifreq __user *ifcu_req;//指向结构ifreq的指针
	}ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf	//缓冲区地址的宏
#define ifc_req ifc_ifcu.ifcu_req	//结构ifc_req的宏

具体示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if_arp.h>
#include <string.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	int s;									/*套接字描述符*/
	int err = -1;							/*错误值*/
	/*建立一个数据报套接字*/
	s = socket(AF_INET, SOCK_DGRAM, 0);
	if (s < 0)	{
		printf("socket() 出错\n");
		return -1;
	}
	/*获得网络接口的名称*/
	{
		struct ifreq ifr;
		ifr.ifr_ifindex = 2; 				/*获取第2个网络接口的名称*/
		err = ioctl(s, SIOCGIFNAME, &ifr);
		if(err){
			printf("SIOCGIFNAME Error\n");
		}else{
			printf("the %dst interface is:%s\n",ifr.ifr_ifindex,ifr.ifr_name);
		}
	}
	/*获得网络接口配置参数*/
	{
	/*查询网卡“eth0”的情况*/
		struct ifreq ifr;
		memcpy(ifr.ifr_name, "eth0",5);
										/*获取标记*/
		err = ioctl(s, SIOCGIFFLAGS, &ifr);
		if(!err){
			printf("SIOCGIFFLAGS:%d\n",ifr.ifr_flags);
		}
		/*获取METRIC*/
		err = ioctl(s, SIOCGIFMETRIC, &ifr);
		if(!err){
			printf("SIOCGIFMETRIC:%d\n",ifr.ifr_metric);
		}
		/*获取MTU*/
		err = ioctl(s, SIOCGIFMTU, &ifr);
		if(!err){
			printf("SIOCGIFMTU:%d\n",ifr.ifr_mtu);
		}	

		/*获取MAC地址*/
		err = ioctl(s, SIOCGIFHWADDR, &ifr);
		if(!err){
			char *hw = ifr.ifr_hwaddr.sa_data;
			printf("SIOCGIFHWADDR:%02x:%02x:%02x:%02x:%02x:%02x\n",hw[0],hw[1],hw[2],hw[3],hw[4],hw[5]);
		}
		/*获取网卡映射参数*/
		err = ioctl(s, SIOCGIFMAP, &ifr);
		if(!err){
			printf("SIOCGIFMAP,mem_start:%ld,mem_end:%ld, base_addr:%d, irq:%d, dma:%d,port:%d\n",
				ifr.ifr_map.mem_start, 		/*开始地址*/
				ifr.ifr_map.mem_end,		/*结束地址*/
				ifr.ifr_map.base_addr,		/*基地址*/
				ifr.ifr_map.irq ,			/*中断*/
				ifr.ifr_map.dma ,			/*直接访问内存*/
				ifr.ifr_map.port );			/*端口*/
		}
		/*获取网卡序号*/
		err = ioctl(s, SIOCGIFINDEX, &ifr);
		if(!err){
			printf("SIOCGIFINDEX:%d\n",ifr.ifr_ifindex);
		}
		/*获取发送队列长度*/		
		err = ioctl(s, SIOCGIFTXQLEN, &ifr);
		if(!err){
			printf("SIOCGIFTXQLEN:%d\n",ifr.ifr_qlen);
		}			
	}
	/*获得网络接口IP地址*/
	{
		struct ifreq ifr;
										/*方便操作设置指向sockaddr_in的指针*/
		struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr;
		char ip[16];					/*保存IP地址字符串*/
		memset(ip, 0, 16);
		memcpy(ifr.ifr_name, "eth0",5);/*查询eth0*/
		
										/*查询本地IP地址*/		
		err = ioctl(s, SIOCGIFADDR, &ifr);
		if(!err){
										/*将整型转化为点分四段的字符串*/
			inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
			printf("SIOCGIFADDR:%s\n",ip);
		}
		
										/*查询广播IP地址*/
		err = ioctl(s, SIOCGIFBRDADDR, &ifr);
		if(!err){
										/*将整型转化为点分四段的字符串*/
			inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
			printf("SIOCGIFBRDADDR:%s\n",ip);
		}
		
										/*查询目的IP地址*/
		err = ioctl(s, SIOCGIFDSTADDR, &ifr);
		if(!err){
										/*将整型转化为点分四段的字符串*/
			inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
			printf("SIOCGIFDSTADDR:%s\n",ip);
		}
		
										/*查询子网掩码*/
		err = ioctl(s, SIOCGIFNETMASK, &ifr);
		if(!err){
										/*将整型转化为点分四段的字符串*/
			inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16 );
			printf("SIOCGIFNETMASK:%s\n",ip);
		}
	}
	/*测试更改IP地址*/
	{
		struct ifreq ifr;
		/*方便操作设置指向sockaddr_in的指针*/
		struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr;
		char ip[16];						/*保存IP地址字符串*/
		int err = -1;
		
		/*将本机IP地址设置为192.169.1.175*/
		printf("Set IP to 192.168.1.175\n");
		memset(&ifr, 0, sizeof(ifr));		/*初始化*/
		memcpy(ifr.ifr_name, "eth0",5);	/*对eth0网卡设置IP地址*/
		inet_pton(AF_INET, "192.168.1.175", &sin->sin_addr.s_addr);
											/*将字符串转换为网络字节序的整型*/
		sin->sin_family = AF_INET;			/*协议族*/
		err = ioctl(s, SIOCSIFADDR, &ifr);	/*发送设置本机IP地址请求命令*/
		if(err){							/*失败*/
			printf("SIOCSIFADDR error\n");
		}else{								/*成功,再读取一下进行确认*/
			printf("check IP --");
			memset(&ifr, 0, sizeof(ifr));	/*重新清零*/
			memcpy(ifr.ifr_name, "eth0",5);/*操作eth0*/
			ioctl(s, SIOCGIFADDR, &ifr);	/*读取*/
			inet_ntop(AF_INET, &sin->sin_addr.s_addr, ip, 16);
											/*将IP地址转换为字符串*/
			printf("%s\n",ip);/*打印*/
		}		
	}
	close(s);
	return 0;
}

5、ioctl()函数对ARP高速缓存操作

操作通过类型为arpreq的参数进行的。定义在文件<net/if_arp.h>中。

struct arpreq {
      struct sockaddr   arp_pa;     	//协议地址
      struct sockaddr   arp_ha;     	//硬件地址
      int               arp_flags;      //标志位
      struct sockaddr   arp_netmask;    //网络掩码(只用于代理ARP)
      char              arp_dev[16];    //对应的网络设备接口的名称。
    };
	#define ATF_COM         0x02        //查找已完成的地址 (成员ha有效,且含有正确的MAC地址)
    #define ATF_PERM        0x04        //永久性记录(邻居状态有NUD_PERMANENT)
    #define ATF_PUBL        0x08        //发布记录
    #define ATF_USETRAILERS 0x10        //使用扩展存档名称,不再使用
    #define ATF_NETMASK     0x20        //仅用于代理ARP。
    #define ATF_DONTPUB     0x40        //不回复
    #define ATF_MAGIC		0X80		//自动添加的邻居

三、fcntl()

int fcntl(int fd,int cmd,void arg);

对套接字进行操作的fcntl()函数有4种,如下:
在这里插入图片描述
可见对套接字的fcntl()使用ioctl()函数可以完全替代。

注:

LINUX网络编程 第二版 第十二章 读书笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值