Linux内核网络栈1.2.13-af_inet.c概述

参考资料
<<linux内核网络栈源代码情景分析>>
socket常用函数继续调用分析

根据socket提供的常用库函数,socket、read和write等函数,继续往下一层分析tcp/ip协议的执行过程,本文分析的函数大部分位于af_inet.c文件中,该层大部分逻辑还是将调用tcp.c中的逻辑处理,作为了一个过渡层,待到tcp.c文件内容时,得到的细节会慢慢深入。

socket的执行过程

执行过程中有sock_socket函数,sock_bind函数等,本文就接着上一篇文章内容继续分析

sock_socket调用的net_create函数

主要的函数逻辑如下:

/*
 *	Create an inet socket.
 *
 *	FIXME: Gcc would generate much better code if we set the parameters
 *	up in in-memory structure order. Gcc68K even more so
 * 
 *  用于创建一个套接字对应的sock结构并对其进行初始化
 */

static int inet_create(struct socket *sock, int protocol)
{
	struct sock *sk;
	struct proto *prot;
	int err;

	sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL);  // 申请内存
	if (sk == NULL) 
		return(-ENOBUFS);
	
	sk->num = 0;
	sk->reuse = 0;
	
	switch(sock->type)  									// 根据传入的套接字进行赋值
	{
		case SOCK_STREAM: 									// 流式套接字使用tcp协议操作函数
		case SOCK_SEQPACKET:
			if (protocol && protocol != IPPROTO_TCP)        // 检查protocl是否与IPPROTO_TCP相同 如果不同则报错返回
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPROTONOSUPPORT);
			}
			protocol = IPPROTO_TCP; 						// 设置协议
			/* TCP_NO_CHECK设置为1 表示对于tcp协议默认使用校验	*/
			sk->no_check = TCP_NO_CHECK;
			prot = &tcp_prot; 								// 设置协议操作函数集
			break;

		case SOCK_DGRAM:
			if (protocol && protocol != IPPROTO_UDP)  		// 设置UDP协议操作集
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPROTONOSUPPORT);
			}
			protocol = IPPROTO_UDP;
			sk->no_check = UDP_NO_CHECK;
			prot=&udp_prot; 								// 设置UDP操作集
			break;
      
		case SOCK_RAW: 										// 原始套接字操作集
			if (!suser()) 
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPERM);
			}
			if (!protocol) 
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPROTONOSUPPORT);
			}
			prot = &raw_prot; 								// 设置套接字操作函数集
			sk->reuse = 1;
			sk->no_check = 0;	/*
						 * Doesn't matter no checksum is
						 * performed anyway.
						 */
			sk->num = protocol; 						// protocol则为端口号
			break;

		case SOCK_PACKET: 								// 包套接字
			if (!suser()) 
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPERM);
			}
			if (!protocol) 
			{
				kfree_s((void *)sk, sizeof(*sk));
				return(-EPROTONOSUPPORT);
			}
			prot = &packet_prot; 						// 设置流式套接字操作集
			sk->reuse = 1;
			sk->no_check = 0;	/* Doesn't matter no checksum is
						 * performed anyway.
						 */
			sk->num = protocol;
			break;

		default:
			kfree_s((void *)sk, sizeof(*sk)); 			// 都没有则报错
			return(-ESOCKTNOSUPPORT);
	}
	
	sk->socket = sock; 									// 建立socket对应的sock,socket先创建成功
#ifdef CONFIG_TCP_NAGLE_OFF 							// 是否定义了nagle算法 如果定义则初始化为0 否则为1
	sk->nonagle = 1;
#else    
	sk->nonagle = 0;
