网络编程

一、网络基础

1.1 协议

就是一种规则

1.2 典型协议

应用层 常见的协议有HTTP协议,FTP协议。
传输层 常见协议有TCP/UDP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层(链路层) 常见协议有ARP协议、RARP协议。

HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
IP协议是因特网互联协议(Internet Protocol)
ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。

1.3 分层模型结构

1、OSI七层模型:物、数、网、传、会、表、应
2、TCP/IP四层模型:网(链路层/网络接口层)、网、传、应
在这里插入图片描述(1)物理层:硬件部分,为上层协议提供了一个传输数据的可靠物理媒体。
(2)数据链路层:把网络层传下来的数据报组装成帧。数据链路层的传输单位是帧。
(3)网络层:对数据包进行路由选择,建立连接、保持和终止连接。(IP)
(4)传输层:负责不同主机中两个进程(每个进程都有端口号)的通信,即端到端(两个端口之间)的通信。传输单位是报文段或用户数据报。(TCP/UDP)
(5)会话层:向表示层实体/用户进程提供建立连接并在连接上有序的传输数据。这是会话,也是建立同步。
(6)表示层:对上层信息进行变化,以保证一个主机应用层信息可以被另一个主机的应用层程序理解。
(7)应用层:为操作系统或网络应用程序提供访问网络服务的接口。

1.4 通信过程

两台计算机通过TCP/IP协议通讯的过程如下所示:在这里插入图片描述

1.5 网络传输流程:

数据在没有封装前是不能进行传递的,其实就是不知道目的地址。
需要先后经过应用层、传输层、网络层和链路层的封装后才能进行传输。我们需要做的是应用层的封装,后面三层的封装是内核完成的。

1.6 IP协议

版本:IPv4、IPv6
TTL:time to live 设置数据包在路由节点中的跳转上限,每经过一个路由节点该值-1,减为0的路由,有义务将该数据包丢弃。
源IP:32位—4字节。 192.168.1.108(点分十进制,人看的,string类型)进入网络后会转成二进制。
目的IP:32位—4字节。
IP地址:可以在网络环境中,唯一标识一台主机。

1.7 UDP协议

16位:源端口号 2^16 = 65536 (端口号最大只能取到65535.)
16位:目的端口号
端口号:可以在一台主机上唯一标识一个进程。
IP地址+端口号:可以在网络环境中唯一标识一个进程。

1.8 TCP协议

16位:源端口号 2^16 = 65536。
16位:目的端口号。
32位序号
32位确认序号
6个标志位
16位窗口大小
三次握手是由内核完成的,在用户端就是accept()和connect()函数成功执行并返回了。

1.9 网络应用层序设计模式

C/S模式:client-server模式,客户机-服务器模式。
B/S模式:browser-server模式,浏览器-服务器模式。

优缺点C/SB/S
优点缓存大量数据(端游)、协议选择更灵活、速度快安全性、跨平台、开发工作量小
缺点安全性、不能跨平台、开发工作量大不能缓存大量数据(页游)、必须严格遵守http协议

二、socket编程

2.1 预备知识

在通信过程中套接字一定是成对出现的。一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。

网络字节序:网络数据流采用大端字节序(高位在低地址,低位在高地址),但是计算机采用的是小端法,因此需要进行网络字节序和主机字节序的转换。

常用的四个转换函数:
	htonl()  本地-->网络(IP)这里不能用点分十进制写法,因为它的本质是字符串
	htons() 本地-->网络(Port)
	ntohl() 网络-->本地(IP)
	ntohs() 网络-->本地(Port)

IP地址转换函数

	int inet_pton(int af, const char* src, void* dst);  本地字节序(string IP) --> 网络字节序
		af:AF_INET(IPv4)、AF_INET6(IPv6)
		src:传入IP地址(点分十进制)
		dst:传出转换后的网络字节序的IP地址
		返回值:
			成功:1
			异常:0,说明src指向的不是一个有效的ip地址
			失败:-1
	
	const char* inet_ntop(int af, const void* src, char* dst socklen_t size);  网络字节序 --> 本地字节序(string IP)
		af:AF_INET(IPv4)、AF_INET6(IPv6)
		src:网络字节序IP地址
		dst:本地字节序(string IP)
		size:dst的大小
		返回值:
			成功:dst
			失败:NULLL

2.2 socket的分类

socket提供了流(stream)和数据报(datagram)两种通信机制,即流socket和数据报socket。

流socket基于TCP协议,是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。
数据报socket基于UDP协议,不需要建立和维持链接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但它的效率比较高。

