NetBIOS网络编程

NetBIOS网络编程

        “网络基本输入/输出系统”(Network Basic Input/Output System,NetBIOS)是1983年由Sytex公司为IBM公司开发的一种标准应用程序编程接口,并被微软采用。1985年,IBM改进了NetBIOS,推出了NetBIOS扩展用户接口(NetBIOS Extended User Interface,NetBEUI)通信协议,它占用内存少,配置简单,适用于小型局域网不同计算机之间的通信,但不具有跨网段工作的能力,不支持路由机制。NetBIOS是一种与“协议无关”的编程接口,它使应用程序不用理解网络细节,应用程序可通过TCP/IP、NetBEUI、SPX/IPX运行。

        它定义了一种软件接口以及在应用程序连接介质之间提供通信接口的标准方法,它可以人提供名字服务、会话服务和数据库服务,基于NetBIOS的比较典型的应用是远程计算机的Mac地址、名称和所在工作组等信息。

 

PS:网上很多说NetBIOS网络编程已经过时了,不过还是稍做了解吧~

(以下为个人概括的五点,如有什么不是很对的地方,欢迎指正)

重点1:它是基于会话层的服务。

重点2:它可以让两台机器相互连接,建立通信。

重点3:它可以基于UDP或者TCP实现。

重点4:实现这个NetBIOS与基本的TCP/IP编程的最大不同点就是向另一台机发送的是NetBIOS请求包,接收的是NetBIOS回应包,然后对NetBIOS回应包进行解释,其编程思路与TCP/IP编程基本相似,只是一些recv和send的东西不同。

重点5: 它的实现,也可以在“cmd”中使用nbtstat指令体现。

 

下面是三个与NetBIOS网络编程稍有关系的实例应用:

01. GetNetBIOSNames.cpp -- 获取LANA上所有NetBIOS名字

02. GetMacAddress.cpp -- 获取网络适配器上的MAC地址

03. NbtStat.cpp -- 获取网络中指定计算机的基本信息

 

源代码:

01. GetNetBIOSNames.cpp -- 获取LANA上所有NetBIOS名字

// GetNetBIOSNames.cpp -- 获取LANA上所有NetBIOS名字

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <Nb30.h>

#pragma comment(lib, "netapi32.lib")

// Set LANANUM and LOCALNAME as appropriate for your system
#define LANANUM     0
#define LOCALNAME   "UNIQUENAME"
#define NBCheck(x)  if (NRC_GOODRET != x.ncb_retcode) { \
	                    printf("Line %d: Got 0x%x from NetBios()\n", \
                               __LINE__, x.ncb_retcode); \
                    }

void MakeNetbiosName (char *, LPCSTR);
BOOL NBReset (int, int, int);
BOOL NBAddName (int, LPCSTR);
BOOL NBListNames (int, LPCSTR);
BOOL NBAdapterStatus (int, PVOID, int, LPCSTR);

int main()
{
	// 初始化,清空本地名字表和会话表
	if (!NBReset (LANANUM, 20, 30)){
		return -1;
	}
	// 向本地名字表中添加UNIQUENAME
    if (!NBAddName (LANANUM, LOCALNAME)) {
		return -1;
	}
	// 列出本地名字表中的名字
    if (!NBListNames (LANANUM, LOCALNAME)) {
		return -1;
	}
    printf ("Succeeded.\n");
	system("pause");
	return 0;
}

/**
 * NCB结构体的定义:
 * typedef struct _NCB{
 *   UCHAR ncb_command;  // 指定命令编码以及表明NCB结构体是否被异步处理的标识
 *   UCHAR ncb_retcode;  // 指定命令的返回编码
 *   UCHAR ncb_lsn;      // 表示本地会话编号,在指定环境中此编号唯一标识一个会话
 *   UCHAR ncb_num;      // 指定本地网络名字编号。
 *   PUCHAR ncb_buffer;  // 指定消息缓冲区。(有发送信息、接收信息、接收请求状态信息三个缓冲区)
 *   WORD ncb_length;    // 指定消息缓冲区的大小,单位为字节。
 *   UCHAR ncb_callname[NCBNAMSZ]; // 指定远程端应用程序的名字
 *   UCHAR ncb_name[NCBNAMSZ];     // 指定应用程序可以识别的名字
 *   UCHAR ncb_rto;                // 指定会话执行接收操作的超时时间
 *   void (CALLBACK * ncb_post)(struct NCB);
 *   UCHAR ncb_lana_num;           // 指定LANA编号
 *   UCHAR ncb_cmd_cplt;           // 指定命令完成标识
 *   UCHAR ncb_reserve[X];         // 保留字段
 *   HANDLE ncb_event;             // 指向事件对象的句柄
 * }NCB, *PNCB;
 **/


