Select网络模型2.0(封装模式)

1.实现功能,客户端,线程端不因收发数据阻塞
2.实现封装

一。消息结构文件

//用来存放消息结构

//传输的数据结构,最简单的数据包
//每个函数的类型必须一样,而且在客户端和服务端传输和接收顺序一致,也就是内存对齐
//long类型的在64位编译器下就是64位,而32位编译器下就是32位,
//所以需要考虑平台和系统,关注是否内存对齐

enum CMD
{
	CMD_LOGIN,
	CMD_LOGIN_RESULT,
	CMD_LOGOUT_RESULT,
	CMD_LOGOUT,
	CMD_NEW_USER_JOIN,
	CMD_ERROR
};


//消息结构体
struct DataHeader {
	short dataLength;//数据长度
	short cmd;//命令:接收的命令,服务器处理后反馈数据。
};
//定义登录的数据结构
//使用继承方式,是报文更完整,不容易出错,也不用每次单独定义dataheader
struct Login :public DataHeader {
	Login()
	{
		dataLength = sizeof(Login);
		cmd = CMD_LOGIN;
	}
	char userName[32];
	char PassWord[32];
};

struct LoginResult :public DataHeader {
	LoginResult()
	{
		dataLength = sizeof(LoginResult);
		cmd = CMD_LOGIN_RESULT;
	}
	int result;
};

struct LogoutResult :public DataHeader {
	LogoutResult()
	{
		dataLength = sizeof(LogoutResult);
		cmd = CMD_LOGOUT_RESULT;
	}
	int result;
};

struct NewUserJoin :public DataHeader {
	NewUserJoin()
	{
		dataLength = sizeof(NewUserJoin);
		cmd = CMD_NEW_USER_JOIN;
		sock = 0;
	}
	int sock;
};

struct Logout :public DataHeader {
	Logout()
	{
		dataLength = sizeof(Logout);
		cmd = CMD_LOGOUT;
	}
	char userName[32];
};

二.客户端文件

EasyTcpClient.hpp

/*用封装的形式,将客户端封装起来*/

//防止重编译
#ifndef _EasyTcpClient_hpp_
#define _EasyTcpClient_hpp_
//inet_addr
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
	#include<Windows.h>
	#include<WinSock2.h>
	//链接了ws2_32这个库
	#pragma comment(lib,"ws2_32.lib")
#else
	#include<unistad.h>
	#include<arpa/inet.h>
	#include<string.h>
#endif // _WIN32

#include"MessageHeader.hpp"
#include<iostream>

class EasyTcpClient
{
	SOCKET _sock;
public:
	EasyTcpClient()
	{
		_sock = INVALID_SOCKET;
	}

	//虚析构函数
	virtual ~EasyTcpClient()
	{
		Close();
	}

	//初始化Socket
	int initSocket() 
	{
#ifdef _WIN32


		//启动Win Sock 2.x环境
		WORD ver = MAKEWORD(2, 2);
		WSADATA dat;
		WSAStartup(ver, &dat);

#endif // _WIN32
		//1.建立一个Socket
		//如果这个socket二次调用,有链接的话,就关闭掉连接,然后重新赋值连接
		if (INVALID_SOCKET != _sock) {
			printf("socket=%d关闭了旧链接\n",_sock);
			Close();
		}
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (INVALID_SOCKET == _sock)
		{
			printf("错误,建立socket失败。。\n");
			return -1;
		}
		else
		{
			printf("建立socket成功\n");
			return 1;
		}
		
	}

	//链接服务器
	int Connect(const char *ip, unsigned short port)
	{
		//如果不是一个有效连接,初始化链接。防止忘记调用初始化socket
		if (INVALID_SOCKET == _sock) {
			initSocket();
		}
		//2.连接服务器
		sockaddr_in _sin = {};
		_sin.sin_family = AF_INET;
		_sin.sin_port = htons(port);
#ifdef _WIN32
		_sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
		_sin.sin_addr.S_addr = inet_addr(ip);
#endif // _WIN32
		int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
		if (SOCKET_ERROR == ret)
		{
			printf("错误,连接服务器失败。\n");
		}
		else
		{
			printf("连接服务器成功。。。\n");
		}
		return ret;

	}