2.3 socket函数

#include <sys/socket.h>

int socket(int domain, int type, int protocol); //创建一个套接字
	domain:AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX
	type:数据传输方式(套接字类型)SOCK_STREAM、SOCK_DGRAM
	protocol:传输协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP传输协议和UDP传输协议。
			传0表示系统会自动推演出使用什么协议,因为当使用SOCK_STREAM格式的套接字,必须使用TCP协议;反之必须使用UDP协议
	返回值:
		成功:新套接字所对应的文件描述符
		失败:-1

int bind(Int sockfd, const struct sockaddr* addr, socklen_t addrlen); //给socket绑定一个地址结构(IP+port)
	sockfd:socket函数返回值
		addr的初始化:
			#include <arpa/inet.h>
			struct sockaddr_in addr;
			addr.sin_family = AF_INET;
			addr.sin_port = htons(8888);
			addr.sin_addr.s_addr = hton(INADDR_ANY);
	addr:(struct sockaddr* )&addr
	addrlen:sizeof(addr)地址结构的大小
	返回值:
		成功:0
		失败:-1
	
int listen(int sockfd, int backlog); //设置同时与服务器建立连接的上限数值(同时进行三次握手的客户端数量)
	sockfd:socket函数返回值
	backlog:上限数值,最大128
	返回值:
		成功:0
		失败:-1

int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); //阻塞等待客户端建立链接,成功的话返回一个与客户端成功链接的socket文件描述符
	sockfd:socket函数返回值
	addr:传出参数,成功与服务器建立链接的那个客户端的地址结构(IP+port)
	addrlen:传入传出。入:addr的大小。出:客户端addr的实际大小。
	返回值:
		成功:能与服务器进行数据通信的socket对应的文件描述符
		失败:-1

Int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen); //使用现有的socket与服务器建立链接
	sockfd:socket函数返回值
		addr初始化:
			struct sockaddr_in srv_addr;	//服务器地址结构
			srv_addr.sin_family = AF_INET;
			srv_addr.sin_port = 9527;	//跟服务器bind时设定的port完全一致
			inet_pton(AF_INET, "服务器的IP地址", &srv_addr.sin_addr.s_addr);
	addr:传入参数。服务器的地址结构
	addrlen:服务器的地址结构的大小
	返回值:
		成功:0
		失败:-1
		
如果不使用bind绑定客户端地址结构,采用”隐式绑定“

2.4 TCP通信流程分析

server:	
	1.socket()			创建socket
	2.bind()			绑定服务器地址结构
	3.listen()			设置监听上限
	4.accept()			阻塞监听客户端链接
	5.read()			读socket获取客户端数据
	6.小写-->大写		toupper()
	7.write()	
	8.close()			
client:
	1.socket()			创建socket
	2.connect()			与服务器建立链接
	3.write()			写数据到socket
	4.read()			读转换后的数据
	5.显示读取结果
	6.close()

三、多进程、多线程并发服务器

3.1 多进程并发服务器

多进程并发服务器思路分析

1. Socket();		创建 监听套接字 lfd
2. Bind()	绑定地址结构 Strcut scokaddr_in addr;
3. Listen();	
4. while (1) {

	cfd = Accpet();			接收客户端连接请求。
	pid = fork();
	if (pid == 0){			子进程 read(cfd) --- 小-》大 --- write(cfd)

		close(lfd)		关闭用于建立连接的套接字 lfd

		read()
		小--大
		write()

	} else if (pid > 0) {	

		close(cfd);		关闭用于与客户端通信的套接字 cfd	
		contiue;
	}
  }

5. 子进程:

	close(lfd)

	read()

	小--大

	write()	

   父进程:

	close(cfd);

	注册信号捕捉函数:	SIGCHLD

	在回调函数中, 完成子进程回收

		while (waitpid());

3.2 多线程并发服务器

多线程并发服务器思路分析

1. Socket();		创建 监听套接字 lfd

2. Bind()		绑定地址结构 Strcut scokaddr_in addr;

3. Listen();		

4. while (1) {		

	cfd = Accept(lfd, );

	pthread_create(&tid, NULL, tfn, (void *)cfd);

	pthread_detach(tid);  				// pthead_join(tid, void **);  新线程---专用于回收子线程。
  }

5. 子线程:

	void *tfn(void *arg) 
	{
		// close(lfd)			不能关闭。 主线程要使用lfd

		read(cfd)

		小--大

		write(cfd)

		pthread_exit((void *)10);	
	}