#endif  
	sk->type = sock->type; 								// 设置type类型
	sk->stamp.tv_sec=0;
	sk->protocol = protocol;		// 设置协议
	sk->wmem_alloc = 0;
	sk->rmem_alloc = 0;
	sk->sndbuf = SK_WMEM_MAX;	// 最大发送缓冲区
	sk->rcvbuf = SK_RMEM_MAX;	// 最大接受缓冲区
	sk->pair = NULL;
	sk->opt = NULL;
	sk->write_seq = 0;
	sk->acked_seq = 0;
	sk->copied_seq = 0;
	sk->fin_seq = 0;
	sk->urg_seq = 0;
	sk->urg_data = 0;
	sk->proc = 0;
	sk->rtt = 0;				/*TCP_WRITE_TIME << 3;*/
	sk->rto = TCP_TIMEOUT_INIT;		/*TCP_WRITE_TIME*/
	sk->mdev = 0;
	sk->backoff = 0;
	sk->packets_out = 0;
	sk->cong_window = 1;		 /* start with only sending one packet at a time. 
							  * tcp进入满启动阶段	*/
	sk->cong_count = 0;
	sk->ssthresh = 0;
	sk->max_window = 0;
	sk->urginline = 0;
	sk->intr = 0;
	sk->linger = 0;
	sk->destroy = 0;
	sk->priority = 1;
	sk->shutdown = 0;
	sk->keepopen = 0;
	sk->zapped = 0;
	sk->done = 0;
	sk->ack_backlog = 0;
	sk->window = 0;
	sk->bytes_rcv = 0;
	sk->state = TCP_CLOSE;	// 由于还未进行连接,状态设置为TCP_CLOSE
	sk->dead = 0;
	sk->ack_timed = 0;
	sk->partial = NULL;
	sk->user_mss = 0;
	sk->debug = 0;

	/* 设置最大可暂缓应答的字节数	*/
	/* this is how many unacked bytes we will accept for this socket.  */
	sk->max_unacked = 2048; /* needs to be at most 2 full packets. */

	/* how many packets we should send before forcing an ack. 
	   if this is set to zero it is the same as sk->delay_acks = 0 */
	sk->max_ack_backlog = 0;
	sk->inuse = 0;
	sk->delay_acks = 0;
	skb_queue_head_init(&sk->write_queue);
	skb_queue_head_init(&sk->receive_queue);
	sk->mtu = 576;		// mtu设置为576字节
	sk->prot = prot; 	// 设置协议操作集函数
	sk->sleep = sock->wait;
	sk->daddr = 0;
	sk->saddr = 0 /* ip_my_addr() */;
	sk->err = 0;
	sk->next = NULL;
	sk->pair = NULL;
	sk->send_tail = NULL;
	sk->send_head = NULL;
	sk->timeout = 0;
	sk->broadcast = 0;
	sk->localroute = 0;
	init_timer(&sk->timer);
	init_timer(&sk->retransmit_timer);
	sk->timer.data = (unsigned long)sk;
	sk->timer.function = &net_timer;
	skb_queue_head_init(&sk->back_log);
	sk->blog = 0;
	sock->data =(void *) sk;
	sk->dummy_th.doff = sizeof(sk->dummy_th)/4;  // 设置tcp首部信息
	sk->dummy_th.res1=0;
	sk->dummy_th.res2=0;
	sk->dummy_th.urg_ptr = 0;
	sk->dummy_th.fin = 0;
	sk->dummy_th.syn = 0;
	sk->dummy_th.rst = 0;
	sk->dummy_th.psh = 0;
	sk->dummy_th.ack = 0;
	sk->dummy_th.urg = 0;
	sk->dummy_th.dest = 0;
	sk->ip_tos=0;
	sk->ip_ttl=64;
#ifdef CONFIG_IP_MULTICAST
	sk->ip_mc_loop=1;
	sk->ip_mc_ttl=1;
	*sk->ip_mc_name=0;
	sk->ip_mc_list=NULL;
#endif
  	
	sk->state_change = def_callback1; 			// 设置回调函数
	sk->data_ready = def_callback2;
	sk->write_space = def_callback3;
	sk->error_report = def_callback1;

	if (sk->num)  									// 如果已经设置端口号
	{
	/*
	 * It assumes that any protocol which allows
	 * the user to assign a number at socket
	 * creation time automatically
	 * shares.
	 */
		put_sock(sk->num, sk); 						// 添加到sock列表中
		sk->dummy_th.source = ntohs(sk->num); 		// 在tcp首部信息中添加本地端口信息
	}

	if (sk->prot->init)  							// 如果协议有初始化方法
	{
		err = sk->prot->init(sk); 					// 调用该协议的初始化方法
		if (err != 0)  							 	// 如果出错
		{
			destroy_sock(sk); 						// 销毁该sk
			return(err);
		}
	}
	return(0); 										// 初始化完成
}

