用C++寫一個文件分割器

在成功將 mac 由 10.10 升級到 10.12 後,我發現除了新增一個並不怎麼好用的 Siri 外,原來支持 NTFS 硬盤的驅動居然也成功失效了。我那塊 500 GB 的東芝硬盤,雖不至於成磚,但一塊只能讀不能寫的硬盤,實在讓人慾哭無淚。巧的是,最近需要頻繁地將一些數據文件( GB 級別)拷貝到其他電腦,而手頭又僅剩一些小容量 U 盤。於是,我突然萌生了寫一個文件分割器的想法,將大的壓縮文件分片後,再用這些小 U 盤搬到到其他電腦上去。

有人會問,這樣的軟件明明網上有的是,何必自己寫呢?沒錯,我就是這麼無聊的人。

需求分析

其實也不用怎麼分析,功能非常簡單。我需要兩個函數(分別用於分割和合成),分割函數的輸入是:一個文件、分片數量,輸出是:分片文件、一個配置文件(記錄分片文件的順序);合成函數的輸入是:配置文件,輸出是:完整的數據文件(根據配置,程序會尋找分片文件用於合成)。

基於此,其實要實現的是兩個這樣的函數:

// 分割文件的函數,第三個參數指定配置文件名稱
void segment(string file_name, int segment_num, string json_file);
// 合成文件的函數,參數爲分割時生成的配置文件
void merge(string json_file);

配置文件的格式,我使用了 json(其實用簡單的字符串記錄一下也是可以的)。

另外,爲了方便使用,最好再用一個類將兩個方法封裝一下。

難點分析

這麼小的程序會有難點?!其實還是有一丟丟,就是切割文件的時候,由於文件可能太大,因此不能一口氣讀入內存中,所以這裏採用分塊的方法,讀一小塊寫一小塊。當然啦,速度方面的優化,這裏先不考慮了。

程序實現

首先,我們把所有功能放在一個類FileSegment裏面實現,對外只暴露上面的兩個函數接口。

segment

上面的難度分析已經指出,我們需要分塊讀取文件,然後分塊寫入。

首先需要定義分塊大小:const int FileSegment::kBlockSize = 1024 * 1024; ,這裏設定一個塊大小爲1 MB。

我們再定義兩個輔助函數,用來分塊讀文件、寫文件:

// 從input流中讀取size(默認大小kBlockSize)大小的字節到data裏面
inline void read_file_in_block(char* data, ifstream &input, int size=kBlockSize) {
    input.read(data, size);
}
// 從data中將size(默認大小kBlockSize)大小的字節寫入到output流
inline void write_file_in_block(char* data, ofstream &output, int size=kBlockSize) {
    output.write(data, size);
}

這兩個函數因爲要經常用到,所以把它們作爲內聯函數使用。

綜合這兩個輔助函數,我們定義另一個輔助函數,用於從輸入文件中將大批量的數據寫入到輸出文件中:

// 將input流中讀取input_size大小的字節內容,寫入到output流中
void FileSegment::copy_file(ifstream &input, ofstream &output, size_t input_size) {
    char* data = new char[kBlockSize];

    for (size_t block = 0; block < input_size / kBlockSize; block++) {
        read_file_in_block(data, input);
        write_file_in_block(data, output);
    }

    // 讀取剩餘的字節
    size_t left_size = input_size % kBlockSize;
    if (left_size != 0) {
        read_file_in_block(data, input, left_size);
        write_file_in_block(data, output, left_size);
    }

    delete [] data;
    data = nullptr;
}

有了上面的輔助函數後,我們可以聚焦於segment()函數的核心代碼部分了。

我們只需要利用copy_file()函數,將源文件分片寫入到幾個分片文件中即可。

// 分片文件名
vector<string> segment_files;
for (int i = 0; i < segment_num; i++) {
  segment_files.push_back(file_name + to_string(i+1) + ".tmp");
  cout << "segment_file --- " << segment_files[i] << endl;
}

ifstream src_file_input(file_name);
// 輸入文件大小
size_t src_file_size = file_size(src_file_input);
// 分片文件大小
size_t segment_size = src_file_size / segment_num;

// 分片輸出文件
for (int i = 0; i < segment_num; i++) {
  ofstream segment_file_output(segment_files[i]);
  if (i == segment_num-1) {  // 最後一次,要將剩餘文件片全部寫入
    size_t left_size = src_file_size % segment_size;
    copy_file(src_file_input, segment_file_output, segment_size + left_size);
  } else {
    copy_file(src_file_input, segment_file_output, segment_size);
  }
  segment_file_output.close();
}

src_file_input.close();

另外,我們需要將分片文件的文件名和分割順序等信息寫入配置文件中,這裏使用json格式,並用這個第三方庫來操縱json對象。

const string FileSegment::kSegmentFileNum = "SegmentNum";
const string FileSegment::kSourceFileName = "SourceFileName";
const string FileSegment::kSegmentFiles = "SegmentFiles";