3.3、select

3.3.1 select函数原型

select多路IO转接:

原理: 借助内核用select 来监听:客户端连接、数据通信事件。

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

	nfds:监听的所有文件描述符中,最大文件描述符+1

	readfds: 读 文件描述符监听集合。	传入、传出参数

	writefds:写 文件描述符监听集合。	传入、传出参数		NULL

	exceptfds:异常 文件描述符监听集合	传入、传出参数		NULL

	timeout: 	> 0: 	设置监听超时时长。

				NULL:	阻塞监听

				0:	    非阻塞监听,轮询
	返回值:

		> 0:	所有监听集合(3个:读、写、异常)中, 满足对应事件的总数。

		0:	    没有满足监听条件的文件描述符

		-1: 	errno

3.3.2 select相关参数

void FD_CLR(int fd, fd_set *set) 把某一个fd清除出去
int FD_ISSET(int fd, fd_set *set) 判定某个fd是否在位图中
void FD_SET(int fd, fd_set *set) 把某一个fd添加到位图
void FD_ZERO(fd_set *set) 位图所有二进制位置零

void FD_ZERO(fd_set *set);	--- 清空一个文件描述符集合。

	fd_set rset;

	FD_ZERO(&rset);

void FD_SET(int fd, fd_set *set);	--- 将待监听的文件描述符,添加到监听集合中

	FD_SET(3, &rset);	FD_SET(5, &rset);	FD_SET(6, &rset);

void FD_CLR(int fd, fd_set *set);	--- 将一个文件描述符从监听集合中 移除。

	FD_CLR(4, &rset);

int  FD_ISSET(int fd, fd_set *set);	--- 判断一个文件描述符是否在监听集合中。

	返回值: 在:1;不在:0;

	FD_ISSET(4, &rset);

3.3.3 select实现多路IO转接设计思路

思路分析:

int maxfd = 0;

lfd = socket() ;			创建套接字

maxfd = lfd;

bind();					绑定地址结构

listen();				设置监听上限

fd_set rset, allset;			创建r监听集合

FD_ZERO(&allset);				将r监听集合清空

FD_SET(lfd, &allset);			将 lfd 添加至读集合中。

while(1) {

	rset = allset;			保存监听集合

	ret  = select(lfd+1, &rset, NULL, NULL, NULL);		监听文件描述符集合对应事件。

	if(ret > 0) {							有监听的描述符满足对应事件
	
		if (FD_ISSET(lfd, &rset)) {				// 1 在。 0不在。

			cfd = accept();				建立连接,返回用于通信的文件描述符

			maxfd = cfd;

			FD_SET(cfd, &allset);				添加到监听通信描述符集合中。
		}

		for (i = lfd+1; i <= 最大文件描述符; i++){

			FD_ISSET(i, &rset)				有read、write事件

			read()

			小 -- 大

			write();
		}	
	}
}

3.3.4 select实现多路IO转接-代码