// 清空本地名字和会话表
BOOL NBReset (int nLana, int nSessions, int nNames)
{
    NCB ncb;
    memset (&ncb, 0, sizeof (ncb));		// 清空ncb结构体
    ncb.ncb_command = NCBRESET;			// 执行NCBRESET命令,复位局域网网络适配器,清空指定LANA编号上定义的本地名字表和会话表。
    ncb.ncb_lsn = 0;					// 分配新的lana_num资源,Netbios()函数成功执行了NCBCALL命令后返回的编号
    ncb.ncb_lana_num = nLana;			// 设置lana_num资源,指定本地网络名字编号。
    ncb.ncb_callname[0] = nSessions;	// 设置最大会话数
    ncb.ncb_callname[2] = nNames;		// 设置最大名字数
    Netbios (&ncb);						// 执行NCBRESET命令
    NBCheck (ncb);						// 如果执行结果不正确,则输出ncb.ncb_retcode
	// 如果成功返回TRUE,否则返回FALSE
    return (NRC_GOODRET == ncb.ncb_retcode);
}


// 向本地名字表中添加名字
BOOL NBAddName (int nLana, LPCSTR szName)
{
    NCB ncb;
    memset (&ncb, 0, sizeof (ncb));		// 清空ncb结构体
    ncb.ncb_command = NCBADDNAME;		// 执行NCBDDNAME命令,向本地名字表中添加一个唯一的名字
    ncb.ncb_lana_num = nLana;			// 设置lana_num资源,指定本地网络名字编号。
	MakeNetbiosName ((char*) ncb.ncb_name, szName); // 将szName赋值到ncb.ncb_name中
    Netbios (&ncb);						// 执行NCBRESET命令
    NBCheck (ncb);						// 如果执行结果不正确,则输出ncb.ncb_retcode
	// 如果成功返回TRUE,否则返回FALSE
    return (NRC_GOODRET == ncb.ncb_retcode);
}


// Build a name of length NCBNAMSZ, padding with spaces.
// 将szSrc中的名字赋值到achDest中,名字的长度为NCBNAMESZ
// 如果不足,则使用空格补齐
void MakeNetbiosName (char *achDest, LPCSTR szSrc)
{
    int cchSrc = strlen ((char*)szSrc);	// 取名字的长度
    if (cchSrc > NCBNAMSZ){
        cchSrc = NCBNAMSZ;
	}
    memset (achDest, ' ', NCBNAMSZ);
    memcpy (achDest, szSrc, cchSrc);
}

// 列出指定LANA上所有的名字
BOOL NBListNames (int nLana, LPCSTR szName)
{
    int cbBuffer;					// 获取数据的缓冲区
    ADAPTER_STATUS *pStatus;		// 保存网络适配器的信息
    NAME_BUFFER *pNames;			// 保存本地名字信息
    HANDLE hHeap;					// 当前调用进程的堆句柄

    hHeap = GetProcessHeap();		// 当前调用进程的堆句柄
    cbBuffer = sizeof (ADAPTER_STATUS) + 255 * sizeof (NAME_BUFFER);// 分配可能的最大缓冲区空间
	pStatus = (ADAPTER_STATUS *) HeapAlloc (hHeap, 0, cbBuffer);// 为pStatus分配空间
    if (NULL == pStatus){
        return FALSE;
	}
	// 获取本地网络适配器信息,结果保存到pStatus中
    if (!NBAdapterStatus (nLana, (PVOID) pStatus, cbBuffer, szName)){
        HeapFree (hHeap, 0, pStatus);
        return FALSE;
    }
    // 列出跟在ADAPTER_STATUS结构体后面的名字信息
    pNames = (NAME_BUFFER *) (pStatus + 1);
    for (int i = 0; i < pStatus->name_count; i++){
        printf ("\t%.*s\n", NCBNAMSZ, pNames[i].name);
	}

    HeapFree (hHeap, 0, pStatus);// 释放分配的堆空间

    return TRUE;
}

