CCF CSP 202104-3 DHCP服务器

博客介绍了CCF CSP的一道关于实现DHCP服务器的题目,详细阐述了DHCP协议的工作流程和数据报文格式。文章详细解释了服务器配置、分配策略以及实现细节,并提供了输入输出格式和样例。最后,博主分享了实现简单DHCP服务器的关键函数,包括处理Discover和Request报文的函数,以及处理过期时间的函数。在实现过程中遇到了因忽略IP范围限制导致的运行错误,并给出了修正方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 试题背景
动态主机配置协议(Dynamic Host Configuration Protocol, DHCP)是一种自动为网络客户端分配 IP 地址的网络协议。当支持该协议的计算机刚刚接入网络时,它可以启动一个 DHCP 客户端程序。后者可以通过一定的网络报文交互,从 DHCP 服务器上获得 IP 地址等网络配置参数,从而能够在用户不干预的情况下,自动完成对计算机的网络设置,方便用户连接网络。DHCP 协议的工作过程如下:

当 DHCP 协议启动的时候,DHCP 客户端向网络中广播发送 Discover 报文,请求 IP 地址配置;
当 DHCP 服务器收到 Discover 报文时,DHCP 服务器根据报文中的参数选择一个尚未分配的 IP 地址,分配给该客户端。DHCP 服务器用 Offer 报文将这个信息传达给客户端;
客户端收集收到的 Offer 报文。由于网络中可能存在多于一个 DHCP 服务器,因此客户端可能收集到多个 Offer 报文。客户端从这些报文中选择一个,并向网络中广播 Request 报文,表示选择这个 DHCP 服务器发送的配置;
DHCP 服务器收到 Request 报文后,首先判断该客户端是否选择本服务器分配的地址:如果不是,则在本服务器上解除对那个 IP 地址的占用;否则则再次确认分配的地址有效,并向客户端发送 Ack 报文,表示确认配置有效,Ack 报文中包括配置的有效时间。如果 DHCP 发现分配的地址无效,则返回 Nak 报文;
客户端收到 Ack 报文后,确认服务器分配的地址有效,即确认服务器分配的地址未被其它客户端占用,则完成网络配置,同时记录配置的有效时间,出于简化的目的,我们不考虑被占用的情况。若客户端收到 Nak 报文,则从步骤 1 重新开始;
客户端在到达配置的有效时间前,再次向 DHCP 服务器发送 Request 报文,表示希望延长 IP 地址的有效期。DHCP 服务器按照步骤 4 确定是否延长,客户端按照步骤 5 处理后续的配置;
在本题目中,你需要理解 DHCP 协议的工作过程,并按照题目的要求实现一个简单的 DHCP 服务器。

问题描述
报文格式
为了便于实现,我们简化地规定 DHCP 数据报文的格式如下:

<发送主机> <接收主机> <报文类型> <IP 地址> <过期时刻>

DHCP 数据报文的各个部分由空格分隔,其各个部分的定义如下:

发送主机:是发送报文的主机名,主机名是由小写字母、数字组成的字符串,唯一地表示了一个主机;
接收主机:当有特定的接收主机时,是接收报文的主机名;当没有特定的接收主机时,为一个星号(*);
报文类型:是三个大写字母,取值如下:
DIS:表示 Discover 报文;
OFR:表示 Offer 报文;
REQ:表示 Request 报文;
ACK:表示 Ack 报文;
NAK:表示 Nak 报文;
IP 地址,是一个非负整数:
对于 Discover 报文,该部分在发送的时候为 0,在接收的时候忽略;
对于其它报文,为正整数,表示一个 IP 地址;
过期时刻,是一个非负整数:
对于 Offer、Ack 报文,是一个正整数,表示服务器授予客户端的 IP 地址的过期时刻;
对于 Discover、Request 报文,若为正整数,表示客户端期望服务器授予的过期时刻;
对于其它报文,该部分在发送的时候为 0,在接收的时候忽略。
例如下列都是合法的 DHCP 数据报文

a * DIS 0 0
d a ACK 50 1000

服务器配置
为了 DHCP 服务器能够正确分配 IP 地址,DHCP 需要接受如下配置:

