c++网络编程——用TCP实现文件传输(windows下基于VS2017)

c++网络编程——用TCP实现文件传输

文件传输协议FTP是基于TCP协议实现的,为了体验这个过程,本文自定义一个简单的文件传输协议,并基于TCP协议编程实现,经过本机和局域网的测试,代码初步可用。

定义文件传输协议

	自定义文件传输协议:
	下载文件过程:
			客户端发送:download:filename:
			服务端反馈:
						文件存在,反馈文件大小size
						文件不存在,反馈“error”,断开连接
			客户端发送:
						缓冲区开辟完成,反馈“ok”
						缓冲区开辟失败,反馈“error”,断开连接
			服务端反馈:
						二进制文件信息
	上传文件过程:
			客户端发送:download:filename:file_size
			服务端反馈:
						缓冲区开辟完成,反馈“ok”
						缓冲区开辟失败,反馈“error”,断开连接
			客户端发送:
						二进制文件信息

注意:
1. 在使用new读取、发送或接受文件时,考虑到new申请内存失败,因此需要用到异常处理机制 try catch;
2. 文件使用IOS::binary打开,这样可以读取所有格式的文件;
3. 文件需要完整传输,如果接收到的文件内容不完整,即长度不符合,需要将接收的文件删除;
4. 获取文件的大小使用以下代码:
file.seekg(0, ios::end);
int length = infile.tellg();
file.seekg(0, ios::beg);
具体参考:http://www.cplusplus.com/reference/istream/istream/seekg/?kw=seekg
5. 每次发送数据\接收数据都需要判断数据是否发送\接收成功,如果失败,则表示TCP断开连接,程序结束;

本文代码的缺陷:
(1)因为需要将文件读取到数组中再发送,所以文件太大时这种方式不好,应该将文件分段发送;
(2)没有“优雅的断开TCP连接”,即没有用到半关闭TCP连接,文件发送后成功与否没有反馈以及进一步处理;

参考资料
1、C++中TCP通信实现文件传输
2、c++网络编程——用TCP实现Echo服务端与客户端(windows下基于VS2017)
3、《TCP IP网络编程》(韩)尹圣雨
4、TCP/IP协议详解

服务端代码

服务端创建日志文本存储日志,并于while(true)中死循环等待客户端连接。

#include "pch.h"
#include <iostream>
#include <fstream> 
#include <cstdlib>
#include<winsock.h>
#include <string>
#include "time.h"
#include<windows.h>
#include <exception>
//使用localtime
#pragma warning(disable:4996)
//使用套接字库
#pragma comment(lib,"ws2_32.lib")
using namespace std;