	//关闭Socket
	void Close()
	{
		//关闭Win Sock 2.x环境
		//if 防止主动关闭socket后,析构再调用关闭,产生问题。INVALID_SOCKET就是关闭的socket
		if (_sock != INVALID_SOCKET)
		{
#ifdef _WIN32
			closesocket(_sock);
			WSACleanup();
#else
			close(_sock);
#endif // _WIN32
			_sock = INVALID_SOCKET;
		}
	}

	//查询网络消息
	bool onRun()
	{
		if (isRun())
		{
			fd_set fdReads;
			FD_ZERO(&fdReads);
			FD_SET(_sock, &fdReads);
			timeval t = { 1,0 };
			int ret = select(_sock + 1, &fdReads, 0, 0, &t);
			if (ret < 0)
			{
				printf("<socket=%d>Select 任务结束\n", _sock);
				return false;
			}
			if (FD_ISSET(_sock, &fdReads))
			{
				FD_CLR(_sock, &fdReads);
				if (-1 == RecvData(_sock))
				{
					printf("<socket=%d>selesct任务结束\n", _sock);
					return false;
				}
			}
			return true;
		}
		return false;
	}

	//判断是否运行
	bool isRun()
	{
		return _sock != INVALID_SOCKET;
	}

	//收数据,处理粘包,拆分包
	int RecvData(SOCKET _serverSock) {
		//第一次接受一个缓冲数据,设置缓冲区大小
		char szRecv[1024] = {};
		//5.接收客户端数据
		//第一次收了header的数据包,只剩下CMD的数据长度,指针移动到数据包包体位置
		int nLen = recv(_serverSock, szRecv, sizeof(DataHeader), 0);
		DataHeader* header = (DataHeader*)szRecv;

		if (nLen <= 0)
		{
			printf("与服务器断开连接。\n", _serverSock);
			return -1;
		}
		printf("收到命令:%d ,数据长度:%d\n", header->cmd, header->dataLength);
		//6.处理请求
		
		//拆分包
		//从_serverSock缓冲区,接收DataHeader后的数据,接收的数据长度为数据包-包头的长度
		recv(_serverSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		OnNetMsg(header);

		return 0;
	}

	//响应网络消息
	void OnNetMsg(DataHeader* header)
	{
		switch (header->cmd)
		{
		case CMD_LOGIN_RESULT:
		{
			//因为都是login,logout都是header的子类所以可以相互转化
			LoginResult* login = (LoginResult*)header;
			printf("收到服务器消息:CMD_LOGIN_RESULT,数据长度:%d\n", login->dataLength);

		}
		break;
		case CMD_LOGOUT:
		{
			LogoutResult* logoutret = (LogoutResult*)header;
			printf("收到服务端返回消息:CMD_LOGOUT_RESULT,数据长度:%d\n", logoutret->dataLength);

		}
		break;
		case CMD_NEW_USER_JOIN:
		{
			NewUserJoin* newuser = (NewUserJoin*)header;
			printf("新的SOCKET:%d 加入进服务器\n", newuser->sock);

		}
		break;
		//定义默认的错误信息
		}
	}

	//发送数据
	int SendData(DataHeader* header)
	{
		//isRun is bool && header 是不是为空
		if (isRun() && header)
		{	//int _stdcall send() _stdcall意思为从右往左压入栈(0-》header->dataLength->header->_sock)
			return send(_sock, (const char*)header, header->dataLength, 0);
		}
		return SOCKET_ERROR;
	}

private:

};



#endif

client.cpp


#include "EasyTcpClient.hpp"
#include<thread>


//Thread can input the command
//windows 下可以传easytcpclient 的引用&,其他的不行。所以建议传指针
void cmdThread(EasyTcpClient* client)
{
	while (true)
	{
		char cmdBuf[1024] = {};
		std::cin >> cmdBuf;
		//scanf_s("%s", cmdBuf);
		if (0 == strcmp(cmdBuf, "exit"))
		{
			client->Close();
			printf("Exit the thread\n");
			break;
		}
		else if (0 == strcmp(cmdBuf, "login"))
		{
			Login login;
			strcpy_s(login.userName, "李逵");
			strcpy_s(login.PassWord, "110");
			client->SendData(&login);
		}
		else if(0==strcmp(cmdBuf,"logout"))
		{
			Logout logout;
			strcpy_s(logout.userName, "李逵");
			//因为logout是继承header的结构体,所以发送header也可以发送logout
			client->SendData(&logout);
		}
		else
		{
			printf("The command is not supported!\n");
		}
	}
}

int main()
{
	//start up the thread
	EasyTcpClient client;
	
	//为什么会连接多个socket也就是多个网关
	//同一个客户端宏client 4567 可能连接登录服务器
	//client.initSocket(); 在连接函数中已经判断是否进行初始化
	client.Connect("127.0.0.1",4567);
	Login login;
	strcpy_s(login.userName, "李逵");
	strcpy_s(login.PassWord, "110");
	//启动UI线程
	std::thread t1(cmdThread, &client);
	t1.detach();
	
	while (client.isRun())
	{
		client.onRun();
		client.SendData(&login);
		Sleep(5000);
	}

	client.Close();
	printf("已退出\n");
	getchar();
	return 0;
}

三.服务端
EasyTcpServer.hpp

#ifndef _EasyTcpServer_hpp_
#define _EasyTcpServer_hpp_
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
//winsock2一定要在windows前,否则会有宏定义重编译问题。sock2是新库,windows是老库。
#include<WinSock2.h>
#include<Windows.h>
#pragma comment(lib,"ws2_32.lib")
#else
#include<unistd.h>
#include<arpa/inet.h>
#include<string.h>

#define SOCKET int
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR(-1)
#endif
#include<iostream>
#include<vector>
#include"MessageHeader.hpp"
class EasyTcpServer
{

private:
	SOCKET _sock;
	std::vector<SOCKET> g_clients;
public:
	EasyTcpServer() {
		_sock = INVALID_SOCKET;
	}
	virtual ~EasyTcpServer() {
		Close();
	}