ofstream json_output(json_file);
json j;
j[kSegmentFileNum] = segment_num;
j[kSourceFileName] = file_name;
j[kSegmentFiles] = segment_files;   // 這裏segment_files是vector對象
json_output << j;
json_output.close();

下面給出segment()函數的完整代碼:

void FileSegment::segment(string file_name, int segment_num, string json_file) {

    // 檢查源文件是否存在
    if (!exist(file_name)) {
        cout << "file [" << file_name << "] doesn't exist!" << endl;
        return;
    }

    // 檢查分片數量是否大於0
    if (segment_num <= 0) {
        cout << "segment number should be greater than 0!" << endl;
        return;
    }

    // 分片文件名
    vector<string> segment_files;
    for (int i = 0; i < segment_num; i++) {
        segment_files.push_back(file_name + to_string(i+1) + ".tmp");
        cout << "segment_file --- " << segment_files[i] << endl;
    }

    ifstream src_file_input(file_name);
    // 輸入文件大小
    size_t src_file_size = file_size(src_file_input);
    // 分片文件大小
    size_t segment_size = src_file_size / segment_num;

    // 分片輸出文件
    for (int i = 0; i < segment_num; i++) {
        ofstream segment_file_output(segment_files[i]);
        if (i == segment_num-1) {  // 最後一次,要將剩餘文件片全部寫入
            size_t left_size = src_file_size % segment_size;
            copy_file(src_file_input, segment_file_output, segment_size + left_size);
        } else {
            copy_file(src_file_input, segment_file_output, segment_size);
        }
        segment_file_output.close();
    }

    src_file_input.close();

    ofstream json_output(json_file);
    json j;
    j[kSegmentFileNum] = segment_num;
    j[kSourceFileName] = file_name;
    j[kSegmentFiles] = segment_files;
    json_output << j;
    json_output.close();
}

merge

有了前面的輔助函數後,merge()函數的實現基本是依葫蘆畫瓢。首先需要從配置文件中讀取出json對象,根據配置信息去合成文件:

json j;

if (!exist(json_file)) {
  cout << "json file [" << json_file << "] doesn't exist!" << endl;
  return;
}

ifstream json_input(json_file);
json_input >> j;

// 源文件名
string src_file = j[kSourceFileName];

// 檢查源文件是否已經存在
if (exist(src_file)) {
  src_file += ".copy";
}
ofstream result(src_file);

// 文件分片數量
int segment_num = j[kSegmentFileNum];
// 分片文件名
vector<string> segment_files = j[kSegmentFiles];

之後,根據分片文件來合成大文件:

// 合併文件
for (auto it = segment_files.begin(); it != segment_files.end(); it++) {
  cout << "copy file [" << *it << "]" << endl;
  ifstream seg_input(*it);
  size_t seg_input_size = file_size(seg_input);  // 計算分片文件大小
  copy_file(seg_input, result, seg_input_size);
  seg_input.close();
}

接下來照例給出merge()函數完整實現:

void FileSegment::merge(string json_file) {
    json j;

    if (!exist(json_file)) {
        cout << "json file [" << json_file << "] doesn't exist!" << endl;
        return;
    }

    ifstream json_input(json_file);
    json_input >> j;

    // 源文件名
    string src_file = j[kSourceFileName];

    // 檢查源文件是否已經存在
    if (exist(src_file)) {
        src_file += ".copy";
    }
    ofstream result(src_file);

    // 文件分片數量
    int segment_num = j[kSegmentFileNum];
    // 分片文件名
    vector<string> segment_files = j[kSegmentFiles];

    // 檢查文件分片是否齊全
    for (auto it = segment_files.begin(); it != segment_files.end(); ++it) {
        if (!exist(*it)) {
            cout << "segment file [" << *it << "] doesn't exist!" << endl;
            return; 
        }
    }

    // 合併文件
    for (auto it = segment_files.begin(); it != segment_files.end(); it++) {
        cout << "copy file [" << *it << "]" << endl;
        ifstream seg_input(*it);
        size_t seg_input_size = file_size(seg_input);
        copy_file(seg_input, result, seg_input_size);
        seg_input.close();
    }

    json_input.close();
    result.close();
}

main

main()中,直接實例化FileSegment類,通過segment()merge()函數分割或者合成文件。

int main(int argc, char const *argv[]) {
  FileSegment fs;
  // 分割data.zip文件,分爲4片
  fs.segment("data.zip", 4, "config.json");
  // 根據config.json文件合成最終文件
  fs.merge("config.json");
}

另外,爲了方便使用,我特意寫了一個解析命令的類InputParser,然後,我們可以按照如下方式使用該程序:

分割文件

./main -s data.zip 4 config.json

合成文件

./main -m config.json

完整工程代碼,請看:https://github.com/Jermmy/file_segmentation

測試結果

在我的 mac (雙核,2.7 GHz Intel Core i5) 上,將一個 7.35 G 的 zip 文件分割爲 10 片,所用時間爲 37.7 s。

同樣的機器,將上面的 10 片文件合成原來的大文件,所用時間爲 31.8 s。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章