Linux学习笔记——网络编程socket篇


前言

  • TCP/IP协议是Internet事实上的工业标准
    一共有四层

对于通信而言,每下降一层都需要加一个包头,每上升一次都需要解一次包头。

Socket 简介

Socket 是一个编程接口 是一种特殊的文件描述符 (everything in Unix is a file) 并不仅限于TCP/IP协议 面向连接 (Transmission Control Protocol - TCP/IP) 无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)

Socket类型

流式套接字(SOCK_STREAM)

提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。

数据报套接字(SOCK_DGRAM)

提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。

原始套接字(SOCK_RAW)

可以对较低层次协议如IP、ICMP直接访问。

IP地址与端口号

IP地址是Internet中主机的标识 Internet中的主机要与别的机器通信必须具有一个IP地址 IP地址为32位(IPv4)或者128位(IPv6) 每个数据包都必须携带目的IP地址和源IP地址,路由器依靠此信息为数据包选择路由 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。

为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别 TCP端口号与UDP端口号独立 端口号一般由IANA (Internet Assigned Numbers Authority) 管理

众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用) 注册端口:1024~49150 动态或私有端口:49151~65535

字节序

不同类型CPU的主机中,内存存储多字节整数序列有两种方法,称为主机字节序(HBO)

小端序(little-endian) - 低序字节存储在低地址 将低字节存储在起始地址,称为“Little-Endian”字节序,Intel、AMD等采用的是这种方式;

大端序(big-endian)- 高序字节存储在低地址 将高字节存储在起始地址,称为“Big-Endian”字节序,由ARM、Motorola等所采用

网络中传输的数据必须按网络字节序,即大端字节序

字节序转换函数

把给定系统所采用的字节序称为主机字节序。为了避免不同类别主机之间在数据交换时由于对于字节序的不同而导致的差错,引入了网络字节序。

主机字节序到网络字节序

u_long htonl (u_long hostlong);

u_short htons (u_short short);

网络字节序到主机字节序

u_long ntohl (u_long hostlong);

u_short ntohs (u_short short);

IP地址的转换

inet_aton( )

将strptr所指的字符串转换成32位的网络字节序二进制值
 

#include <arpa/inet.h>

int inet_aton(const char *strptr, struct in_addr *addrptr);

inet_addr( )

功能同上,返回转换后的地址。

 int_addr_t inet_addr(const char *strptr);

inet_ntoa( ) 将32位网络字节序二进制地址转换成点分十进制的字符串。

char *inet_ntoa(stuct in_addr inaddr);

inet_pton() 将IPV4/IPV6的地址转换成binary格式   

  int inet_pton(int af, const char *src, void *dst);

TCP

TCP协议特点

TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)

适用情况: 适合于对传输质量要求较高,以及传输大量数据的通信。 在需要可靠数据传输的场合,通常使用TCP协议 MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议

常用API

socket() 创建套接字

bind() 绑定本机地址和端口

connect() 建立连接

listen() 设置监听端口

accept() 接受TCP连接

recv(), read(), recvfrom() 数据接收

send(), write(), sendto() 数据发送

close(), shutdown() 关闭套接字

socket

int socket (int domain, int type, int protocol);

domain 是地址族

PF_INET  // internet 协议

PF_UNIX // unix internal协议

PF_NS      // Xerox NS协议

PF_IMPLINK  // Interface Message协议

type  // 套接字类型

SOCK_STREAM   // 流式套接字,对应TCP

SOCK_DGRAM    // 数据报套接字,对应UDP

SOCK_RAW         //  原始套接字

protocol 参数通常置为0

bind

int bind (int sockfd, struct sockaddr* addr, int addrLen);

sockfd 由socket() 调用返回。

addr 是指向 sockaddr_in 结构的指针,包含本机IP 地址和端口号

struct sockaddr_in 
    u_short sin_family // protocol family 
    u_short sin_port     // port number struct in_addr  
    sin_addr  //IP address (32-bits)

addrLen : sizeof (struct sockaddr_in)

地址结构的一般用法

1.定义一个struct sockaddr_in类型的变量并清空

 struct sockaddr_in myaddr; 
memset(&myaddr, 0, sizeof(myaddr));

2.填充地址信息

myaddr.sin_family = PF_INET; 
myaddr.sin_port = htons(8888); 
myaddr.sin_addr.s_addr = inet_addr(“192.168.1.100”);

3.将该变量强制转换为struct sockaddr类型在函数中使用

bind(listenfd, (struct sockaddr*)(&myaddr), sizeof(myaddr));

地址转换函数

unsigned long inet_addr(char *address);
address是以’\0’结尾的点分IPv4字符串。该函数返回32位的地址。如果字符串包含的不是合法的IP地址,则函数返回-1。例如:
struct in_addr addr;
addr.s_addr = inet_addr(" 192.168.1.100 ");