string timetoStr() {
	char tmp[64];
	time_t t = time(NULL);
	tm *_tm = localtime(&t);
	int year = _tm->tm_year + 1900;
	int month = _tm->tm_mon + 1;
	int date = _tm->tm_mday;
	int hh = _tm->tm_hour;
	int mm = _tm->tm_min;
	int ss = _tm->tm_sec;
	sprintf(tmp, "%04d%02d%02d %02d:%02d:%02d   ", year, month, date, hh, mm, ss);
	return string(tmp);
}
string GetLogNameByDate() {
	char tmp[30];
	time_t t = time(NULL);
	tm *_tm = localtime(&t);
	int year = _tm->tm_year + 1900;
	int month = _tm->tm_mon + 1;
	int date = _tm->tm_mday;
	sprintf(tmp, "%04d%02d%02d.txt", year, month, date);
	return string(tmp);
}
void ErrorHandling(const string &s)
{
	cerr << timetoStr() << s << endl;
	exit(1);
}
//************************************
// Method:    ClientDownloadFile
// FullName:  ClientDownloadFile
// Access:    public 
// Returns:   void
// Qualifier: 客户端下载文件处理函数
// Parameter: SOCKET s_accept 服务端接收套接字
// Parameter: string file_name	文件名
// Parameter: string clinet_IP 客户端IP
// Parameter: ostream & out_stream 输出流 这里主要为日志文件输出流/cout
//************************************
void ClientDownloadFile(SOCKET s_accept, string file_name, string clinet_IP,ostream &out_stream)
{
	int send_len = 0, recv_len = 0;
	//发送、接收缓冲区
	char send_buf[100], recv_buf[100];
	memset(send_buf, '\0', 100);
	memset(recv_buf, '\0', 100);
	string send_info;

	ifstream infile;
	infile.open(file_name, ios::in | ios::binary);
	if (!infile.good())
	{
		//文件不存在,回复客户端
		send_len = send(s_accept, "error", 100, 0);
		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << "error" << endl;
		if (send_len < 0)
		{
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 请求文件不存在而断开连接" << endl;
		closesocket(s_accept);
		return;
	}
	//文件存在,返回客户端文件大小
	else
	{
		infile.seekg(0, ios::end);
		int length = infile.tellg();
		infile.seekg(0, ios::beg);
		send_info = to_string(length);
		send_len = send(s_accept, send_info.c_str(), send_info.length(), 0);
		//Sleep(1000);
		if (send_len < send_info.length())
		{
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		else
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息:" << send_info << endl;
		//等待客户端开辟缓冲区,接收信息后再发送文件
		memset(recv_buf, '\0', 100);
		recv_len = recv(s_accept, recv_buf, 100, 0);
		if (recv_len < 0)
		{
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收数据失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		else
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收来自客户端的信息:" << recv_buf << endl;

		if (string(recv_buf) == "ok")
		{
			//发送整个文件
			char* data;
			//此处没有考虑到new失败的情况!!!!
			data = new char[length];
			infile.read(data, length);
			send_len = send(s_accept, data, length, 0);
			if (send_len < length)
			{
				out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端文件信息失败!断开连接" << endl;
				closesocket(s_accept);
				return;
			}
			delete data;
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 文件发送成功!断开连接" << endl;
			closesocket(s_accept);
			return;
		}

		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 客户端缓冲区开辟失败!断开连接" << endl;
		closesocket(s_accept);
	}
}

//************************************
// Method:    ClientUploadFile
// FullName:  ClientUploadFile
// Access:    public 
// Returns:   void
// Qualifier:客户端上传文件处理函数
// Parameter: SOCKET s_accept 服务端接收套接字
// Parameter: string file_name	文件名
// Parameter: int file_size	文件大小
// Parameter: string clinet_IP	客户端IP
// Parameter: ostream & out_stream  输出流 这里主要为日志文件输出流/cout
//************************************
void ClientUploadFile(SOCKET s_accept, string file_name,int file_size, string clinet_IP, ostream &out_stream)
{
	int send_len = 0, recv_len = 0;
	//发送、接收缓冲区
	char send_buf[100], recv_buf[100];
	memset(send_buf, '\0', 100);
	memset(recv_buf, '\0', 100);
	string send_info;

	//根据文件大小,开辟文件缓冲区
	ofstream outfile;
	outfile.open(file_name, ios::out | ios::binary | ios::trunc);
	if (!outfile.good())
	{
		//文件无法打开
		send_len = send(s_accept, "error", 100, 0);
		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << "error" << endl;
		if (send_len < 0)
		{
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 上传的文件无法建立而断开连接" << endl;
		closesocket(s_accept);
		return;
	}
	//文件打开成功,开辟缓冲区,并返回客户端结果
	else
	{
		char* data=nullptr;
		try
		{
			data = new char[file_size];
		}
		catch (const std::exception&)
		{
			//失败,发送error		
			memset(send_buf, '\0', 100);
			strcpy(send_buf, "error");
			send_len = send(s_accept, send_buf, 100, 0);
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << send_buf << endl;
			if (send_len < 0)
			{
				out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
				closesocket(s_accept);
				return;
			}
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 服务端开辟缓冲区失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		//成功,发送ok
		memset(send_buf, '\0', 100);
		strcpy(send_buf, "ok");
		send_len = send(s_accept, send_buf, 100, 0);
		out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << send_buf << endl;
		if (send_len < 0)
		{
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
			closesocket(s_accept);
			return;
		}
		recv_len = recv(s_accept, data, file_size, 0);
		outfile.write(data, file_size);
		outfile.close();
		if (recv_len < file_size)
		{
			//接收的信息不全,需要删除文件
			remove(file_name.c_str());
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收客户端文件信息失败!断开连接" << endl;
			closesocket(s_accept);
			return;		
		}
		else
			out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收客户端文件成功!断开连接" << endl;
		delete data;
		
		closesocket(s_accept);
	}
}


void InitiallizationWSA()
{
	WSADATA wsaData;
	//初始化套接字库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("初始化套接字库失败!");
	//检查版本号
	if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wHighVersion) != 2)
		ErrorHandling("套接字库版本号不符!");

}

int main()
{
	//创建日志文件,存储日志
	string log_file_name = GetLogNameByDate();
	ofstream Log;
	try
	{
		Log.open(log_file_name, std::ofstream::out | std::ofstream::app);
	}
	catch (ios_base::failure &e)
	{
		cout << "创建日志文件失败!退出程序" << endl;
		exit(1);
	}

	Log << timetoStr() << "启动:文件传输TCP实现——服务端。。。" << endl;
	//初始化套接字库
	InitiallizationWSA();
	//发送、接收数据的大小
	int send_len = 0, recv_len = 0;
	int len = 0;
	//发送、接收缓冲区
	char send_buf[100], recv_buf[100];
	memset(send_buf,'\0',100);
	memset(recv_buf, '\0', 100);
	string send_info;
	//定义服务端套接字,接受请求套接字
	SOCKET s_server, s_accept;
	//客户端IP
	string clinet_IP;
	//定义服务端地址,客户端地址
	SOCKADDR_IN server_addr, clnt_addr;
	//创建服务端套接字
	s_server = socket(AF_INET, SOCK_STREAM, 0);
	//填充服务端信息
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(5010);
	//绑定套接字
	if (bind(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
		ErrorHandling("套接字绑定失败!");
	//监听套接字
	if (listen(s_server, SOMAXCONN) < 0)
		ErrorHandling("监听套接字失败!");
	//接受连接
	while (true)
	{
		len = sizeof(SOCKADDR);
		s_accept = accept(s_server, (SOCKADDR *)&clnt_addr, &len);
		if (s_accept == SOCKET_ERROR)
			ErrorHandling("连接失败!");
		//获取客户端IP
		clinet_IP = inet_ntoa(clnt_addr.sin_addr);
		Log << timetoStr() << "连接" << clinet_IP << "成功!" << endl;
		//获取客户端发送的文件基本信息:命令:文件名:文件大小
		memset(recv_buf, '\0', 100);
		recv_len = recv(s_accept, recv_buf, 100, 0);
		if (recv_len < 0)
		{
			Log << timetoStr() << "客户端为:" << clinet_IP << " 接收数据失败!断开连接" << endl;
			closesocket(s_accept);
			continue;
		}
		else
		{
			Log << timetoStr() << "客户端为:" << clinet_IP << " 接收来自客户端的信息:" << recv_buf << endl;
		}
		//根据协议解析命令数据,格式为命令:文件名:文件大小
		string order;//文件操作命令
		string file_name;//文件名
		int size_of_file;//文件大小
		string file_info(recv_buf);
		auto id_addr1 = file_info.find_first_of(':');
		order = file_info.substr(0, id_addr1);
		auto id_addr2 = file_info.find_first_of(':', id_addr1+1);
		file_name = file_info.substr(id_addr1 + 1, id_addr2- id_addr1-1);
		auto Size= file_info.substr(id_addr2 + 1, file_info.length() - id_addr2 - 1);
		//下载文件
		if (order == "download")
		{
			ClientDownloadFile(s_accept, file_name,clinet_IP, Log);

		}
		if (order == "upload")
		{
			ClientUploadFile(s_accept, file_name,atoi(Size.c_str()), clinet_IP, Log);
		}
	}
	Log.close();
	//关闭套接字
	closesocket(s_server);
	//释放DLL资源
	WSACleanup();
	return 0;
}

客户端代码

客户端每次执行一次上传/下载命令,执行完毕后连接断开,程序退出。

#include "pch.h"
#include <iostream>
#include <cstdlib>
#include<winsock.h>
#include <string>
#include <fstream>
#include <cstdio>
#pragma warning( disable : 4996)
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void ErrorHandling(const string &s)
{
	cerr << s << endl;
	exit(1);
}
void InitiallizationWSA()
{
	WSADATA wsaData;
	//初始化套接字库
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("初始化套接字库失败!");
	//检查版本号
	if(LOBYTE(wsaData.wVersion) !=2 || HIBYTE(wsaData.wHighVersion)!=2)
		ErrorHandling("套接字库版本号不符!");

}
int main()
{
	cout << "欢迎使用:文件传输TCP实现——客户端" << endl;
	//初始化套接字库
	InitiallizationWSA();
	string server_ip;
	int port = 0;
	//发送、接收数据的大小
	int send_len = 0, recv_len = 0;
	int len = 0;
	//发送、接收缓冲区
	char send_buf[100];
	char recv_buf[100];
	string send_info;
	memset(recv_buf,'\0',100);
	memset(send_buf, '\0', 100);
	//定义服务端套接字
	SOCKET s_server;
	//定义服务端地址
	SOCKADDR_IN server_addr;
	//创建套接字
	s_server = socket(AF_INET, SOCK_STREAM, 0);
	//填充服务端信息
	cout << "请输入服务端IP地址(点分十进制):";
	cin >> server_ip;
	cout << "请输入服务端端口号:";
	cin >> port;
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.S_un.S_addr = inet_addr(server_ip.c_str());
	server_addr.sin_port = htons(port);//5010


	//连接服务端
	if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
		ErrorHandling("服务端连接失败!");
	cout << "连接服务端成功!" << endl;
	//发送/接收数据
		cout << "请输入发送信息的内容:(格式为 命令:文件名:)";
		cin >> send_buf;
		//根据协议解析命令数据,格式为命令:文件名:文件大小
		string order;//文件操作命令
		string file_name;//文件名
		int size_of_file;//文件大小
		string file_info(send_buf);
		auto id_addr1 = file_info.find_first_of(':');
		order = file_info.substr(0, id_addr1);
		auto id_addr2 = file_info.find_first_of(':', id_addr1 + 1);
		file_name = file_info.substr(id_addr1 + 1, id_addr2 - id_addr1 - 1);
		//auto Size = file_info.substr(id_addr2 + 1, file_info.length() - id_addr2 - 1);
		if (order == "download")
		{		
			send_len = send(s_server, send_buf, 100, 0);
			if (send_len < 0)
			{
				ErrorHandling("发送失败!断开连接");
			}
			//获取反馈的命令
	
			memset(recv_buf, '\0', 100);
			recv_len = recv(s_server, recv_buf, 100, 0);
			if (recv_len < 0)
			{
				ErrorHandling("接收失败!断开连接");
			}
			else
				cout << "接收服务端信息:" << recv_buf << endl;

			string recv_info(recv_buf);
			if (recv_info != "error")
			{
				//接收文件大小,开辟文件缓冲区
				ofstream outfile;
				outfile.open(file_name, ios::out | ios::binary | ios::trunc);
				if (!outfile.good())
				{
					//文件无法打开
					cerr << "文件无法建立" << endl;		
				}
				else
				{
					//开辟缓冲区
					auto size_of_file = atoi(recv_info.c_str());
					char* data=nullptr;
					try
					{
						data = new char[size_of_file];
					}
					catch (const std::exception&)
					{
						//失败,发送error		
						memset(send_buf, '\0', 100);
						strcpy(send_buf, "error");
						send_len = send(s_server, send_buf, 100, 0);
						if (send_len < 0)
						{
							ErrorHandling("发送失败!断开连接");
						}
						ErrorHandling("开辟缓冲区失败!");
					}
					//成功,发送ok
					memset(send_buf, '\0', 100);
					strcpy(send_buf, "ok");
					send_len = send(s_server, send_buf, 100, 0);
					if (send_len < 0)
					{
						ErrorHandling("发送失败!断开连接");
					}					
					recv_len = recv(s_server, data, size_of_file, 0);
					outfile.write(data, size_of_file);
					outfile.close();
					//cout << data << endl;
					cout << "接收文件大小为:"<<recv_len << endl;
					if (recv_len < size_of_file)
					{
						cout << "接收失败!" << endl;
						//接收的信息不全,需要删除文件
						remove(file_name.c_str());
					}				
					else
						cout << "接收成功!" << endl;
					delete data;
				}
				
			}
			cout << "断开连接" << endl;
			//关闭套接字
			closesocket(s_server);
		}
		
		if (order == "upload")
		{
			//获取文件长度,并发送给服务端
			ifstream infile;
			infile.open(file_name, ios::in | ios::binary);
			if (!infile.good())
				//文件不存在,提示
				ErrorHandling("文件不存在!");				
			else
			{
				infile.seekg(0, ios::end);
				int length = infile.tellg();
				infile.seekg(0, ios::beg);
				send_info=string(send_buf)+to_string(length);	
				//发送 命令:文件名:文件大小
				//等待服务端开辟缓冲区
				send_len = send(s_server, send_info.c_str(), 100, 0);
				if (send_len < 0)
				{
					ErrorHandling("发送失败!断开连接");
				}
				memset(recv_buf, '\0', 100);
				recv_len = recv(s_server, recv_buf, 100, 0);
				if (recv_len < 0)
				{
					ErrorHandling("接收失败!断开连接");
				}
				else
					cout << "接收服务端信息:" << recv_buf << endl;
				string recv_info(recv_buf);
				if (recv_info == "ok")
				{
					//服务端开辟缓冲区成功,上传文件数据
					//发送整个文件
					char* data;
					data = new char[length];
					infile.read(data, length);
					send_len = send(s_server, data, length, 0);
					if (send_len < length)
						ErrorHandling("发送文件失败!断开连接");
					delete data;
					cout << "文件上传成功!" << endl;

				}
				else
					cout << "服务端开辟缓冲区失败!" << endl;			
				infile.close();
				cout << "断开连接" << endl;
				//关闭套接字
				closesocket(s_server);
			}		
		}
		while (true)
		{
			string in;
			cout << "输入任意字符退出程序:" << endl;
			if(cin>>in)
				break;
		}
	//释放DLL资源
	WSACleanup();
	return 0;
}

测试

本机测试:
客户端控制台:
1、客户端上传、下载文件测试:
在这里插入图片描述在这里插入图片描述2、客户端错误输入/命令测试:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

服务端日志:
在这里插入图片描述

局域网通信测试:
1、客户端上传、下载文件测试:
在这里插入图片描述在这里插入图片描述2、客户端错误输入/命令测试:
在这里插入图片描述在这里插入图片描述服务端日志:
在这里插入图片描述

  • 23
    点赞
  • 123
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴能够回答你的问题。以下是一个简单的即时通信系统的C++网络编程示例,基于Linux平台使用socket库进行开发。 客户端代码: ```c++ #include <iostream> #include <cstring> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; int main() { // 创建socket int client_socket = socket(AF_INET, SOCK_STREAM, 0); // 配置服务器地址 sockaddr_in server_address{}; server_address.sin_family = AF_INET; server_address.sin_port = htons(8080); server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 连接服务器 int connect_result = connect(client_socket, (sockaddr *) &server_address, sizeof(server_address)); if (connect_result == -1) { cout << "连接服务器失败!" << endl; return -1; } // 发送消息 char message[1024] = {0}; while (true) { cin.getline(message, sizeof(message)); if (strlen(message) == 0) { continue; } send(client_socket, message, strlen(message), 0); memset(message, 0, sizeof(message)); } // 关闭socket close(client_socket); return 0; } ``` 服务器端代码: ```c++ #include <iostream> #include <cstring> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; int main() { // 创建socket int server_socket = socket(AF_INET, SOCK_STREAM, 0); // 配置服务器地址 sockaddr_in server_address{}; server_address.sin_family = AF_INET; server_address.sin_port = htons(8080); server_address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 绑定socket和地址 bind(server_socket, (sockaddr *) &server_address, sizeof(server_address)); // 监听客户端连接 listen(server_socket, 10); // 接受客户端连接 sockaddr_in client_address{}; socklen_t client_address_len = sizeof(client_address); int client_socket = accept(server_socket, (sockaddr *) &client_address, &client_address_len); // 接收消息 char message[1024] = {0}; while (true) { int recv_result = recv(client_socket, message, sizeof(message), 0); if (recv_result <= 0) { break; } cout << "收到消息:" << message << endl; memset(message, 0, sizeof(message)); } // 关闭socket close(client_socket); close(server_socket); return 0; } ``` 在上面的代码中,客户端和服务器端都使用了socket库中的函数来完成网络通信。客户端使用`socket`函数创建socket,使用`connect`函数连接服务器,并使用`send`函数发送消息。服务器端使用`socket`函数创建socket,使用`bind`函数将socket与服务器地址绑定,使用`listen`函数监听客户端连接,并使用`accept`函数接受客户端连接,最后使用`recv`函数接收客户端发送的消息。 当然,这只是一个简单的示例。在实际开发中,你需要考虑更多的细节,例如如何处理多个客户端连接、如何处理异常情况等等。 希望这个示例能够帮助你了解Linux下C++ socket网络编程的基本流程和方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值