该函数的主要工作就是传入sock_socket中创建的socket,并将在inet_create函数中创建的sock结构进行互相关联,然后再初始化sock,初始化sock的过程就是根据传入不同的协议来进行初始化,并选择不同的操作集函数,如果是tcp协议就选择tcp_prot作为sock的prot后续的操作都会通过sock->prot函数集来操作,在设置初始化完成之后,如果检查到有协议注册的init函数就调用该函数进行注册。

sock_bind调用的inet_bind函数

主要的函数逻辑如下:

/* this needs to be changed to disallow
   the rebinding of sockets.   What error
   should it return?  该函数进行地址绑定 */

static int inet_bind(struct socket *sock, struct sockaddr *uaddr,
	       int addr_len)
{
	struct sockaddr_in *addr=(struct sockaddr_in *)uaddr;
	struct sock *sk=(struct sock *)sock->data, *sk2;
	unsigned short snum = 0 /* Stoopid compiler.. this IS ok */;
	int chk_addr_ret;

	/* check this error. */
	if (sk->state != TCP_CLOSE)
		return(-EIO);
	if(addr_len<sizeof(struct sockaddr_in))
		return -EINVAL;
		
	if(sock->type != SOCK_RAW) 		// 是否是原始套接字类型
	{
		/* 检查是否时Row类型 如果端口不为空则报错 */
		if (sk->num != 0) 
			return(-EINVAL);

		snum = ntohs(addr->sin_port); 		// 获取绑定中的端口号

		/*
		 * We can't just leave the socket bound wherever it is, it might
		 * be bound to a privileged port. However, since there seems to
		 * be a bug here, we will leave it if the port is not privileged.
		 */
		if (snum == 0)  					// 如果没有则分配一个
		{
			snum = get_new_socknum(sk->prot, 0);
		}
		if (snum < PROT_SOCK && !suser())  	// 如果小于1024 并且不是超级用户则报错
			return(-EACCES);
	}
	
	chk_addr_ret = ip_chk_addr(addr->sin_addr.s_addr); 		// 检查ip地址是否为一个本地地址
	if (addr->sin_addr.s_addr != 0 && chk_addr_ret != IS_MYADDR && chk_addr_ret != IS_MULTICAST) // 如果指定的地址不是本地地址也不是一个多播地址则报错
		return(-EADDRNOTAVAIL);	/* Source address MUST be ours! */
	  	
	if (chk_addr_ret || addr->sin_addr.s_addr == 0) 	   // 如果没有指定地址,则系统自动分配一个本地地址
		sk->saddr = addr->sin_addr.s_addr;
	
	if(sock->type != SOCK_RAW) 							// 如果不是原始套接字
	{
		/* Make sure we are allowed to bind here. */
		cli(); 												// 关闭中断
		for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)];  	// 根据端口号找到对应的sock队列
					sk2 != NULL; sk2 = sk2->next) 
		{
		/* should be below! */
			if (sk2->num != snum)  										// 如果端口不相同则循环下一个
				continue;
			if (!sk->reuse) 											// 检查是否设置了端口复用参数
			{
				sti();  												// 如果没有设置则返回报错
				return(-EADDRINUSE);
			}
			
			if (sk2->num != snum) 
				continue;		/* more than one */
			if (sk2->saddr != sk->saddr)  								// 检查地址
				continue;	/* socket per slot ! -FB */
			if (!sk2->reuse || sk2->state==TCP_LISTEN)  				// 如果sock的状态为TCP_LISTEN则表示该套接字为一个服务端 服务端不可使用地址复用
			{
				sti();
				return(-EADDRINUSE);
			}
		}
		sti();

		remove_sock(sk); 												// 删除该sk
		put_sock(snum, sk); 											// 加入该端口和sk
		sk->dummy_th.source = ntohs(sk->num); 							// 设置tcp头部的本地端口信息
		sk->daddr = 0; 													// 设置远端地址
		sk->dummy_th.dest = 0;
	}
	return(0);
}