地址池大小 N:表示能够分配给客户端的 IP 地址的数目,且能分配的 IP 地址是 1,2,…,N;
默认过期时间 Tdef:表示分配给客户端的 IP 地址的默认的过期时间长度;
过期时间的上限和下限 Tmax、Tmin:表示分配给客户端的 IP 地址的最长过期时间长度和最短过期时间长度,客户端不能请求比这个更长或更短的过期时间;
本机名称 H:表示运行 DHCP 服务器的主机名。
分配策略
当客户端请求 IP 地址时,首先检查此前是否给该客户端分配过 IP 地址,且该 IP 地址在此后没有被分配给其它客户端。如果是这样的情况,则直接将 IP 地址分配给它,否则,
总是分配给它最小的尚未占用过的那个 IP 地址。如果这样的地址不存在,则分配给它最小的此时未被占用的那个 IP 地址。如果这样的地址也不存在,说明地址池已经分配完毕,因此拒绝分配地址。

实现细节
在 DHCP 启动时,首先初始化 IP 地址池,将所有地址设置状态为未分配,占用者为空,并清零过期时刻。
其中地址的状态有未分配、待分配、占用、过期四种。
处于未分配状态的 IP 地址没有占用者,而其余三种状态的 IP 地址均有一名占用者。
处于待分配和占用状态的 IP 地址拥有一个大于零的过期时刻。在到达该过期时刻时,若该地址的状态是待分配,则该地址的状态会自动变为未分配,且占用者清空,过期时刻清零;否则该地址的状态会由占用自动变为过期,且过期时刻清零。处于未分配和过期状态的 IP 地址过期时刻为零,即没有过期时刻。

对于收到的报文,设其收到的时刻为 t。处理细节如下:

判断接收主机是否为本机,或者为 *,若不是,则判断类型是否为 Request,若不是,则不处理;
若类型不是 Discover、Request 之一,则不处理;
若接收主机为 *,但类型不是 Discover,或接收主机是本机,但类型是 Discover,则不处理。
对于 Discover 报文,按照下述方法处理:

检查是否有占用者为发送主机的 IP 地址:
若有,则选取该 IP 地址;
若没有,则选取最小的状态为未分配的 IP 地址;
若没有,则选取最小的状态为过期的 IP 地址;
若没有,则不处理该报文,处理结束;
将该 IP 地址状态设置为待分配,占用者设置为发送主机;
若报文中过期时刻为 0 ,则设置过期时刻为 t+Tdef;否则根据报文中的过期时刻和收到报文的时刻计算过期时间,判断是否超过上下限:若没有超过,则设置过期时刻为报文中的过期时刻;否则则根据超限情况设置为允许的最早或最晚的过期时刻;
向发送主机发送 Offer 报文,其中,IP 地址为选定的 IP 地址,过期时刻为所设定的过期时刻。
对于 Request 报文,按照下述方法处理:

检查接收主机是否为本机:
若不是,则找到占用者为发送主机的所有 IP 地址,对于其中状态为待分配的,将其状态设置为未分配,并清空其占用者,清零其过期时刻,处理结束;
检查报文中的 IP 地址是否在地址池内,且其占用者为发送主机,若不是,则向发送主机发送 Nak 报文,处理结束;
无论该 IP 地址的状态为何,将该 IP 地址的状态设置为占用;
与 Discover 报文相同的方法,设置 IP 地址的过期时刻;
向发送主机发送 Ack 报文。
上述处理过程中,地址池中地址的状态的变化可以概括为如下图所示的状态转移图。为了简洁,该图中没有涵盖需要回复 Nak 报文的情况。

输入格式
输入的第一行包含用空格分隔的四个正整数和一个字符串,分别是:N、Tdef、Tmax、Tmin 和 H,保证 Tmin≤Tdef≤Tmax。

输入的第二行是一个正整数 n,表示收到了 n 个报文。

输入接下来有 n 行,第 (i+2) 行有空格分隔的正整数 ti 和约定格式的报文 Pi。表示收到的第 i 个报文是在 ti 时刻收到的,报文内容是 Pi。保证 ti

输出格式
输出有若干行,每行是一个约定格式的报文。依次输出 DHCP 服务器发送的报文。