char* inet_ntoa(struct in_addr address);
address是IPv4地址结构,函数返回一指向包含点分IP地址的静态存储区字符指针。如果错误则函数返回NULL

listen

int listen (int sockfd, int backlog);

sockfd:监听连接的套接字

backlog

        指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求。         DoS(拒绝服务)攻击即利用了这个原理,非法的连接占用了全部的连接数,造成正常的连接请求被拒绝。

返回值: 0 或 -1

完成listen()调用后,socket变成了监听socket(listening socket).

accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
返回值:已建立好连接的套接字或-1
头文件
#include <sys/types.h>
#include <sys/socket.h>  
sockfd : 监听套接字 
addr : 对方地址
addrlen:地址长度

listen()和accept()是TCP服务器端使用的函数

connect()

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
返回值:0 或 -1
头文件:
#include <sys/types.h>
#include <sys/socket.h>  
sockfd : socket返回的文件描述符
serv_addr : 服务器端的地址信息  
addrlen : serv_addr的长度 

connect()是客户端使用的系统调用

send()

ssize_t  send(int  socket,  const  void  *buffer,  size_t  length, int flags);
返回值:
成功:实际发送的字节数
失败:-1, 并设置errno
头文件:
#include <sys/socket.h>
buffer : 发送缓冲区首地址
length : 发送的字节数
flags : 发送方式(通常为0)

recv()

ssize_t  recv(int  socket,  const  void  *buffer,  size_t  length, int flags);
返回值:
成功:实际接收的字节数
失败:-1, 并设置errno
头文件:
#include <sys/socket.h>
buffer : 发送缓冲区首地址
length : 发送的字节数
flags : 接收方式(通常为0)

read()/write()

ssize_t read(int fd, void *buf, size_t count);

ssize_t write(int fd, const void *buf, size_t count);

read()和write()经常会代替recv()和send(),通常情况下,看程序员的偏好

使用read()/write()和recv()/send()时最好统一

套接字的关闭

int close(int sockfd);
关闭双向通讯

int shutdown(int sockfd, int howto);

TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。 针对不同的howto,系统回采取不同的关闭方式。

howto = 0 关闭读通道,但是可以继续往套接字写数据。

howto = 1 和上面相反,关闭写通道。只能从套接字读取数据。

howto = 2 关闭读写通道,和close()一样

示例

服务器端

#include "net.h"

int main (void)
{

	int fd = -1;
	struct sockaddr_in sin;

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/*优化1: 让服务器程序能绑定在任意的IP上 */
#if 1
	sin.sin_addr.s_addr = htonl (INADDY_ANY);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	/*3. 调用listen()把主动套接字变成被动套接字 */
	if (listen (fd, BACKLOG) < 0) {
		perror ("listen");
		exit (1);
	}
	printf ("Server starting....OK!\n");
	int newfd = -1;
	/*4. 阻塞等待客户端连接请求 */
#if 0
	newfd = accept (fd, NULL, NULL);
	if (newfd < 0) {
		perror ("accept");
		exit (1);
	}
#else
	/*优化2:通过程序获取刚建立连接的socket的客户端的IP地址和端口号 */
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof (cin);
	if ((newfd = accept (fd, (struct sockaddr *) &cin, &addrlen)) < 0) {
		perror ("accept");
		exit (1);
	}

	char ipv4_addr[16];
	if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
		perror ("inet_ntop");
		exit (1);
	}

	printf ("Clinet(%s:%d) is connected!\n", ipv4_addr, ntons (cin.sin_port));

#endif
	/*5. 读写 */
	//..和newfd进行数据读写
	int ret = -1;
	char buf[BUFSIZ];
	while (1) {
		bzero (buf, BUFSIZ);
		do {
			ret = read (newfd, buf, BUFSIZ - 1);
		} while (ret < 0 && EINTR == errno);
		if (ret < 0) {

			perror ("read");
			exit (1);
		}
		if (!ret) {				//对方已经关闭
			break;
		}
		printf ("Receive data: %s\n", buf);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client is exiting!\n");
			break;
		}
	}
	close (newfd);

	close (fd);
	return 0;
}

客户端

#include "net.h"

int main (void)
{
	int fd = -1;
	struct sockaddr_in sin;
	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}
	/*2.连接服务器 */

	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号
#if 0
	sin.sin_addr.s_addr = inet_addr (SERV_IP_ADDR);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif

	if (connect (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("connect");
		exit (1);
	}

	printf ("Client staring...OK!\n");
	/*3. 读写数据 */
	char buf[BUFSIZ];
	int ret = -1;
	while (1) {
		bzero (buf, BUFSIZ);
		if (fgets (buf, BUFSIZ - 1, stdin) == NULL) {
			continue;
		}
		do {
			ret = write (fd, buf, strlen (buf));
		} while (ret < 0 && EINTR == errno);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client is exiting!\n");
			break;
		}
	}

	/*4.关闭套接字 */
	close (fd);
}