该函数主要是完成本地地址绑定,本地地址绑定包括ip地址和端口号两个部分,如果在绑定过程中没有指定地址和端口号,则系统就会进行自动分配,由于原始套接字的实现不同,故在该函数的处理过程中将处理tcp等协议的远端地址的设置与绑定,主要绑定在tcp协议的头部。

sock_listen调用的inet_listen函数

该函数如下:

/*
 *	Move a socket into listening state.
 */
 
static int inet_listen(struct socket *sock, int backlog)
{
	struct sock *sk = (struct sock *) sock->data; 						// 获取sock结构

	if(inet_autobind(sk)!=0)											// 检查是否有未绑定的端口号,设置source,设置端口号
		return -EAGAIN;

	/* We might as well re use these. */ 
	/*
	 * note that the backlog is "unsigned char", so truncate it
	 * somewhere. We might as well truncate it to what everybody
	 * else does..
	 */
	if ((unsigned) backlog > 128) 										// 如果设置的队列长度大于128则设置为128
		backlog = 128;
	sk->max_ack_backlog = backlog; 										// 设置最大队列数
	if (sk->state != TCP_LISTEN) 										// 如果sock的状态不是TCP_LISTEN改为TCP_LISTEN
	{
		sk->ack_backlog = 0; 											// ack_backlog设置为0
		sk->state = TCP_LISTEN; 									    // 设置为TCP_LISTEN状态
	}
	return(0);
}

该函数主要是将检查传入的链接大小,不能超过最大128,如果超过则设置为128,并更改当前连接的监听状态为TCP_LISTEN。该函数实现较为简单。

sock_send函数调用的inet_send和sock_recv函数调用的inet_recv函数

inet_send函数主要就是调用了协议层的发送函数;

static int inet_send(struct socket *sock, void *ubuf, int size, int noblock, 
	       unsigned flags)
{
	struct sock *sk = (struct sock *) sock->data;
	if (sk->shutdown & SEND_SHUTDOWN) 
	{
		send_sig(SIGPIPE, current, 1);		//发送信号
		return(-EPIPE);
	}
	if(sk->err)
		return inet_error(sk);
	/* We may need to bind the socket. */
	if(inet_autobind(sk)!=0)
		return(-EAGAIN);
	return(sk->prot->write(sk, (unsigned char *) ubuf, size, noblock, flags)); 	// 调用inet的操作函数集操作
}

直接调用了sk对应的prot函数集来发送数据。

inet_recv函数主要就是接受函数,接受底层往上传入的数据;

static int inet_recv(struct socket *sock, void *ubuf, int size, int noblock,
	  unsigned flags)
{
	/* BSD explicitly states these are the same - so we do it this way to be sure */
	return inet_recvfrom(sock,ubuf,size,noblock,flags,NULL,NULL);
}

/*
 *	The assorted BSD I/O operations
 * 	读取sock中的数据
 */

static int inet_recvfrom(struct socket *sock, void *ubuf, int size, int noblock, 
		   unsigned flags, struct sockaddr *sin, int *addr_len )
{
	struct sock *sk = (struct sock *) sock->data;
	
	if (sk->prot->recvfrom == NULL) 
		return(-EOPNOTSUPP);
	if(sk->err)
		return inet_error(sk);
	/* We may need to bind the socket. */
	if(inet_autobind(sk)!=0)
		return(-EAGAIN);
	return(sk->prot->recvfrom(sk, (unsigned char *) ubuf, size, noblock, flags,
			     (struct sockaddr_in*)sin, addr_len)); 			// 调用sock的函数操作集获取数据
}

这两个函数都是通过协议函数集来操作的,例如tcp协议对应的tcp_prot函数,直接调用对应的函数操作。

sock_connect对应的inet_connect函数

该函数如下;

/*
 *	Connect to a remote host. There is regrettably still a little
 *	TCP 'magic' in here.
 *
 *	调用方式: sock_connect -> inet_connect
 *   完成套接字连接操作
 */
 