// 获取指定LANA的网络适配器信息
// nLana, LANA编号
// pBuffer, 获取到的网络适配器缓冲区
// cbBuffer, 缓冲区长度
// szName, 主机名字
BOOL NBAdapterStatus (int nLana, PVOID pBuffer, int cbBuffer,  LPCSTR szName)
{
    NCB ncb;
    memset (&ncb, 0, sizeof (ncb));		// 清空ncb结构体
    ncb.ncb_command = NCBASTAT;			// 设置执行NCBASTAT命令,获取本地或远程网络适配器的状态
    ncb.ncb_lana_num = nLana;			// 设置LANA编号

    ncb.ncb_buffer = (PUCHAR) pBuffer;	// 将获取到的数据保存到参数pBuffer中
    ncb.ncb_length = cbBuffer;			// 设置缓冲区长度

    MakeNetbiosName ((char*) ncb.ncb_callname, szName);// 设置参数ncb.ncb_callname
    Netbios (&ncb);						// 执行NetBIOS命令
    NBCheck (ncb);						// 如果执行不成功,则输出返回值
	// 如果成功返回TRUE,否则返回FALSE
    return (NRC_GOODRET == ncb.ncb_retcode);
}


 

02. GetMacAddress.cpp -- 获取网络适配器上的MAC地址

// GetMacAddress.cpp -- 获取网络适配器上的MAC地址

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <Nb30.h>

#pragma comment(lib, "netapi32.lib")


/**
 * ADAPTER_STATUS结构体中包含网络适配器的信息
 * typedef struct _ADAPTER_STATUS{
 *   UCHAR adapter_address[6];  // 指定网络适配器的地址
 *   UCHAR rev_major;           // 指定发布软件的主版本号
 *   UCHAR reserved0;           // 保留字段,始终为零
 *   UCHAR adapter_type;        // 指定网络适配器的类型
 *   UCHAR rev_minor;           // 指定发布软件的副版本号
 *   WORD duration;             // 指定报告的时间周期,单位为分钟
 *   WORD frmr_recv;            // 指定接收到的FRMR(帧拒绝)帧数量
 *   WORD frmr_xmit;            // 指定传送的FRMR帧数量
 *   WORD iframe_recv_err;      // 指定接收到的错误帧数量
 *   WORD xmit_aborts;          // 指定终于传输的包数量
 *   DWORD xmit_success;        // 指定成功传输的包数量
 *   DWORD recv_success;        // 指定成功接收的包数量
 *   DWORD iframe_xmit_err;     // 指定传输的错误帧数量
 *   WORD recv_buf_unavail;     // 指定缓冲区无法为远程计算机提供服务次数
 *   WORD tl_timeouts;          // 指定DLC(Data Link Control, 数据链路控制)T1计数器超时的次数
 *   WORD ti_timeouts;          // 指定ti非活动计时器超时的次数。ti计时器用于检测断开的连接
 *   DWORD reservedl;           // 保留字段,始终为0
 *   WORD free_ncbs;            // 指定当前空闲的网络控制块的数量
 *   WORD max_cfg_ncbs;         // 最大网络控制块数据包的大小
 *   WORD max_ncbs;             // 最大网络控制块的数量
 *   WORD xmit_buf_unavail;     // 不可用的传输包的缓冲区
 *   WORD max_dgram_size;       // 包的最大值
 *   WORD pending_sess;         // 指定挂起会话的数量
 *   WORD max_cfg_sess;         // 指定数据包的最大大小,该值至少为512字节
 *   WORD max_sess;             // 最大数量
 *   WORD max_sess_pkt_size;    // 指定会话数据包的最大大小
 *   WORD name_count;           // 指定本地名字表中名字的数量
 * }ADAPTER_STATUS, *PADAPTER_STATUS;
 **/