样例输入
4 5 10 5 dhcp
16
1 a * DIS 0 0
2 a dhcp REQ 1 0
3 b a DIS 0 0
4 b * DIS 3 0
5 b * REQ 2 12
6 b dhcp REQ 2 12
7 c * DIS 0 11
8 c dhcp REQ 3 11
9 d * DIS 0 0
10 d dhcp REQ 4 20
11 a dhcp REQ 1 20
12 c dhcp REQ 3 20
13 e * DIS 0 0
14 e dhcp REQ 2 0
15 b dhcp REQ 2 25
16 b * DIS 0 0


样例输出
dhcp a OFR 1 6
dhcp a ACK 1 7
dhcp b OFR 2 9
dhcp b ACK 2 12
dhcp c OFR 3 12
dhcp c ACK 3 13
dhcp d OFR 4 14
dhcp d ACK 4 20
dhcp a ACK 1 20
dhcp c ACK 3 20
dhcp e OFR 2 18
dhcp e ACK 2 19
dhcp b NAK 2 0


样例说明
输入第一行,分别设置了 DHCP 的相关参数,并收到了 16 个报文。

第 1 个报文和第 2 个报文是客户端 a 正常请求地址,服务器为其分配了地址 1,相应地设置了过期时刻是 7(即当前时刻 2 加上默认过期时间 5)。

第 3 个报文不符合 Discover 报文的要求,不做任何处理。

第 4 个报文 b 发送的 Discover 报文虽然有 IP 地址 3,但是按照处理规则,这个字段被忽略,因此服务器返回 Offer 报文,过期时刻是 9。

第 5 个报文中,Request 报文不符合接收主机是 DHCP 服务器本机的要求,因此不做任何处理。

第 6 个报文是 b 发送的 Request 报文,其中设置了过期时刻是 12,没有超过最长过期时间,因此返回的 Ack 报文中过期时刻也是 12。

第 7 个报文中,过期时刻 11 小于最短过期时间,因此返回的过期时刻是 12。虽然此时为 a 分配的地址 1 过期,但是由于还有状态为未分配的地址 3,因此为 c 分配地址 3。第 8 个报文同理,为 c 分配的地址过期时刻是 13。

第 9、10 两个报文中,为 d 分配了地址 4,过期时刻是 20。

第 11 个报文中,a 请求重新获取此前为其分配的地址 1,虽然为其分配的地址过期,但是由于尚未分配给其它客户端,因此 DHCP 服务器可以直接为其重新分配该地址,并重新设置过期时刻为 20。

第 12 个报文中,c 请求延长其地址的过期时刻为 20。DHCP 正常向其回复 Ack 报文。

第 13、14 个报文中,e 试图请求地址。此时地址池中已经没有处于“未分配”状态的地址了,但是有此前分配给 b 的地址 2 的状态是“过期”,因此把该地址重新分配给 e。

第 15 个报文中,b 试图重新获取此前为其分配的地址 2,但是此时该地址已经被分配给 e,因此返回 Nak 报文。

第 16 个报文中,b 试图重新请求分配一个 IP 地址,但是此时地址池中已经没有可用的地址了,因此忽略该请求。

评测用例规模与约定
对于 20% 的数据,有 N≤200,且 n≤N,且输入仅含 Discover 报文,且 t

对于 50% 的数据,有 N≤200,且 n≤N,且 t*;

对于 70% 的数据,有 N≤1000,且 n≤N,且报文的接收主机或为本机,或为 *;

对于 100% 的数据,有 N≤10000,且 n≤10000,主机名的长度不超过 20,且 t,Tmin,Tdefault,Tmax≤109,输入的报文格式符合题目要求,且数字不超过 109。
 

这道题目其实不是很复杂,就是坑挖的有点多,不仔细读懂题目的话,很难拿到AC。

题目只要求我们对两种格式的报文进行处理,一种接受主机为*的DIS报文,另一种则是接收主机不为*的REQ报文。其余的报文都是不需要进行处理的。那么给两种报文各写一个函数负责处理信息就可以了。

两种报文处理过期时间的方法是一致的,所以我们先编写一个get_righttime()函数来得到正确的过期时间