	//Init Socket
	void InitSocket()
	{
#ifdef _WIN32
		//startup windows socket 2.2
		WORD ver = MAKEWORD(2, 2);
		WSADATA dat;
		WSAStartup(ver, &dat);

#endif // _WIN32
		if (INVALID_SOCKET != _sock)
		{
			printf("<socket=%d 关闭旧连接 ...\n", _sock);
			Close();
		}
		_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (INVALID_SOCKET == _sock)
		{
			printf("Error, startup Socket failling ...\n");
		}
		else
		{
			printf("Success! Startup Socket=<%d>...\n", _sock);
		}
	}
	//Blanding port
	//为什么用大写 bind 已经在socket库中被声明定义 
	void Bind(const char* ip, unsigned short port)
	{
		if (INVALID_SOCKET == _sock)
		{
			InitSocket();
		}
		//绑定用于连接客户端的网络端口
		sockaddr_in _sin = {};
		_sin.sin_family = AF_INET;
		_sin.sin_port = htons(port);//忘记加htons转换port导致客户端连接不上服务器
		
#ifdef _WIN32
		//判断IP地址是否为空
		if (ip)
		{
			
			_sin.sin_addr.S_un.S_addr = inet_addr(ip);
		}
		else
		{
			_sin.sin_addr.S_un.S_addr = INADDR_ANY;//任何ip都可以
		}
#else
		if (ip)
		{

			_sin.sin_addr.S_addr = inet_addr(ip);
		}
		else
		{
			_sin.sin_addr.S_addr = INADDR_ANY;
		}
#endif // _WIN32
			int ret = bind(_sock, (sockaddr*)&_sin, sizeof(_sin));
			if (SOCKET_ERROR == ret)
			{
				printf("ERROR!Blinded port<%d> failing..\n",port);
			}
			else
			{
				printf("Success!Blinded port<%d> successful..\n",port);
			}
	}
	/* \breif Listening port
	*  \param n 等待socket的连接数
	*/
	int Listen(int n)
	{
		int ret = listen(_sock, n);
		if (SOCKET_ERROR == ret)
		{
			printf("ERROR! Listening the socket <%d> port failing.\n",(int)_sock);
		}
		else
		{
			printf("SUCCESS! Listening the socket<%d> port success.\n",(int)_sock);
		}
		return ret;
	}
	//Recieve client
	SOCKET Accept()
	{
		sockaddr_in clientAddr = {};
		int nAddrLen = sizeof(sockaddr_in);
		//_cSock clientSocket
		SOCKET _cSock = INVALID_SOCKET;
#ifdef _WIN32
		_cSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
#else
		_cSock = accept(_sock, (sockaddr*)&clientAddr, (socklen_t*)&nAddrLen);
#endif
		if (INVALID_SOCKET == _sock)
		{
			printf("ERROR!socket=<%d> Accept the invalid socket ...\n",(int)_sock);
		}
		else
		{
		
			NewUserJoin userJoin;
			SendDataToAll(&userJoin);
			g_clients.push_back(_cSock);
			printf("Socket=<%d> New User in: socket=%d,IP=%s \n",_sock, (int)_cSock, inet_ntoa(clientAddr.sin_addr));
		}
		return _cSock;
	}
	//Close Socket
	void Close()
	{
		if (_sock != INVALID_SOCKET)
		{
#ifdef _WIN32
			for (int n = (int)g_clients.size() - 1; n >= 0; n--)
			{
				//1.先关闭所有客户端的socket
				closesocket(g_clients[n]);
			}
			//2.再关闭自己的socket
			closesocket(_sock);

			//windows 专用
			WSACleanup();
#else
			for (int n = (int)g_clients.size() - 1; n >= 0; n--)
			{
				closesocket(g_clients[n]);
			}
			closesocket(_sock);

#endif // _WIN32

		}

	}
	