// 结构体ASTAT用于定义网络适配器状态和名字表信息
typedef struct _ASTAT_
{
    ADAPTER_STATUS adapt;		// 网络适配器状态
    NAME_BUFFER NameBuff [30];	// 名字表信息
}ASTAT, * PASTAT;

ASTAT Adapter;

int main()
{
	NCB ncb;						// NCB结构体,用于设置执行的NetBIOS命令和参数
    UCHAR uRetCode;					// 执行Netbios()函数的返回值
    memset( &ncb, 0, sizeof(ncb) );	// 初始化ncb结构体
    ncb.ncb_command = NCBRESET;		// 设置执行NCBRESET,复位网络适配器
    ncb.ncb_lana_num = 0;			// 设置LANA编号

    uRetCode = Netbios( &ncb );		// 调用Netbios()函数,执行NCBRESET命令
	// 输出执行NCBRESET命令的结果
    printf( "The NCBRESET return code is: 0x%x \n", uRetCode );

    memset( &ncb, 0, sizeof(ncb) );	// 初始化ncb
    ncb.ncb_command = NCBASTAT;		// 执行NCBASTAT命令,获取网络适配器状态
    ncb.ncb_lana_num = 0;			// 设置LANA编号
	// 设置执行NCBASTAT命令的参数,将获取到的网络适配器数据保存到Adapter结构体中
    memcpy( &ncb.ncb_callname, "*               ", 16 );
    ncb.ncb_buffer = (UCHAR*) &Adapter;
    ncb.ncb_length = sizeof(Adapter);
    uRetCode = Netbios( &ncb );		// 调用Netbios()函数,执行NCBASTAT命令
    printf( "The NCBASTAT return code is: 0x%x \n", uRetCode );
    if ( uRetCode == 0 ) {			// 输出MAC地址
        printf( "The Ethernet Number is: %02x-%02x-%02x-%02x-%02x-%02x\n",
                Adapter.adapt.adapter_address[0],
                Adapter.adapt.adapter_address[1],
                Adapter.adapt.adapter_address[2],
                Adapter.adapt.adapter_address[3],
                Adapter.adapt.adapter_address[4],
                Adapter.adapt.adapter_address[5] );
    }
	system("pause");
	return 0;
}


 

 

03. NbtStat.cpp -- 获取网络中指定计算机的基本信息

// NbtStat.cpp -- 获取网络中指定计算机的基本信息

#include <string.h>
#include <stdio.h>
#include <winsock2.h>
#include <map>

#pragma comment(lib, "Ws2_32.lib")

using namespace std;


class CDevice
{
public:
	CDevice(void){}
	CDevice(string ip):IP(ip), Name (""), Mac(""), Workgroup(""){}

public:
	~CDevice(void){}

public:
	string IP;			// IP地址
	string Name;		// 名称
	string Mac;			// Mac地址
	string Workgroup;	// 工作组
};



// 通过NbtStat获取计算机名字信息的结构体
struct names
{
	unsigned char nb_name[16];	// 表示接收到的名字
	unsigned short name_flags;	// 标识名字的含义
};


// 保存获取NetBIOS信息的Socket和IP地址列表
struct workstationNameThreadStruct
{
	SOCKET s;					// 指定发送和接收NetBIOS数据包的Socket
	std::map<unsigned long, CDevice*> *ips;	// 指定获取NetBIOS信息的IP地址列表
};

// 格式化ethernet中的字节为字符串
void GetEthernetAdapter(unsigned char *ethernet, char *macstr)
{
	sprintf(macstr, "%02x %02x %02x %02x %02x %02x",
		ethernet[0],	ethernet[1],		ethernet[2],
		ethernet[3],	ethernet[4],		ethernet[5]
	);
	return;
}


// 获取设备的名称和MAC地址等信息
/**
 * GetHostInfo()函数的运行过程
 * 创建发送和接收NetBIOS数据包的Socket, 并将其绑定在本地地址的端口0上。
 * 创建接收NetBIOS回应包的线程,参数unionStruct中包含要获取NetBIOS信息的IP地址和通信用的Socket。
 * 使用for循环语句依次向每个IP地址发送NetBIOS请求包,请求包数据保存在字符数组input中。
 * 调用WaitForSingleObject()函数,等待接收线程结束。
 * 如果超时,则结束接收线程NetBiosRecvThreadProc()函数。
 * 关闭线程句柄,释放资源。
 **/