1.	#include <stdio.h>  
2.	#include <stdlib.h>  
3.	#include <unistd.h>  
4.	#include <string.h>  
5.	#include <arpa/inet.h>  
6.	#include <ctype.h>  
7.	  
8.	#include "wrap.h"  
9.	  
10.	#define SERV_PORT 6666  
11.	  
12.	int main(int argc, char *argv[])  
13.	{  
14.	    int i, j, n, nready;  
15.	  
16.	    int maxfd = 0;  
17.	  
18.	    int listenfd, connfd;  
19.	  
20.	    char buf[BUFSIZ];         /* #define INET_ADDRSTRLEN 16 */  
21.	  
22.	    struct sockaddr_in clie_addr, serv_addr;  
23.	    socklen_t clie_addr_len;  
24.	  
25.	    listenfd = Socket(AF_INET, SOCK_STREAM, 0);    
26.	    int opt = 1;  
27.	    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  
28.	    bzero(&serv_addr, sizeof(serv_addr));  
29.	    serv_addr.sin_family= AF_INET;  
30.	    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
31.	    serv_addr.sin_port= htons(SERV_PORT);  
32.	    Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));  
33.	    Listen(listenfd, 128);  
34.	      
35.	  
36.	    fd_set rset, allset;                            /* rset 读事件文件描述符集合 allset用来暂存 */  
37.	  
38.	    maxfd = listenfd;  
39.	  
40.	    FD_ZERO(&allset);  
41.	    FD_SET(listenfd, &allset);                                  /* 构造select监控文件描述符集 */  
42.	  
43.	    while (1) {     
44.	        rset = allset;                                          /* 每次循环时都从新设置select监控信号集 */  
45.	        nready = select(maxfd+1, &rset, NULL, NULL, NULL);  
46.	        if (nready < 0)  
47.	            perr_exit("select error");  
48.	  
49.	        if (FD_ISSET(listenfd, &rset)) {                        /* 说明有新的客户端链接请求 */  
50.	  
51.	            clie_addr_len = sizeof(clie_addr);  
52.	            connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);       /* Accept 不会阻塞 */  
53.	  
54.	            FD_SET(connfd, &allset);                            /* 向监控文件描述符集合allset添加新的文件描述符connfd */  
55.	  
56.	            if (maxfd < connfd)  
57.	                maxfd = connfd;  
58.	  
59.	            if (0 == --nready)                                  /* 只有listenfd有事件, 后续的 for 不需执行 */  
60.	                continue;  
61.	        }   
62.	  
63.	        for (i = listenfd+1; i <= maxfd; i++) {                 /* 检测哪个clients 有数据就绪 */  
64.	  
65.	            if (FD_ISSET(i, &rset)) {  
66.	  
67.	                if ((n = Read(i, buf, sizeof(buf))) == 0) {    /* 当client关闭链接时,服务器端也关闭对应链接 */  
68.	                    Close(i);  
69.	                    FD_CLR(i, &allset);                        /* 解除select对此文件描述符的监控 */  
70.	  
71.	                } else if (n > 0) {  
72.	  
73.	                    for (j = 0; j < n; j++)  
74.	                        buf[j] = toupper(buf[j]);  
75.	                    Write(i, buf, n);  
76.	                }  
77.	            }  
78.	        }  
79.	    }  
80.	  
81.	    Close(listenfd);  
82.	  
83.	    return 0;  
84.	}  

编译运行,结果如下:
在这里插入图片描述
如图,借助select也可以实现多线程

3.3.5 select优缺点

缺点:	监听上限受文件描述符限制。 最大 1024.

	检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。

优点:	跨平台。win、linux、macOS、Unix、类Unix、mips

3.4 poll

poll是对select的改进,但是它是个半成品,相对select提升不大。最终版本是epoll,所以poll了解一下就完事儿,重点掌握epoll。

3.4.1 POLL函数原型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

	fds:监听的文件描述符【数组】

		struct pollfd {
			
			int fd:	待监听的文件描述符
			
			short events:	待监听的文件描述符对应的监听事件。取值:POLLIN、POLLOUT、POLLERR

			short revnets:	传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
		}

	nfds: 监听数组的,实际有效监听个数。

	timeout:  > 0:  超时时长。单位:毫秒。

		       -1:	阻塞等待

		        0: 不阻塞

	返回值:返回满足对应监听事件的文件描述符 总个数。

优点:
	自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。

	拓展 监听上限。 超出 1024限制。

缺点:
	不能跨平台。 Linux

	无法直接定位满足监听事件的文件描述符, 编码难度较大。

3.4.2 poll函数实现服务器

