提示
这是一篇简单的学习记录,没有过多复杂的内容。但是因为是随心记录的,不适合于初学者学习,建议有一定编程基础与计算机知识的人参考。
环境
- Microsoft Visual Stduio 2022 Community
- ISO C++ 20 标准
- vcpkg 包管理器
目标
制作一个简易的下载器,通过调整请求头 Range 来实现分段下载。此程序考查了:
- 输入、输出
- 模块、支持库的调用
- 文件的读写
- if 判断语句
- for while 循环语句
- 字符串拼接
- 命名空间的使用
- 异常的处理
代码
#include <iostream>
#include <fstream>
#include <string>
#include <thread>
#include <cpr/cpr.h>
static void progress(int per);
static std::string strip(const std::string& str);
static bool download_file(std::string download_url);
// 程序入口点
int main() {
std::string download_url;
std::cout << "欢迎使用没什么用下载器,请输入下载链接:";
// 不用 std:cin 因为不可以获取空格
std::getline(std::cin, download_url);
download_url = strip(download_url);
std::cout << "你输入的是:" + download_url + "\n我要开始下载喽亲!" << std::endl;
std::cin.sync(); // 清空缓存区 或者使用 std::cin 吃掉换行
// 请求服务器下载
try {
// 判断是否为合法的 URL
if (download_url.substr(0, 3) != "http") {
std::cout << "这不是一个合法的请求。" << std::endl;
std::abort();
}
download_file(download_url);
}
catch (const std::exception& e) {
std::cout << "(" << e.what() << ")我死了,因为程序运行出现了错误。" << std::endl;
}
return 0;
}
// 绘制进度条
static void progress(int per) {
std::cout << "\r进度条:|";
for (int i = 0; i < per; i++) {
std::cout << "*";
}
for (int i = 0; i < 100 - per; i++) {
std::cout << " ";
}
std::cout << "|" << per << "%" << std::flush;
}
static std::string strip(const std::string& str) {
size_t start = str.find_first_not_of(" \t\r\n"); // 找到第一个非空白字符的位置
size_t end = str.find_last_not_of(" \t\r\n"); // 找到最后一个非空白字符的位置
if (start == std::string::npos) // 如果全是空白字符
return ""; // 返回空字符串
return str.substr(start, end - start + 1); // 返回去除首尾空白字符后的子串
}
static bool download_file(std::string download_url) {
// 取出文件名
std::string filename = download_url.substr(download_url.rfind("/") + 1);
// 打开一个文件
std::ofstream file{ filename , std::ios::binary };
if (!file.is_open()) {
std::cout << "文件不能创建!取消下载!" << std::endl;
return false;
}
// 输出文件名
std::cout << "文件名为 " << filename << " " << std::endl;
// 建立一个会话
cpr::Session session;
session.SetUrl(
cpr::Url(download_url)
);
// 获取文件长度
size_t file_length = session.GetDownloadFileLength();
std::cout << "长度为 " << file_length << " B" << std::endl;
// 分段下载文件
size_t response_length = 1024; // 每次请求大小
size_t downloaded_length = 0; // 已经下载的大小
while (downloaded_length < file_length) {
session.SetHeader(
cpr::Header{
{"Range", "bytes=" + std::to_string(downloaded_length) + "-" + std::to_string(downloaded_length + response_length - 1)}
}
);
cpr::Response response = session.Get();
// & 代表直接引用 response.header 中的元素,const 设置常量,禁止修改
/*for (const auto& header : response.header) {
std::cout << header.first << ":" << header.second << std::endl;
}*/
auto data = response.text.data();
auto size = std::stoi(response.header["Content-Length"]);
file.write(data, size);
downloaded_length += size;
progress(static_cast<double> (downloaded_length) / file_length * 100);
}
return true;
}
说明
- 爬虫使用的是 cpr 库,其中的 spr::Response 类中的 text 对象虽然直译为“文本”,但本质就是存在内存的中的一段数据,是 std::string 类型的。使用
response.text.data()可以获取到对应的指针。通过response.downloaded_bytes或者response.header["Content-Length"]可以获取到获取的字节大小,从而实现获取原字节数据。 - for 循环有两种写法,第一种是
for (int i=0;i<100;i++)对于可遍历的对象可以有for (const auto& item: items)。其中const表示定义为常量,不让后续代码进行破坏。&表示的是直接引用items中的各个项,避免不必要的复制浪费内存。 - 关于数据类型转化,一般可用
(变量类型) 变量或变量 (变量类型)进行强制转化,转化过程会产生一个新的变量(即会有一个新的指针地址)。- 在 《C++ Primer Plus 第8版 中文版》第 55 页:C++还引入了4个强制类型转换运算符,对数据类型的转换更为严格。例如
static_cast<>可将数值类型转化为另一种数据类型。(个人理解:用于数值间的转化。) - 在代码中我使用了
static_cast<double>对size_t类型的downloaded_length进行强制转化,传送到接收int类型的函数中,会抛出warning C4244: “参数”: 从“double”转换到“int”,可能丢失数据的警告。我们可以显式的告诉编译器我知道这个风险,progress((int)(static_cast<double> (downloaded_length) / file_length * 100));同样,你可以掩耳盗铃在前面加上:#pragma warning(disable: 4244)之后再#pragma warning(default: 4244)。
- 在 《C++ Primer Plus 第8版 中文版》第 55 页:C++还引入了4个强制类型转换运算符,对数据类型的转换更为严格。例如
- 另外对于初始化数据类型时,使用
{}更加安全,准确来说在 C++11 以上的标准中才加入进来的,其不允许窄化变量:int a{1.1};会报错,因为这种转化会丢失浮点位数据。但是使用()与=是允许的。只不过会抛出异常。更多内容查看 《C++ Primer Plus 第8版 中文版》第 54 页。 - 程序中引用的
#include <thread>并未使用,因为只是在测试进度条的时候用来延迟的,忘记删除了,延迟方法std::this_thread::sleep_for(std::chrono::milliseconds(100));。 - 输入时使用
std::getline(std:cin, 变量)之后还需要使用std::cin.sync();或者std::cin.get();来吃掉换行符,因为std::getline(std:cin, 变量)不会读取换行符,会将其留在缓存区中。注:std::cin.sync()是清空缓存区。
不足
- 程序没有判断是否支持断点续传
- 对异常处理不够精确