static int inet_connect(struct socket *sock, struct sockaddr * uaddr,
		  int addr_len, int flags)
{
	struct sock *sk=(struct sock *)sock->data; 			// 获取sock
	int err;
	sock->conn = NULL;

	if (sock->state == SS_CONNECTING && tcp_connected(sk->state)) 		// 如果该套接字状态是连接状态则返回
	{
		sock->state = SS_CONNECTED;
		/* Connection completing after a connect/EINPROGRESS/select/connect */
		return 0;	/* Rock and roll */
	}

	if (sock->state == SS_CONNECTING && sk->protocol == IPPROTO_TCP && (flags & O_NONBLOCK)) {
		/* 如果套接字处于建立连接阶段并且套接字使用了TCP协议,并且使用了非阻塞选项则立刻返回连接正在进行的错误*/
		if (sk->err != 0)
		{
			err=sk->err;
			sk->err=0;
			return -err;
		}
		return -EALREADY;	/* Connecting is currently in progress */
	}

	/* 如果sock连接字状态不是正在连接中	*/
	if (sock->state != SS_CONNECTING) 
	{
		/* We may need to bind the socket. */
		if(inet_autobind(sk)!=0) 					// 检查自动绑定端口
			return(-EAGAIN);
		if (sk->prot->connect == NULL)  			// 检查协议是否有该操作函数
			return(-EOPNOTSUPP);
		/* 
		 * TCP ¸发送SYN 并将状态更改为sk 状态 TCP_SYN_SENT
		 */
		err = sk->prot->connect(sk, (struct sockaddr_in *)uaddr, addr_len); 		// 调用协议函数的connect函数
		if (err < 0)  																// 如果连接出错则报错
			return(err);
  		sock->state = SS_CONNECTING; 												// 更新状态为正在连接中
	}
	
	// 如果sk状态大于TCP_FIN_WAIT2 但是sock状态为正在连接中 则状态可能为TCP_TIME_WAIT TCP_CLOSE TCP_CLOSE_WAIT
	if (sk->state > TCP_FIN_WAIT2 && sock->state==SS_CONNECTING)
	{
		sock->state=SS_UNCONNECTED; 					// 此时重置该状态并返回连接过程中的错误
		cli();
		err=sk->err;
		sk->err=0;
		sti();
		return -err;
	}

	if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK))  	// 如果连接过程中不是建立完成,并且不是阻塞状态
	  	return(-EINPROGRESS); 									// 返回错误

	cli(); /* avoid the race condition */
	while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 		// 状态为TCP_SYN_SEND 或者 TCP_SYN_RECV
	{
		interruptible_sleep_on(sk->sleep); 								// 等待
		if (current->signal & ~current->blocked)  						// 如果出错则返回错误
		{
			sti();
			return(-ERESTARTSYS);
		}
		/* This fixes a nasty in the tcp/ip code. There is a hideous hassle with
		   icmp error packets wanting to close a tcp or udp socket. */
		if(sk->err && sk->protocol == IPPROTO_TCP) 						// 检查错误 并且协议为TCP
		{
			sti();
			sock->state = SS_UNCONNECTED; 								// 设置为未连接
			err = -sk->err; 											// 返回错误
			sk->err=0;
			return err; /* set by tcp_err() */
		}
	}
	sti(); 																// 使能中断

	sock->state = SS_CONNECTED; 										// 更新状态为连接成功

	if (sk->state != TCP_ESTABLISHED && sk->err)  						// 如果连接不成功并且有错误原因则返回
	{
		sock->state = SS_UNCONNECTED;
		err=sk->err;
		sk->err=0;
		return(-err);
	}
	return(0);
}

首先会先检查套接字的连接状态如果是正在连接则返回,如果套接字正在连接并且是tcp协议并且使用了非阻塞选项则立刻返回正在连接的信息,如果不是正在连接中则进行连接,此时就调用prot对应的connect连接函数进行连接,并将该socket结构状态更新为正在连接中,如果此时地址未绑定则自动绑定,该connect函数就是对应的tcp_connect函数,该函数就是将发送SYN数据包进行三路握手连接建立过程,在tcp_connect函数中,会将sock的状态更新TCP_SYN_SENT,然后就继续判断在连接完成之后的状态,通过一个while循环来判断是否连接成功如果连接成功则更新状态为连接成功并返回。