void GetHostInfo(std::map<unsigned long, CDevice*> &ips, int timeout)
{
	DWORD WINAPI NetBiosRecvThreadProc(void *param);

	const int defaultPort = 0;      // 设定默认的绑定端口
	SOCKET sock;					// 通信套接字
	struct sockaddr_in origen;		// 本地地址
	WSADATA wsaData;				// Windows Sockets环境变量

	if(WSAStartup(MAKEWORD(2,1),&wsaData) != 0){// 初始化Windows Sockets环境
		return;
	}
	if(INVALID_SOCKET ==(sock = socket(AF_INET, SOCK_DGRAM,IPPROTO_UDP))){// 创建TCP/IP套接字
		return;
	}
	// 设置超时时间
	if(SOCKET_ERROR ==setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,sizeof(timeout))){
		closesocket(sock);
		WSACleanup();
		return;
	}
	// 将套接字绑定到本地地址和端口0
	memset(&origen, 0, sizeof(origen));
	origen.sin_family = AF_INET;
	origen.sin_addr.s_addr = htonl (INADDR_ANY);
	origen.sin_port = htons (defaultPort);
	if (bind (sock, (struct sockaddr *) &origen, sizeof(origen)) < 0) { // 套接字连接
		closesocket(sock);
		WSACleanup();
		return;
	}

	// 为创建接收线程准备数据
	workstationNameThreadStruct unionStruct;
	unionStruct.ips = &ips;
	unionStruct.s = sock;
	// 启动线程等待接收NetBIOS回应包
	DWORD pid;
	HANDLE threadHandle = CreateThread(NULL, 0, NetBiosRecvThreadProc,(void *)&unionStruct, 0, &pid);

	// 依次向ips中的每个IP地址的137端口发送NetBIOS请求包(保存在input字符数组中)
	std::map<unsigned long, CDevice*>::iterator itr;
	for(itr=ips.begin();itr != ips.end();itr++)
	{
		char input[]="\x80\x94\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x20\x43\x4b\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00\x00\x21\x00\x01";
		struct sockaddr_in dest;
		// 发送NetBios请求信息
		memset(&dest,0,sizeof(dest));
		dest.sin_addr.s_addr = itr->first;
		dest.sin_family = AF_INET;
		dest.sin_port = htons(137);
		sendto (sock, input, sizeof(input)-1, 0, (struct sockaddr *)&dest, sizeof (dest));
	}
	// 等待接收线程NetBiosRecvThreadProc结束
	DWORD ret = WaitForSingleObject(threadHandle, timeout * 4);
	// 如果超时,则结束接收线程NetBiosRecvThreadProc
	if(ret == WAIT_TIMEOUT){
		TerminateThread(threadHandle, 0);
	}
	else{
		printf("thread success exit\n");
	}
	// 关闭线程句柄
	CloseHandle(threadHandle);

	// 释放资源
	closesocket(sock);
	WSACleanup();
}




// 获取计算机名称以及MAC地址的函数线程
/**
 * NetBiosRecvThreadProc()函数运行过程
 * 在while循环中调用recvfrom()函数,在套接字sock上接收数据.
 * 如果超时,则多重试两次。
 * NetBIOS回应包的前56位是网络适配器状态信息,第57位保存名字表中名字的数量。
 * 依次处理名字表中每个名字项,如果最后一位是0x00,则表示当前名字项用于保有存计算机名或者工作组。
 *  name_flags字段可以区分当前名字是计算机名还是工作组。
 * 在NetBIOS回应包中,包字表后面的6个字节是计算机的MAC地址。
 *   调用GetEthernetAdapter()函数可以将其转换为字符串。
 * 将获取到的计算机名、工作组和MAC地址保存到ips映射表项中的CDevice对象中。
 **/