TCP多进程/多线程服务器

多进程示例

#include <pthread.h>
#include <signal.h>
#include "net.h"

void cli_data_handle (void *arg);

void sig_child_handle(int signo)
{
	if(SIGCHLD == signo) {
		waitpid(-1, NULL,  WNOHANG);
	}
}
int main (void)
{

	int fd = -1;
	struct sockaddr_in sin;
	
	signal(SIGCHLD, sig_child_handle);	

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*优化4: 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));


	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/*优化1: 让服务器程序能绑定在任意的IP上 */
#if 1
	sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	/*3. 调用listen()把主动套接字变成被动套接字 */
	if (listen (fd, BACKLOG) < 0) {
		perror ("listen");
		exit (1);
	}
	printf ("Server starting....OK!\n");
	int newfd = -1;
	/*4. 阻塞等待客户端连接请求 */
	
        struct sockaddr_in cin;
        socklen_t addrlen = sizeof (cin);
	while(1) {
		pid_t pid = -1;
		if ((newfd = accept (fd, (struct sockaddr *) &cin, &addrlen)) < 0) {
                        perror ("accept");
                        break;
                }
		/*创建一个子进程用于处理已建立连接的客户的交互数据*/
		if((pid = fork()) < 0) {
			perror("fork");
			break;
		}
		
		if(0 == pid) {  //子进程中
			close(fd);
			char ipv4_addr[16];
                
			if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
                        	perror ("inet_ntop");
                        	exit (1);
               	 	}

               	 	printf ("Clinet(%s:%d) is connected!\n", ipv4_addr, ntohs(cin.sin_port));	
			cli_data_handle(&newfd);		
			return 0;	
		
		} else { //实际上此处 pid >0, 父进程中 
			close(newfd);
		}
		

	}		


	close (fd);
	return 0;
}

void cli_data_handle (void *arg)
{
	int newfd = *(int *) arg;

	printf ("Child handling process: newfd =%d\n", newfd);

	//..和newfd进行数据读写
	int ret = -1;
	char buf[BUFSIZ];
	while (1) {
		bzero (buf, BUFSIZ);
		do {
			ret = read (newfd, buf, BUFSIZ - 1);
		} while (ret < 0 && EINTR == errno);
		if (ret < 0) {

			perror ("read");
			exit (1);
		}
		if (!ret) {				//对方已经关闭
			break;
		}
		printf ("Receive data: %s\n", buf);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client(fd=%d) is exiting!\n", newfd);
			break;
		}
	}
	close (newfd);

}

多线程示例

#include <pthread.h>
#include "net.h"

void cli_data_handle (void *arg);

int main (void)
{

	int fd = -1;
	struct sockaddr_in sin;

	/* 1. 创建socket fd */
	if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
		perror ("socket");
		exit (1);
	}

	/*优化4: 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));


	/*2. 绑定 */
	/*2.1 填充struct sockaddr_in结构体变量 */
	bzero (&sin, sizeof (sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons (SERV_PORT);	//网络字节序的端口号

	/*优化1: 让服务器程序能绑定在任意的IP上 */
#if 1
	sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
	if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
		perror ("inet_pton");
		exit (1);
	}
#endif
	/*2.2 绑定 */
	if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
		perror ("bind");
		exit (1);
	}

	/*3. 调用listen()把主动套接字变成被动套接字 */
	if (listen (fd, BACKLOG) < 0) {
		perror ("listen");
		exit (1);
	}
	printf ("Server starting....OK!\n");
	int newfd = -1;
	/*4. 阻塞等待客户端连接请求 */

/* 优化: 用多进程/多线程处理已经建立号连接的客户端数据 */
	pthread_t tid;

	struct sockaddr_in cin;
	socklen_t addrlen = sizeof (cin);

	while (1) {
		if ((newfd = accept (fd, (struct sockaddr *) &cin, &addrlen)) < 0) {
			perror ("accept");
			exit (1);
		}

		char ipv4_addr[16];
		if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
			perror ("inet_ntop");
			exit (1);
		}

		printf ("Clinet(%s:%d) is connected!\n", ipv4_addr, htons (cin.sin_port));

		pthread_create (&tid, NULL, (void *) cli_data_handle, (void *) &newfd);
	}

	close (fd);
	return 0;
}