sock_accept函数调用的inet_accept函数

该函数主要逻辑如下;

/*
 *	Accept a pending connection. The TCP layer now gives BSD semantics.
 *	接受一个连接请求
 */

static int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
	struct sock *sk1, *sk2;
	int err;

	// 获取sock内容
	sk1 = (struct sock *) sock->data;

	/*
	 * We've been passed an extra socket.
	 * We need to free it up because the tcp module creates
	 * its own when it accepts one.
	 */
	if (newsock->data)
	{
	  	struct sock *sk=(struct sock *)newsock->data; 	// 获取data
	  	newsock->data=NULL; 							// 重置data为空
	  	sk->dead = 1;
	  	destroy_sock(sk); 								// 销毁该sk
	}
  
	if (sk1->prot->accept == NULL)  					// 如果accept为空则报错
		return(-EOPNOTSUPP);

	/* Restore the state if we have been interrupted, and then returned. */
	if (sk1->pair != NULL )  							// 如果不为空
	{
		sk2 = sk1->pair; 								// 获取sk1->pair 因为如果被中断pair就是保存的sock结构
		sk1->pair = NULL; 								// 置空该字段
	} 
	else
	{
		sk2 = sk1->prot->accept(sk1,flags); 			// 调用协议的accept函数处理
		if (sk2 == NULL)  								// 如果为空
		{
			if (sk1->err <= 0)
				printk("Warning sock.c:sk1->err <= 0.  Returning non-error.\n");
			err=sk1->err;
			sk1->err=0;
			return(-err); 								// 打印错误信息后并返回错误原因
		}
	}
	
	newsock->data = (void *)sk2; 						// 设置关联的data
	sk2->sleep = newsock->wait; 						
	sk2->socket = newsock; 								// 设置socket的关联
	newsock->conn = NULL;	
	
	if (flags & O_NONBLOCK) 	// 检查是否为非阻塞
		return(0);			

	cli(); /* avoid the race. */	
	while(sk2->state == TCP_SYN_RECV) 	// 状态如果等于TCP_SYN_RECV
	{
		interruptible_sleep_on(sk2->sleep); 		// 中断等待
		if (current->signal & ~current->blocked) 
		{
			sti();
			sk1->pair = sk2;			// 如果中断失败则设置保存到pair中
			sk2->sleep = NULL; 			// 重置为空
			sk2->socket=NULL;
			newsock->data = NULL;
			return(-ERESTARTSYS); 		// 返回失败
		}
	}
	sti(); 								// 使能中断

	if (sk2->state != TCP_ESTABLISHED && sk2->err > 0) 	// 如果状态不等于已经连接 并且有错误内容
	{
		err = -sk2->err; 								// 返回错误内容并销毁sk2
		sk2->err=0;
		sk2->dead=1;	/* ANK */
		destroy_sock(sk2);
		newsock->data = NULL;
		return(err);
	}
	newsock->state = SS_CONNECTED; 						// 如果成功则设置为连接成功
	return(0);
}

该函数主要用于接收一个套接字,该函数是由服务端调用,服务端在接受到一个远端客户端连接请求后,调用相应函数进行处理,首先会创建一个本地套接字用于通信,原来的套接字仍然处于监听状态,然后调用prot的accept函数进行发送应答数据包,将新创建的套接字对于sock结构接到监听sock结构的接受队列中给accept函数使用,然后就检查是否是阻塞状态,如果为非阻塞则直接返回,如果是阻塞状态则依次遍历当前的套接字状态是否从TCP_SYN_RECV改变,如果改变则说明连接建立成功,此时就表示三次握手的过程就完成。

三次握手的过程梳理

在这里插入图片描述

主要的业务逻辑在接受请求的过程中,双方接受连接并更改状态。

总结

本文主要细化到af_inet层来细化tcp的相关的操作函数,本质就是根据tcp协议做了更详细的细化内容,主要涉及到接受tcp协议层的具体的状态检查,并涉及到了tcp协议的几种状态的细节一点的内容,后续会继续根据流程去深入学习。由于本人才疏学浅,如有错误请批评指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值