long int get_righttime(long int wanttime,long int time)//wanttime表示期望过期时间,time表示当前时间
{
	long int ti;
	if(wanttime==0)  //期望时间为零时,为当前时间加默认时间
	{ti=time+Tnormol;
	}
	else if(wanttime-time<Tmin)  //期望时间小于最小过期时间时,为当前时间加最小过期时间
	{
		ti=time+Tmin;
	}
	else //否则为期望时间与期望时间加最大过期时间中较小的一个
	{
		ti=time+Tmax;
		if(ti>wanttime)
		{
			ti=wanttime;
		}
	}
}

关于DIS报文的相关函数

//p数组储存相应ip地址里的过期时间,q储存相应ip地址里的机器名,limit表示有几个ip,fork储存相应ip地址里的状态0为未分配,1为已分配,2为占用,3为过期
int findDIS(long int *p,string *q,long int time,long int limit,long int wanttime,long int *fork,string username)
{
	long int ti=get_righttime(wanttime,time);
	for(int i=0;i<limit;i++)//先判断主机里所有的ip地址中是否已经有了该用户
	{
		if(q[i]==username)
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	for(int i=0;i<limit;i++)//寻找主机里最小的未分配ip地址
	{
		if(q[i]=="NULL")
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	for(int i=0;i<limit;i++)//如果没有未分配的ip,则查找最小的过期ip
	{
		if(p[i]<=time)
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	return -1;//该主机的ip被完全占用,返回-1
}

关于REQ报文的函数

//接收机器为主机时

long int whichchange(long int *p,long int *fork,string *q,long int time,long int limit,string username,long int wanttime,int ip)
{
	long int ti=get_righttime(wanttime,time);//如果可执行,得到正确的过期时间
	if(q[ip-1]==username)//若主机中的ip地址里储存了该用户,则使该ip的状态改变为占用,并返回用户所在的ip
		{
			p[ip-1]=ti;
			fork[ip-1]=2;
			return ip;
		}
	return -1;//若主机没有储存该用户,返回-1
}

//接收主机为其他机器时,将主机里所有储存该用户的已分配ip修改为未分配
void check(long int *p,string *q,long int time,long int limit,string usname,long int *fork)
{
	for(int i=0;i<limit;i++)
	{
		if(q[i]==usname&&fork[i]==1)
		{
			q[i]="NULL";
			p[i]=0;
			fork[i]=0;
		}
	}
}

而在接收到报文,并根据报文内容进行判断前,我们需要将主机中已经过期的已分配状态下的ip修改为未分配状态,所以增加一个checkd函数,负责这一部分

void checkd(long int *p,string *q,long int time,long int limit,long int *fork)
{
for(int i=0;i<limit;i++)
	{
		if(p[i]<=time&&fork[i]==1)//当该ip的过期时间已到,且状态为已分配时,修改为未分配
		{
			q[i]="NULL";
			p[i]=0;
			fork[i]=0;
		}
	}
}

这四个函数便可以实现一个简单的DHCP服务器,功能全部完成了。但是上传测试时却发现,代码只能拿70分,剩下的部分出现运行错误。

出现运行错误最大的可能性是数组溢出,在反复检查了自己的代码后,我确定代码本身没有造成数组溢出的可能性,那么原因就在于我没有考虑到一种特殊的输入。在仔细审题后,很无语的发现题目里并没有关于ip大小的限制,也就是说,REQ报文中的ip有可能超出主机有的ip,这造成了数组溢出。找到原因后就很简单了,加一个简单判断ip是否在主机范围内就行。

以下贴上完整AC代码

#include<iostream>
#include<string.h>
using namespace std;
long int num,Tmin,Tmax,Tnormol;  
long int get_righttime(long int wanttime,long int time)
{
	long int ti;
	if(wanttime==0)
	{ti=time+Tnormol;
	}
	else if(wanttime-time<Tmin)
	{
		ti=time+Tmin;
	}
	else 
	{
		ti=time+Tmax;
		if(ti>wanttime)
		{
			ti=wanttime;
		}
	}
} 
int find(long int *p,string *q,long int time,long int limit,long int wanttime,long int *fork,string username)
{
	long int ti=get_righttime(wanttime,time);
	for(int i=0;i<limit;i++)
	{
		if(q[i]==username)
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	for(int i=0;i<limit;i++)
	{
		if(q[i]=="NULL")
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	for(int i=0;i<limit;i++)
	{
		if(p[i]<=time)
		{
			p[i]=ti;
			fork[i]=1;
			return i;
		}
	}
	return -1;
}
long int whether(long int *p,long int *fork,string *q,long int time,long int limit,string username,long int wanttime,int ip)
{
	long int ti=get_righttime(wanttime,time);
	if(q[ip-1]==username)
		{
			p[ip-1]=ti;
			fork[ip-1]=2;
			return ip;
		}
	return -1;
}
void check(long int *p,string *q,long int time,long int limit,string usname,long int *fork)
{
	for(int i=0;i<limit;i++)
	{
		if(q[i]==usname&&fork[i]==1)
		{
			q[i]="NULL";
			p[i]=0;
			fork[i]=0;
		}
	}
}
void checkd(long int *p,string *q,long int time,long int limit,long int *fork)
{
for(int i=0;i<limit;i++)
	{
		if(p[i]<=time&&fork[i]==1)
		{
			q[i]="NULL";
			p[i]=0;
			fork[i]=0;
		}
	}
}
int main()
{
	string majorname; 
	cin>>num>>Tnormol>>Tmax>>Tmin>>majorname;
	string messger[10020];
	for(int i=0;i<num;i++)
	{
		messger[i]="NULL";
	}
	long int timechi[10020]={0};
	long int state[10020]={0};
	long int turn;
	cin>>turn;
	while(turn)
	{
		long int n,ip,Tma;
	    string usrname,askedname,requir;
	    cin>>n>>usrname>>askedname>>requir>>ip>>Tma;
	    checkd(timechi,messger,n,num,state);
		if(askedname=="*"&&requir=="DIS")
		{
			long int usedbol=find(timechi,messger,n,num,Tma,state,usrname);
			if(usedbol>=0)
			{
				messger[usedbol]=usrname;
				cout<<majorname<<" "<<usrname<<" OFR "<<usedbol+1<<" "<<timechi[usedbol]<<endl;
			}
		}
		else if(requir=="REQ") 
		{
			if(askedname==majorname)
			{
				if(ip>num||ip<=0)
	            {
	            	cout<<majorname<<" "<<usrname<<" NAK "<<ip<<" 0"<<endl;
	            	continue;
	         	}
				long int usedbol=whether(timechi,state,messger,n,num,usrname,Tma,ip);
				if(usedbol>0)
				{
				    cout<<majorname<<" "<<usrname<<" ACK "<<usedbol<<" "<<timechi[usedbol-1]<<endl;
			    }
			    else 
		    	{
			      	cout<<majorname<<" "<<usrname<<" NAK "<<ip<<" 0"<<endl;
		      	}
		    }
		    else if(askedname=="*")
		    {
			}
			else 
			{
				check(timechi,messger,n,num,usrname,state);
			}
		}
		turn--;
	}
	return 0;
} 

说明: 1, 暂未实现重传机制, 所以若抓包无响应, 请尝试停止后重发. 2, dhcp状态显示采用1s定时器刷新, 所以状态显示可能存在延时的情况; 3, xcap通过pcap导入报文会有部分字段自动变化, 且导入的报文DHCP数据部分无法正常解析, 建议通过新建的方式解决; 4, 添加报文格式举例: 1,2 说明: 1表示报文组1, 选中报文组后, 在状态栏会显示报文组的索引, 2表示第三个报文, 即索引为3的报文. 版本记录: V1.0.1(基础版本) 1, 支持连接xcap并读取报文功能; 2, 支持刷新按钮自动更新报文功能; 3, 支持选择网卡功能; 4, 支持通过pcap文件打开报文功能(已废弃); 5, 支持指定服务器交互; 6, 支持dhcp交互状态显示; 7, 支持输入框通过正则表达式限制输入字符; 8, 支持选择特定报文操作; V1.0.2 1, 将状态修改为自动显示, 即动态识别报文类型并显示结果; 2, 解决解析option字段, 若字段中存在多个value时存在丢失的问题; 3, 增加鼠标点击状态显示气泡信息; 4, 增加隔行显示不同颜色; V1.0.3 1, 修改dhcp的状态机, 之前的版本是收到报文则发送request, 之后收到报文则认为收到ack. 现修改为只有收到offer报文才发送request报文 , 收到ack报文才结束. 2, 增加dhcpv6功能; 3, 优化代码; V1.0.4 1, 修改request报文由于校验和和报文长度未初始化导致构造错误的问题 V1.0.5 1, 增加服务器地址的气泡提示; 2, 增加自动填充的气泡提示; 3, 添加的报文默认为选中状态; 4, 选择网卡下拉框中将虚拟网卡排放靠后; 5, 关闭程序时自动保存设置; V1.0.6 1, 优化代码, 将字段设置使用统一的函数处理; 2, 状态气泡显示格式化; 3, 双击表格表头实现全选和反选; 4, 增加renew(50%), rebind(87.5%)和release的自动发送功能; 5, 增加手动释放按钮和实现; 6, 增加部分打印信息用于调试; 暂未实现报文重传机制, 计划下一个版本实现 V1.0.7 1, 实现discover/solicit报文自动重传机制 2, renew, rebind以及release修改为手动发送 3, 解决报文发送错乱问题 4, 增加decline报文的发送 5, 解决设备无故发送discover报文问题 问题解决: 1, 停止后再次发送数据会出现数据错乱 分析: 停止客户端的时候, 删除过滤器是通过callback函数删除的, 这里应该是通过filter来进行删除. self.widget.sniff.del_filter(self.callback)修改为 self.widget.sniff.del_filter(self.filter) 2, 设备无故发送discover问题 分析: 由于发送discover报文使用的定时器, 定时器是通过判断当前的direction来确定是否重传的, 而当定时器老化时, 可能正好收到报文导 致direction被修改, 所以导致错误的发送discover报文的问题. 将接收逻辑修改为重传时判断当前状态是否为discover报文, 若是则重传, 否则不重传. V1.0.8 1, 增加inform实现 V1.0.9 1, 增加报文五元组的源mac地址和xid的气泡显示; 2, 增加步长和报文限制功能; 问题解决: 1, 修改ipv6报文添加失败的问题. 由于ipv4报文为xid, ipv6报文为trid, 需要区分处理. V1.0.10 1, 在发送dhcpv6报文之前, 先发送na报文触发服务器学习nd消息. V1.0.11 1, 增加发送solicit/request前, 自动响应ns报文. 自动响应ns报文的目标地址为solicit/request报文源mac地址生成的ipv6地址 2, 实现dhcpv6的renew续约功能. 3, 解决ipv6地址转换格式化不正确, 导致无法响应ns报文问题. 4, 增加日志输出到dhcp.log文件. V1.0.12 1, 增加dhcpv6的续约功能, 通过renew和rebind实现续约, 增加release、decline报文的实现; V1.0.13 1, 解决服务器无法设置ipv6地址的问题. 之前的输入框只允许输入数字和., 修改为运行输入数字.:和a-f 2, 解决多个客户端时, 若选中其中的部分客户端发送时报错. 由于客户端采用的是列表中包含元组的形式, 即[(row, [client1, client2])], 这样实际客户端无法直接通过row索引到clients, 导致列表读 取时溢出. 譬如有1、2、3三行数据, 这里只选中了第三行, 限制为1, 那么如果点击发送, 则clients = [(row, [client1]], 此时clients[2] 就会溢出. 所以这里讲clients修改为字典, 即通过row来索引客户端client = {3: [client1]} 3, 将数据发送放到线程中, 规避模拟大量客户端时界面假死的问题. 4, 当客户端限制小于等于50, 则气泡显示trid和ip地址信息. 当大于50, 则气泡显示获取ip地址的数量. V1.0.14 1, 解决监听报文使用的网卡不正确问题. V1.0.15 1, 解决dhcpv6的响应报文的IANA中包含Status code选项导致程序无法解析的问题. 兼容性处理, 即option为IAAddress时按照IAAddress解析, 当option为status code时按照Status code解析 V1.0.16 1, 解决DHCPv6的client_id的duid处理, 支持任意格式的duid.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值