1.	/* server.c */  
2.	#include <stdio.h>  
3.	#include <stdlib.h>  
4.	#include <string.h>  
5.	#include <netinet/in.h>  
6.	#include <arpa/inet.h>  
7.	#include <poll.h>  
8.	#include <errno.h>  
9.	#include "wrap.h"  
10.	  
11.	#define MAXLINE 80  
12.	#define SERV_PORT 6666  
13.	#define OPEN_MAX 1024  
14.	  
15.	int main(int argc, char *argv[])  
16.	{  
17.	    int i, j, maxi, listenfd, connfd, sockfd;  
18.	    int nready;  
19.	    ssize_t n;  
20.	    char buf[MAXLINE], str[INET_ADDRSTRLEN];  
21.	    socklen_t clilen;  
22.	    struct pollfd client[OPEN_MAX];  
23.	    struct sockaddr_in cliaddr, servaddr;  
24.	  
25.	    listenfd = Socket(AF_INET, SOCK_STREAM, 0);  
26.	  
27.	    bzero(&servaddr, sizeof(servaddr));  
28.	    servaddr.sin_family = AF_INET;  
29.	    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
30.	    servaddr.sin_port = htons(SERV_PORT);  
31.	  
32.	    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));  
33.	  
34.	    Listen(listenfd, 20);  
35.	  
36.	    client[0].fd = listenfd;  
37.	    client[0].events = POLLRDNORM;                  /* listenfd监听普通读事件 */  
38.	  
39.	    for (i = 1; i < OPEN_MAX; i++)  
40.	        client[i].fd = -1;                          /* 用-1初始化client[]里剩下元素 */  
41.	    maxi = 0;                                       /* client[]数组有效元素中最大元素下标 */  
42.	  
43.	    for ( ; ; ) {  
44.	        nready = poll(client, maxi+1, -1);          /* 阻塞 */  
45.	        if (client[0].revents & POLLRDNORM) {       /* 有客户端链接请求 */  
46.	            clilen = sizeof(cliaddr);  
47.	            connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);  
48.	            printf("received from %s at PORT %d\n",  
49.	                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),  
50.	                    ntohs(cliaddr.sin_port));  
51.	            for (i = 1; i < OPEN_MAX; i++) {  
52.	                if (client[i].fd < 0) {  
53.	                    client[i].fd = connfd;  /* 找到client[]中空闲的位置,存放accept返回的connfd */  
54.	                    break;  
55.	                }  
56.	            }  
57.	  
58.	            if (i == OPEN_MAX)  
59.	                perr_exit("too many clients");  
60.	  
61.	            client[i].events = POLLRDNORM;      /* 设置刚刚返回的connfd,监控读事件 */  
62.	            if (i > maxi)  
63.	                maxi = i;                       /* 更新client[]中最大元素下标 */  
64.	            if (--nready <= 0)  
65.	                continue;                       /* 没有更多就绪事件时,继续回到poll阻塞 */  
66.	        }  
67.	        for (i = 1; i <= maxi; i++) {            /* 检测client[] */  
68.	            if ((sockfd = client[i].fd) < 0)  
69.	                continue;  
70.	            if (client[i].revents & (POLLRDNORM | POLLERR)) {  
71.	                if ((n = Read(sockfd, buf, MAXLINE)) < 0) {  
72.	                    if (errno == ECONNRESET) { /* 当收到 RST标志时 */  
73.	                        /* connection reset by client */  
74.	                        printf("client[%d] aborted connection\n", i);  
75.	                        Close(sockfd);  
76.	                        client[i].fd = -1;  
77.	                    } else {  
78.	                        perr_exit("read error");  
79.	                    }  
80.	                } else if (n == 0) {  
81.	                    /* connection closed by client */  
82.	                    printf("client[%d] closed connection\n", i);  
83.	                    Close(sockfd);  
84.	                    client[i].fd = -1;  
85.	                } else {  
86.	                    for (j = 0; j < n; j++)  
87.	                        buf[j] = toupper(buf[j]);  
88.	                        Writen(sockfd, buf, n);  
89.	                }  
90.	                if (--nready <= 0)  
91.	                    break;              /* no more readable descriptors */  
92.	            }  
93.	        }  
94.	    }  
95.	    return 0;  
96.	}  

3.4.3 poll函数总结

优点:
	自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。

	拓展 监听上限。 超出 1024限制。

缺点:
	不能跨平台。 Linux

	无法直接定位满足监听事件的文件描述符, 编码难度较大。

3.5 epoll

3.5.1 epoll函数原型

int epoll_create(int size);						创建一棵监听红黑树

	size:创建的红黑树的监听节点数量。(仅供内核参考。)

	返回值:指向新创建的红黑树的根节点的 fd。 

		失败: -1 errno

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	操作监听红黑树

	epfd:epoll_create 函数的返回值。 epfd

	op:对该监听红黑数所做的操作。

		EPOLL_CTL_ADD 添加fd到 监听红黑树

		EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。

		EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)

	fd:
		待监听的fd

	event:	本质 struct epoll_event 结构体 地址

		成员 events:

			EPOLLIN / EPOLLOUT / EPOLLERR

		成员 data: 联合体(共用体):

			int fd;	  对应监听事件的 fd

			void *ptr; 

			uint32_t u32;

			uint64_t u64;		

	返回值:成功 0; 失败: -1 errno

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 	 阻塞监听。

	epfd:epoll_create 函数的返回值。 epfd

	events:传出参数,【数组】, 满足监听条件的 那些 fd 结构体。

	maxevents:数组 元素的总个数。 1024
			
		struct epoll_event evnets[1024]
	timeout:

		-1: 阻塞

		0: 不阻塞

		>0: 超时时间 (毫秒)

	返回值:

		> 0: 满足监听的 总个数。 可以用作循环上限。

		0: 没有fd满足监听事件

		-1:失败。 errno
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值