	//Deal msg
	bool OnRun()
	{
		if (isRun())
		{
			fd_set fdRead;
			fd_set fdWrite;
			fd_set fdExp;

			FD_ZERO(&fdRead);
			FD_ZERO(&fdWrite);
			FD_ZERO(&fdExp);

			FD_SET(_sock, &fdRead);
			FD_SET(_sock, &fdWrite);
			FD_SET(_sock, &fdExp);

			SOCKET maxSock = _sock;
			// 记得-1 因为vector从0开始否则vector越界
			for (int n = (int)g_clients.size()-1; n >=0; n--)
			{
				FD_SET(g_clients[n], &fdRead);
				if (maxSock < g_clients[n])
				{
					maxSock = g_clients[n];
				}
			}
			timeval t = { 1.0 };
			int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
			if (ret < 0)
			{
				printf("Select is finish... \n ");
				Close();
				return false;
			}
			//判断服务器的_sock是否在集合中,如果在就在clientsock的集合中清除_sock
			if (FD_ISSET(_sock, &fdRead))
			{
				FD_CLR(_sock, &fdRead);
				Accept();
			}
			for (int n = (int)g_clients.size()-1; n >= 0; n--)
			{
				if (FD_ISSET(g_clients[n], &fdRead))
				{
					if (-1 == RecvData(g_clients[n]))
					{
						auto iter = g_clients.begin() + n;
						if (iter != g_clients.end())
						{
							g_clients.erase(iter);
						}
					}
				}
			}
			return true;
		}
		return false;
	}

	//Is run
	bool isRun()
	{
		return _sock != INVALID_SOCKET;
	}

