第五章 文件读写
5.1 文件总览
libedk文件对象一览。
- transfer 代表一个传输任务,一个传输任务通常只有一个文件。原始ed2k不支持目录下载
- piece_picker 分片选择器
- piece_manager 分片管理器
- storage_interface 文件操作接口
- disk_io_thread 异步读写文件线程
- file_storage 文件对象抽象
- transfer_info
- file_storage 文件对象抽象 (以引用方式与piece_manager共享对象)
- std::vector<md4_hash> m_piece_hashes 此文件所有分片哈希值
- md4_hash m_info_hash 此文件总哈希值
与文件操作有关对象,及其部分的重要方法和成员:
transfer
|---boost::scoped_ptr<piece_picker> m_picker
###########################################
|---piece_manager* m_storage
|---async_write()
|---async_read()
|---async_xxxx() {create a job, set job.storage=`this` and add it to m_io_thread.}
|---file_storage const& m_files; //注:由transfer::transfer_info::file_storage m_files初始化
|---disk_io_thread& m_io_thread;
|---void disk_io_thread::thread_fun()
|---job.storage->write_impl //if job type is "write" call piece_manager::write_impl
|---storage_interface::writev
|---boost::scoped_ptr<storage_interface> m_storage; //instance of class `default_storage`
|---virtual int readv(...);
|---virtual int writev(...);
|---default_storage::write(...)
|---default_storage::writev(...)
|--- default_storage::readwritev(...)
|---find the FILE iterator and file offset from file_storage const& m_files;
|---default_storage::open_file(FILE)
|---libed2k::copy_bufs
|---libed2k::advance_bufs
|---file::writev() or default_storage::write_unaligned
|---boost::scoped_ptr<file_storage> m_mapped_files;
|---file_storage const& m_files; <== const&
|---std::vector<boost::uint8_t> m_file_priority;
|---std::string m_save_path;
|---file_pool& m_pool; std::map<std::pair<void*, int>, lru_file_entry> file_set;
|---//from session::m_filepool
|---int m_page_size;
|---bool m_allocate_files;
|---boost::intrusive_ptr<piece_manager> m_owning_storage
###########################################
|---boost::intrusive_ptr<transfer_info> m_info
|---file_storage m_files <== 代表文件存储,见下方成员
|---std::vector<internal_file_entry> m_files
|---name
|---offset
|---symlink_index
|---size
|---name_len
|---pad_file
|---hidden_attribute
|---executable_attribute
|---symlink_attribute
|---path_index
|---size_type m_total_size;
|---int m_num_pieces
|---int m_piece_length
|---std::string m_name;在一个file_storage添加多个文件时有点迷,ed2k原不能下载整个目录
|---std::vector<std::string> m_symlinks
|---std::vector<time_t> m_mtime
|---std::vector<std::string> m_paths
|---std::vector<md4_hash> m_piece_hashes
|---md4_hash m_info_hash
5.2 文件分片
5.2.1 文件分片以及文件分片哈希值
文件的分片信息存储在transfer_info::m_piece_hashes,transfer_info在创建一个transfer任务时,在transfer对象的构造函数中创建。transfer_info提供了读写分片哈希值的接口:
- const std::vector<md4_hash>& piece_hashses() const { return m_piece_hashes; }
- void piece_hashses(const std::vector<md4_hash>& hs) { m_piece_hashes = hs; }
在首次创建一个下载任务(而非从临时文件中恢复一个下载任务)时,很明显此时是没有碎片哈希值信息的而只能通过询问其他用户获取(见4.3.4),当获取到以后将保存到transfer_info对象中:
- 文件分片的大小是固定的(const size_type PIECE_SIZE = 9728000ull)
- 文件大小在我们开始一个文件传输任务时就可以确定的(询问服务器或者P2P消息)
- 文件碎片数等于文件大小除以分片大小(除法结果向上取整如3.1=>4)
设置分片大小和数量的操作都是在transfer_info对象的初始化函数中完成:
transfer_info::transfer_info(
md4_hash const& info_hash,
const std::string& filename,
size_type filesize,
const std::vector<md4_hash>& piece_hashses)
: m_info_hash(info_hash) //文件hash
, m_piece_hashes(piece_hashses) //碎片hash(s)
{
m_files.set_num_pieces(div_ceil(filesize, PIECE_SIZE));
m_files.set_piece_length(PIECE_SIZE);
m_files.add_file(filename, filesize);
//文件只有1个分片时,碎片hash必然等于文件hash.
if (m_piece_hashes.empty() && filesize < PIECE_SIZE)
m_piece_hashes.push_back(info_hash);
}
可以看到file_storage transfer_info::m_files这个文件抽象对象管理:
- 分片数
- 分片大小
- 文件名
- 文件大小
而file_storage并没有碎片hash数组,它另外保存到了transfer_info::m_piece_hashes中。在后续的章节中我们将详细介绍file_storage这个文件抽象类,在这里我们先跳过它。
5.2.2 文件分片哈希及临时文件格式分析
4.3.4中曾经说明,ed2k网络里的每一个参与者即使是只拥有一个文件的部分文件片都必须知道此文件的所有分片哈希值,因此有必要保存从其他用户中获取的哈希值以便分享给其他用户。在libed2k中,这个值和其他信息一起保存到临时文件(resume-file)中,在这一节中我们分析一下临时文件对应的数据结构。
首先临时文件长这样:
struct transfer_resume_data
{
// 完整文件的哈希值,等同于transfer_add_param中的file_hash
md4_hash m_hash;
// utf-8 file name
container_holder<boost::uint16_t, std::string> m_filename;
// 完整文件的大小
size_type m_filesize;
// 是否处于做种模式
bool m_seed;
// 临时文件中保存的文件信息(其中之一就是所有碎片的hash值)
tag_list<boost::uint8_t> m_fast_resume_data;
// 用于保存的构造函数
transfer_resume_data(const md4_hash& hash,
const std::string& filename,
size_type size,
bool seed,
const std::vector<char>& fr_data) :
m_hash(hash)
, m_filename(filename)
, m_filesize(size)
, m_seed(seed) {
if (!fr_data.empty())
m_fast_resume_data.add_tag(make_blob_tag(fr_data, FT_FAST_RESUME_DATA, true));
}
// 用于载入的构造函数
transfer_resume_data() : m_filesize(0), m_seed(false)
{}
// 序列化到文件和从文件反序列化。
template<typename Archive>
void serialize(Archive& ar)
{
ar & m_hash;
ar & m_filename;
ar & m_filesize;
ar & m_seed;
ar & m_fast_resume_data;
}
};
从上面的序列化函数中可以知道临时文件的结构是:
名字 |
大小 |
值 |
说明 |
哈希值 |
16字节 |
文件总哈希值 |
|
文件名长度 |
2字节 |
文件名长度 |
boost::uint16_t类型 |
文件名 |
变长 |
文件名 |
utf-8字符串 |
文件大小 |
8字节 |
libed2k::size_type 类型 |
|
是否做种模式 |
1字节 |
0或者1 |
bool类型 |
其他信息 |
变长 |
tag_list类型 |
在conn项目中cc_store命令实现了从临时文件中创建任务,我们看一下它的实现:
- 反序列化临时文件数据到libed2k::transfer_resume_data
- 从m_fast_resume_data成员中取得tag为libed2k::FT_FAST_RESUME_DATA的二进制数据并赋予add_transfer_params::resume_data(它为std::vector<char>*类型)
APP("restore " << vpaths[n]);
std::ifstream ifs(
vpaths[n].c_str(), //这是临时文件的全路径。
std::ios_base::in | std::ios_base::binary);
if (ifs)
{
libed2k::transfer_resume_data trd;
libed2k::archive::ed2k_iarchive ia(ifs);
ia >> trd; //反序列化到libed2k::transfer_resume_data对象
libed2k::add_transfer_params params;
params.seed_mode = false;
params.file_path = trd.m_filename.m_collection;
params.file_size = trd.m_filesize;
if (trd.m_fast_resume_data.size() > 0)
{
params.resume_data = const_cast<std::vector<char>*>(
&trd.m_fast_resume_data.getTagByNameId(
libed2k::FT_FAST_RESUME_DATA)->asBlob());
}
params.file_hash = trd.m_hash;
ses.add_transfer(params);
}
首先从上面的代码知transfer_info::resume_data的来源是tag_list<boost::uint8_t> transfer_resume_dat中NameID = libed2k::FT_FAST_RESUME_DATA的数据块,现在我们需要知道这个数据块的构成。
在session添加任务时ses.add_transfer会调用到transfer::start(),在这个函数中会对transfer::m_resume_data(即上面的Blob数据块,它在transfer构造函数中传入)进行bdecode解析,结果将存到lazy_entry transfer::m_resume_entry:
error_code ec;
if (!m_resume_data.empty() &&
lazy_bdecode(&m_resume_data[0],
&m_resume_data[0] + m_resume_data.size(),
m_resume_entry, ec) != 0)
{
std::vector<char>().swap(m_resume_data);
m_ses.m_alerts.post_alert_should(fastresume_rejected_alert(handle(), errors::fast_resume_parse_error));
}
可以看到数据是通过调用lazy_bdecode解析到m_resume_entry,lazy_bdecode函数用于将一段以bdecode编码的数据解析到lazy_entry对象(一个类似map类型的数据结构)中。
在void transfer::start()随后调用的void transfer::init()里,对lazy_entry对象做如下解析:
//特别注意seed模式不做fast_resume检查
if (!m_seed_mode){
set_state(transfer_status::checking_resume_data);
if (m_resume_entry.type() == lazy_entry::dict_t){
DBG("read resume data: {hash: " << hash() << ", file: " << name() << "}");
int ev = 0;
if (m_resume_entry.dict_find_string_value("file-format") != "libed2k resume file")
ev = errors::invalid_file_tag;
std::string info_hash = m_resume_entry.dict_find_string_value("transfer-hash");
if (!ev && info_hash.empty())
ev = errors::missing_transfer_hash;
if (!ev && (md4_hash::fromString(info_hash) != hash()))
ev = errors::mismatching_transfer_hash;
if (ev){
std::vector<char>().swap(m_resume_data);
lazy_entry().swap(m_resume_entry);
m_ses.m_alerts.post_alert_should(
fastresume_rejected_alert(handle(), error_code(ev, get_libed2k_category())));
}else{
//从bedcode dict节点lazy_entry中读取各种信息。
read_resume_data(m_resume_entry);
m_need_save_resume_data = false;
}
}
m_storage->async_check_fastresume(
&m_resume_entry,
boost::bind(&transfer::on_resume_data_checked,
sha