DWORD WINAPI NetBiosRecvThreadProc(void *param)
{
	char respuesta[1000];					// 保存接收到的NetBIOS
	unsigned int count=0;					// 用于在NetBIOS回应包中定位名字数组的位置
	unsigned char ethernet[6];			// 保存MAC地址
	struct sockaddr_in from;				// 发送NetBIOS回应包
	// 参数是要获取NetBIOS信息的IP地址和套接字
	workstationNameThreadStruct *unionStruct = (workstationNameThreadStruct *)param;
	SOCKET sock = unionStruct->s;	// 用于接收NetBIOS回应包的套接字
	// 要获取NetBIOS信息的IP地址
	std::map<unsigned long, CDevice*> *ips = unionStruct->ips;
	int len = sizeof (sockaddr_in);		// 地址长度
	// 定义名字数组
	struct names Names[20*sizeof(struct names)];

	int ipsCount = ips->size();			// 要获取NetBIOS信息的IP地址数量
	int timeoutRetry = 0;					// 记录超时重试的次数
	while(true){
		count = 0;
		// 在套接字sock上接收消息
		int res= recvfrom (sock, respuesta, sizeof(respuesta), 0, (sockaddr *)&from, &len);
		// 如果超时,则重试,但重试次数不超过两次
		if(res == SOCKET_ERROR)	{
			if(GetLastError() == 10060)	{
				timeoutRetry++;
				if(timeoutRetry == 2)
					break;
			}
			continue;
		}
		if(res <= 121){
			continue;
		}

		// 将count定位到名字表中名字的数量。在NetBIOS回应包中,前面56位是网络适配器的状态信息
		memcpy(&count, respuesta+56, 1);
		if(count > 20){		// 最多有20个名字,超出则错误
			continue;
		}

		// 将名字表内在复制到Names数组中
		memcpy(Names,(respuesta+57), count*sizeof(struct names));

		// 将空格字符替换成空
		for(unsigned int i = 0; i < count;i++) {
			for(unsigned int j = 0;j < 15;j++){
				if(Names[i].nb_name[j] == 0x20)
					Names[i].nb_name[j]=0;
			}
		}

		string mac;
		// 如果发送回应包的地址在ips中,则处理该包
		std::map<unsigned long, CDevice*>::iterator itr;
		if( (itr = ips->find(from.sin_addr.S_un.S_addr)) != ips -> end()){
			// 获取发送NetBIOS回应包的IP地址
			in_addr inaddr;
			inaddr.S_un.S_addr = itr->first;
			itr->second->IP = inet_ntoa(inaddr);
			// 处理名字表中的所有名字
			for(int i=0;i<count;i++){
				// 如果最后一位是0x00,则表示当前名字表项为保存计算机名或者工作组
				if(Names[i].nb_name[15] == 0x00){
					char buffers[17] = {0};
					memcpy(buffers, Names[i].nb_name, 16);
					// 使用name_flags字段来区分当前名字是计算机名还是工作组
					if((Names[i].name_flags & 128) == 0) {
						itr->second->Name = buffers;
					}
					else{
						itr->second->Workgroup = buffers;
					}
				}
				// 名字表后面是MAC地址
				memcpy(ethernet,(respuesta+57+count*sizeof(struct names)),6);
				char mac[20] = {0};
				// 格式化MAC地址
				GetEthernetAdapter(ethernet,mac);
				itr->second->Mac = mac;
			}
		}
	}

	return 0;
}



int main()
{
	std::map<unsigned long, CDevice*> ips;
	// 向ips中添加一个设备
	CDevice dev1("10.5.1.5");
	unsigned long ip1 = inet_addr(dev1.IP.c_str());
	ips.insert(make_pair(ip1, &dev1));
	// 向ips中添加第2个设备
	CDevice dev2("10.10.10.1");
	unsigned long ip2 = inet_addr(dev2.IP.c_str());
	ips.insert(make_pair(ip2, &dev2));

	// 获取设备信息
	GetHostInfo(ips, 2000);
	std::map<unsigned long, CDevice*>::iterator itr;
	for(itr = ips.begin(); itr != ips.end(); itr++)
	{
		printf("\nIP: %s;  \nName: %s;  \nMac: %s;  \nWorkgroup:  %s\n\n",
			itr->second->IP.c_str(), itr->second->Name.c_str(), itr->second->Mac.c_str(), itr->second->Workgroup.c_str());
	}
	system("pause");
	return 0;
}


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值