	//Recive data ,and dealing(unpack the sticking package)
	int RecvData(SOCKET _clientSock) {
		char szRecv[1024] = {};
		int nLen = recv(_clientSock, szRecv, sizeof(DataHeader), 0);
		DataHeader* header = (DataHeader*)szRecv;

		if (nLen <= 0)
		{
			printf("客户端<%d>已经退出,任务结束。", _clientSock);
			return -1;
		}
		recv(_clientSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
		printf("收到命令:%d ,数据长度:%d\n", header->cmd, header->dataLength);
		OnNetMsg(_clientSock,header);
		
		return 0;
			
	}
	
	//On msg 
	/*
	为什么处理消息和接收消息要分开写,因为不同的服务端要处理不同的消息,一个可能处理注册端,
	一个收款端,所以为了方便在不同的服务器上布置,就把处理消息独立成函数,并虚化。
	*/
	//因为服务端需要对应多个服务端,所以处理对应的客户端 需要传入对应客户端sock号
	virtual void OnNetMsg(SOCKET _clientSock, DataHeader* header)
	{
		switch (header->cmd)
		{
		case CMD_LOGIN:
		{

			Login* login = (Login*)header;
			//判断用户名和密码是否正确
			std::cout << login->userName << "  " << login->PassWord << std::endl;
			char name[32] = "李逵";
			char pw[32] = "110";
			if (0 == (strcmp(name, login->userName) | strcmp(pw, login->PassWord)))
			{
				//这个数据包的长度为32+32+2+2=68
				printf("<Socket %d>输入正确!,name is %s , password is %s ,数据长度:%d \n", _clientSock, login->userName, login->PassWord, login->dataLength);
				//short 的长度为2
				std::cout << "sizeof cmd " << sizeof(login->cmd) << "  " << "sizeof datalength" << sizeof(login->dataLength) << std::endl;
			}

			else
				printf("<Socket %d>重新输入密码\n", _clientSock);
			LoginResult result;

			//7.发送请求
			//一定要先发送消息头,然后在发送消息体,这样才是一个完整的报文
			//传过来的header带有cmd,根据cmd选择参数命令。所以直接返回接收的header就行
			send(_clientSock, (char*)&result, sizeof(LoginResult), 0);
		}
		break;

		case CMD_LOGOUT:
		{
			//(logout*)szRecv将szRecv中取出logout长度的字节数据
			//Logout* logout = (Logout*)szRecv;
			Logout* logout = (Logout*)header;
			printf("<Socket %d>登出!,name is %s ,数据长度:%d \n", _clientSock, logout->userName, logout->dataLength);

			LogoutResult result;
			send(_clientSock, (char*)&result, sizeof(LogoutResult), 0);
		}
		break;
		//定义默认的错误信息
		default:
		{
			DataHeader header = { 0,CMD_ERROR };
			send(_clientSock, (char*)&header, sizeof(DataHeader), 0);
		}
		break;
		}
	}
	
	//Send data the one client
	int SendData(SOCKET _cSock, DataHeader* header)
	{
		if (isRun() && header)
		{
			return send(_cSock, (const char*)header, header->dataLength, 0);
		}
		return SOCKET_ERROR;
	}

	//Send data to all the client
	void SendDataToAll( DataHeader* header)
	{
		if (isRun() && header)
		{
			for (int n = (int)g_clients. size() - 1; n >= 0; n--)
			{
				SendData(g_clients[n], header);
			}
		}
	}

private:

};

#endif // !_EasyTcpServer_hpp_

server.cpp

#include"EasyTcpServer.hpp"
int main()
{
	EasyTcpServer server;
	//server.InitSocket();
	server.Bind(nullptr, 4567);
	server.Listen(5);
	
	while (server.isRun())
	{
		server.OnRun();

	}
	server.Close();
	printf("Exited... \n");
	getchar();
	return 0;
}

//$(SolutionDir)../bin/$(Platform)/$(Configuration)
//$(SolutionDir)../temp/$(Platform)/$(Configuration)/$(ProjectName)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
《Windows Sockets网络编程》是WindowsSockets网络编程领域公认的经典著作,由Windows Sockets2.0规范解释小组负责人亲自执笔,权威性毋庸置疑。它结合大量示例,对WindowsSockets规范进行了深刻地解读,系统讲解了WindowsSockets网络编程及其相关的概念、原理、主要命令、操作模式,以及开发技巧和可能的陷阱,从程序员的角度给出了大量的建议和最佳实践,是学习WindowsSockets网络编程不可多得的参考书。   全书分为三部分:第一部分(第1~6章),提供了翔实的背景知识和框架方面的概念,借助于此框架,读者可理解WinSock的具体细节,包括WindowsSockets概述、OSI网络参考模型、TCP/IP协议簇中的协议和可用的服务、WinSock网络应用程序的框架及其工作机制、WinSock的三种操作模式socket通信机制等;第二部分(第7~12章),以FTP客户端实例为基础介绍了函数实例,还介绍了客户端程序、服务器程序和DLL中间构件及它们的相应函数,并涵盖socket命令和选项及移植BSDSockets相关事项等;第三部分(第13~17章),介绍了应用程序调试技术和工具,针对应用编程中的陷阱的建议和措施,WinSockAPI的多种操作系统平台,WinSock规范的可选功能和WinSock规范2.0中的所有新功能。 译者序 序 前言 第1章 Windows Sockets概述 1.1 什么是Windows Sockets 1.2 Windows Sockets的发展历史 1.3 Windows Sockets的优势 1.3.1 Windows Sockets是一个开放的标准 1.3.2 Windows Sockets提供源代码可移植性 1.3.3 Windows Sockets支持动态链接 1.3.4 Windows Sockets的优点 1.4 Windows Sockets的前景 1.5 结论 第2章 Windows Sockets的概念 2.1 OSI网络模型 2.2 WinSock网络模型 2.2.1 信息与数据 2.2.2 应用协议 2.3 WinSock中的OSI层次 2.3.1 应用层 2.3.2 表示层 2.3.3 会话层 2.3.4 传输层 2.3.5 网络层 2.3.6 数据链路层 2.3.7 物理层 2.4 模块化的层次框 2.5 服务和协议 2.6 协议和API 第3章 TCP/IP协议服务 3.1 什么是TCP/IP 3.2 TCP/IP的发展历史 3.3 传输服务 3.3.1 无连接的服务:UDP 3.3.2 面向连接的服务:TCP 3.3.3 传输协议的选择:UDP与TCP的对比 3.4 网络服务 3.4.1 IP服务 3.4.2 ICMP服务 3.5 支持协议和服务 3.5.1 域名服务 3.5.2 地址解析协议 3.5.3 其他支持协议 3.6 TCP/IP的发展前景 第4章 网络应用程序工作机制 4.1 客户端-服务器模型 4.2 网络程序概览 4.3 socket的打开 4.4 socket的命名 4.4.1 sockaddr结构 4.4.2 sockaddr_in结构 4.4.3 端口号 4.4.4 本地IP地址 4.4.5 什么是socket名称 4.4.6 客户端socket名称是可选的 4.5 与另一个socket建立关联 4.5.1 服务器如何准备建立关联 4.5.2 客户端如何发起一个关联 4.5.3 服务器如何完一个关联 4.6 socket之间的发送与接收 4.6.1 在“已连接的”socket上发送数据 4.6.2 在“无连接的”socket上发送数据 4.6.3 接收数据 4.6.4 socket解复用器中的关联 4.7 socket的关闭 4.7.1 closesocket 4.7.2 shutdown 4.8 客户端和服务器概览 第5章 操作模式 5.1 什么是操作模式 5.1.1 不挂机,等待:阻塞 5.1.2 挂机后再拨:非阻塞 5.1.3 请求对方回拨:异步 5.2 阻塞模式 5.2.1 阻塞socket 5.2.2 阻塞函数 5.2.3 伪阻塞的问题 5.2.4 阻塞钩子函数 5.2.5 阻塞情境 5.2.6 撤销阻塞操作 5.2.7 阻塞操作中的超时 5.2.8 无最少接收限制值 5.2.9 代码示例 5.3 非阻塞模式 5.3.1 怎样使socket为非阻塞的 5.3.2 功与失败不是绝对的 5.3.3 探询而非阻塞 5.3.4 显式地避让 5.3.5 代码示例 5.4 异步模式 5.4.1 认识异步函数 5.4.2 撤销异步操作 5.4.3 代码示例 5.4.4 AU_T

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值