void cli_data_handle (void *arg)
{
	int newfd = *(int *) arg;

	printf ("handler thread: newfd =%d\n", newfd);

	//..和newfd进行数据读写
	int ret = -1;
	char buf[BUFSIZ];
	while (1) {
		bzero (buf, BUFSIZ);
		do {
			ret = read (newfd, buf, BUFSIZ - 1);
		} while (ret < 0 && EINTR == errno);
		if (ret < 0) {

			perror ("read");
			exit (1);
		}
		if (!ret) {				//对方已经关闭
			break;
		}
		printf ("Receive data: %s\n", buf);

		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {	//用户输入了quit字符
			printf ("Client(fd=%d) is exiting!\n", newfd);
			break;
		}
	}
	close (newfd);

}

UDP

UDP协议特点

UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。

适用情况: 发送小尺寸数据(如对DNS服务器进行IP地址查询时) 在接收到数据,给出应答较困难的网络中使用UDP。(如:无线网络) 适合于广播/组播式通信中。 MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议 流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

sendto(),recvfrom()

ssize_t sendto(int socket, void *message, size_t length, int flags, struct sockaddr *dest_addr, socklen_t dest_len);

ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);

这两个函数一般在使用UDP协议时使用

服务器示例

#include "net.h"

int main(void)
{

	int fd = -1;
 	struct sockaddr_in sin;
        
	/* 1. 创建socket fd */
        if ((fd = socket (AF_INET, SOCK_DGRAM, 0)) < 0) { //udp程序
                perror ("socket");
                exit (1);
        }

	/* 2. 允许绑定地址快速重用 */
        int b_reuse = 1;
        setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

	        /*2. 绑定 */
        /*2.1 填充struct sockaddr_in结构体变量 */
        bzero (&sin, sizeof (sin));
        sin.sin_family = AF_INET;
        sin.sin_port = htons (SERV_PORT);       //网络字节序的端口号

        /* 让服务器程序能绑定在任意的IP上 */
#if 1
        sin.sin_addr.s_addr = htonl (INADDR_ANY);
#else
        if (inet_pton (AF_INET, SERV_IP_ADDR, (void *) &sin.sin_addr) != 1) {
                perror ("inet_pton");
                exit (1);
        }
#endif
        /*2.2 绑定 */
        if (bind (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
                perror ("bind");
                exit (1);
        }
	
	char buf[BUFSIZ];
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof(cin);
	printf("\nUDP server started!\n");
	while(1) {
		bzero(buf, BUFSIZ);
		if( recvfrom(fd, buf, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen ) < 0) {
			perror("recvfrom");
			continue;
		}
		
		 char ipv4_addr[16];
                if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin))) {
                                perror ("inet_ntop");
                                exit (1);
 	        }

		printf("Recived from(%s:%d), data:%s",ipv4_addr, ntohs(cin.sin_port), buf);
		
		if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) {  //用户输入了quit字符
                       printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));
                }

	}

	close(fd);

	return 0;
}

客户端示例

/*udp demo */
/* usage:
 * ./client serv_ip serv_port 
*/
#include "net.h"
void usage(char *s)
{
	printf("\nThis is udp demo!\n");
	printf("\nUsage:\n\t %s serv_ip serv_port",s);
	printf("\n\t serv_ip: udp server ip address");
	printf("\n\t serv_port: udp server port(serv_port > 5000)\n\n");
}

int main(int argc, char *argv[])
{
	int fd = -1;
	int port = SERV_PORT;
	
	port = atoi(argv[2]);
	if(port < 0 || (port >0 && port <= 5000)) {
		usage(argv[0]);
		exit(1);
	}
        struct sockaddr_in sin;
	if(argc !=3) {
		usage(argv[0]);
		exit(1);
	}        

	/* 1. 创建socket fd*/
        if( (fd = socket(AF_INET,SOCK_DGRAM, 0)) < 0) { //UDP编程
                perror("socket");
                exit(1);
        }

	/*2.1 填充struct sockaddr_in结构体变量 */
        bzero(&sin,sizeof(sin));

        sin.sin_family = AF_INET;
        sin.sin_port = htons(SERV_PORT); //网络字节序的端口号
#if 0
        sin.sin_addr.s_addr = inet_addr(argv[1]);
#else
        if( inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1) {
                perror("inet_pton");
                exit(1);
        }
#endif	
	printf("UDP client started!\n");
	char buf[BUFSIZ];
	while(1) {
		fprintf(stderr,"pls input string:");
		bzero(buf, BUFSIZ);
		if( fgets(buf, BUFSIZ-1, stdin) ==NULL) {
			perror("fgets");
			continue;
		}
		
		sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin)); 
		
		if( !strncasecmp(buf, QUIT_STR, strlen(QUIT_STR))) {  //用户输入了quit字符
                        printf("Client is exited!\n");
                        break;
                }
	
	}
	close(fd);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值