当前位置: 代码网 > it编程>编程语言>Asp.net > 基于C#实现的文件分割与合并工具

基于C#实现的文件分割与合并工具

2026年04月08日 Asp.net 我要评论
一、系统概述文件分割与合并是数据管理中的常见操作,广泛应用于以下场景:大文件传输:突破邮件附件大小限制(如将 50mb 文件分割为 5×10mb)分布式存储:将文件拆分后存储至多个磁盘或云

一、系统概述

文件分割与合并是数据管理中的常见操作,广泛应用于以下场景:

  • 大文件传输:突破邮件附件大小限制(如将 50mb 文件分割为 5×10mb)
  • 分布式存储:将文件拆分后存储至多个磁盘或云存储节点
  • 分卷备份:配合压缩算法实现分卷压缩备份
  • 断点续传:支持文件的分块上传与下载

本工具基于 c# 语言开发,采用 .net 6.0 运行时,利用 system.io 命名空间实现二进制文件流操作。核心特性包括:

  • ✅ 按固定大小分割(支持自定义分块大小)
  • ✅ 按序号自动合并
  • ✅ 完整性校验(md5 校验和)
  • ✅ 跨平台支持(windows / linux / macos)
  • ✅ 异常捕获与友好的错误提示
  • ✅ 命令行 / gui 双模式支持(gui 可作为扩展)

二、核心设计思路

2.1 分割策略

  • 按固定大小分割:将源文件拆分为多个指定大小(如 chunksize = 1024 * 1024字节,即 1mb)的子文件,最后一个子文件可能小于指定大小。
  • 文件命名规则:子文件命名为 原文件名.partn(n 为序号,从 1 开始),如 largefile.zip.part1largefile.zip.part2
  • 元数据记录(可选):生成 原文件名.info文件,记录源文件大小、分割数量、校验和(如 md5),用于合并时校验完整性。
sourcefile: archive.zip
filesize: 104857600
partcount: 10
chunksize: 1048576
md5: a1b2c3d4e5f6...

2.2 合并策略

  • 按序号合并:读取所有 原文件名.partn子文件,按序号从小到大拼接为完整文件。
  • 完整性校验:通过比对子文件数量、总大小或 md5 校验和,确保合并后文件与源文件一致。

三、实现步骤与代码

3.1 开发环境

  • 语言:c# 9.0+
  • 框架:.net 6.0(跨平台支持)
  • 工具:visual studio 2022 / visual studio code
  • 核心类库system.io(文件流)、system.security.cryptography(md5 校验)

3.2 文件分割实现

3.2.1 核心函数:splitfile

using system;
using system.io;
using system.security.cryptography;
using system.text;
public class filesplitter
{
    /// <summary>
    /// 分割文件为多个子文件
    /// </summary>
    /// <param name="sourcepath">源文件路径</param>
    /// <param name="outputdir">输出目录(默认源文件所在目录)</param>
    /// <param name="chunksizebytes">子文件大小(字节,默认 1mb)</param>
    /// <param name="generateinfofile">是否生成 .info 元数据文件</param>
    /// <returns>分割后的子文件数量</returns>
    public static int splitfile(string sourcepath, string outputdir = null, 
        long chunksizebytes = 1024 * 1024, bool generateinfofile = true)
    {
        // 参数校验
        if (!file.exists(sourcepath))
            throw new filenotfoundexception($"源文件不存在: {sourcepath}");
        if (chunksizebytes <= 0)
            throw new argumentexception("分块大小必须大于 0", nameof(chunksizebytes));
        // 确定输出目录
        outputdir ??= path.getdirectoryname(sourcepath);
        directory.createdirectory(outputdir);
        string filename = path.getfilename(sourcepath);
        string basename = path.getfilenamewithoutextension(sourcepath);
        string extension = path.getextension(sourcepath);
        string fullbasename = basename + extension;
        // 计算 md5(需要在读取流之前计算,或重新打开流)
        string md5hash = string.empty;
        if (generateinfofile)
            md5hash = calculatefilemd5(sourcepath);
        using (filestream sourcestream = new filestream(sourcepath, filemode.open, fileaccess.read))
        {
            long filesize = sourcestream.length;
            int partcount = (int)math.ceiling((double)filesize / chunksizebytes);
            byte[] buffer = new byte[chunksizebytes];
            // 生成元数据文件
            if (generateinfofile)
            {
                string infopath = path.combine(outputdir, $"{fullbasename}.info");
                using (streamwriter infowriter = new streamwriter(infopath, false, encoding.utf8))
                {
                    infowriter.writeline($"sourcefile: {filename}");
                    infowriter.writeline($"filesize: {filesize}");
                    infowriter.writeline($"partcount: {partcount}");
                    infowriter.writeline($"chunksize: {chunksizebytes}");
                    infowriter.writeline($"md5: {md5hash}");
                    infowriter.writeline($"createdat: {datetime.now:yyyy-mm-dd hh:mm:ss}");
                }
            }
            // 执行分割
            for (int i = 0; i < partcount; i++)
            {
                long position = i * chunksizebytes;
                int bytestoread = (int)math.min(chunksizebytes, filesize - position);
                sourcestream.seek(position, seekorigin.begin);
                int bytesread = sourcestream.read(buffer, 0, bytestoread);
                if (bytesread != bytestoread)
                    console.writeline($"警告: 第 {i + 1} 个分块实际读取 {bytesread} 字节,预期 {bytestoread} 字节");
                string partpath = path.combine(outputdir, $"{fullbasename}.part{i + 1}");
                using (filestream partstream = new filestream(partpath, filemode.create, fileaccess.write))
                {
                    partstream.write(buffer, 0, bytesread);
                }
                console.writeline($"[分割] 已创建: {path.getfilename(partpath)} ({bytesread} 字节)");
            }
            console.writeline($"分割完成: {partcount} 个分块,输出目录: {outputdir}");
            return partcount;
        }
    }
    /// <summary>
    /// 计算文件的 md5 校验值
    /// </summary>
    private static string calculatefilemd5(string filepath)
    {
        using (var md5 = md5.create())
        using (var stream = file.openread(filepath))
        {
            byte[] hash = md5.computehash(stream);
            return bitconverter.tostring(hash).replace("-", "").tolowerinvariant();
        }
    }
}

3.3 文件合并实现(完整版,含校验)

3.3.1 核心函数:mergefiles

public class filemerger
{
    /// <summary>
    /// 合并子文件为完整文件
    /// </summary>
    /// <param name="partdirectory">子文件所在目录</param>
    /// <param name="originalfilename">原文件名(如 "archive.zip")</param>
    /// <param name="outputpath">合并后的输出路径</param>
    /// <param name="verifyintegrity">是否校验 md5(需要 .info 文件)</param>
    /// <returns>合并后的文件大小(字节)</returns>
    public static long mergefiles(string partdirectory, string originalfilename, 
        string outputpath, bool verifyintegrity = true)
    {
        if (!directory.exists(partdirectory))
            throw new directorynotfoundexception($"目录不存在: {partdirectory}");
        string extension = path.getextension(originalfilename);
        string basename = path.getfilenamewithoutextension(originalfilename);
        string fullbasename = basename + extension;
        string pattern = $"{fullbasename}.part*";
        // 获取并排序分块文件
        string[] partfiles = directory.getfiles(partdirectory, pattern, searchoption.topdirectoryonly);
        if (partfiles.length == 0)
            throw new filenotfoundexception($"未找到匹配的分块文件: {pattern}");
        // 按序号排序(解析 partn 中的 n)
        array.sort(partfiles, (a, b) =>
        {
            int indexa = extractpartnumber(a, fullbasename);
            int indexb = extractpartnumber(b, fullbasename);
            return indexa.compareto(indexb);
        });
        // 验证序号连续性(可选)
        for (int i = 0; i < partfiles.length; i++)
        {
            int expected = i + 1;
            int actual = extractpartnumber(partfiles[i], fullbasename);
            if (actual != expected)
                throw new invalidoperationexception($"分块序号不连续: 期望 {expected},实际 {actual}");
        }
        // 合并文件
        using (filestream outputstream = new filestream(outputpath, filemode.create, fileaccess.write))
        {
            foreach (string partpath in partfiles)
            {
                using (filestream partstream = new filestream(partpath, filemode.open, fileaccess.read))
                {
                    partstream.copyto(outputstream);
                }
                console.writeline($"[合并] 已处理: {path.getfilename(partpath)}");
            }
        }
        console.writeline($"合并完成: {outputpath}");
        long mergedsize = new fileinfo(outputpath).length;
        // 完整性校验
        if (verifyintegrity)
        {
            string infopath = path.combine(partdirectory, $"{fullbasename}.info");
            if (file.exists(infopath))
            {
                var metadata = parseinfofile(infopath);
                string expectedmd5 = metadata["md5"];
                string actualmd5 = calculatefilemd5(outputpath);
                if (string.equals(expectedmd5, actualmd5, stringcomparison.ordinalignorecase))
                {
                    console.writeline($"[校验] md5 一致: {actualmd5}");
                }
                else
                {
                    console.writeline($"[警告] md5 不一致! 期望: {expectedmd5}, 实际: {actualmd5}");
                }
            }
            else
            {
                console.writeline("[校验] 未找到 .info 文件,跳过 md5 校验");
            }
        }
        return mergedsize;
    }
    /// <summary>
    /// 从文件名中提取分块序号(如 file.zip.part3 → 3)
    /// </summary>
    private static int extractpartnumber(string filepath, string fullbasename)
    {
        string filename = path.getfilenamewithoutextension(filepath);
        // 文件名格式: fullbasename.partn
        string suffix = filename.substring(fullbasename.length + 1); // ".partn" → "partn"
        if (suffix.startswith("part", stringcomparison.ordinalignorecase))
        {
            string numberpart = suffix.substring(4);
            if (int.tryparse(numberpart, out int number))
                return number;
        }
        throw new invalidoperationexception($"无法解析分块序号: {filepath}");
    }
    private static dictionary<string, string> parseinfofile(string infopath)
    {
        var dict = new dictionary<string, string>();
        foreach (var line in file.readalllines(infopath))
        {
            int colonindex = line.indexof(':');
            if (colonindex > 0)
            {
                string key = line.substring(0, colonindex).trim();
                string value = line.substring(colonindex + 1).trim();
                dict[key] = value;
            }
        }
        return dict;
    }
    private static string calculatefilemd5(string filepath)
    {
        using (var md5 = md5.create())
        using (var stream = file.openread(filepath))
        {
            byte[] hash = md5.computehash(stream);
            return bitconverter.tostring(hash).replace("-", "").tolowerinvariant();
        }
    }
}

3.4 主程序(增强版命令行交互)

class program
{
    static void main(string[] args)
    {
        console.writeline("=== 文件分割/合并工具 v2.0 ===");
        console.writeline("1. 分割文件");
        console.writeline("2. 合并文件");
        console.write("请选择 (1/2): ");
        string choice = console.readline();
        try
        {
            if (choice == "1")
            {
                console.write("源文件路径: ");
                string source = console.readline();
                console.write("分块大小 (mb,默认 1): ");
                string sizeinput = console.readline();
                int mb = string.isnullorwhitespace(sizeinput) ? 1 : int.parse(sizeinput);
                console.write("输出目录 (留空则使用源文件目录): ");
                string outputdir = console.readline();
                outputdir = string.isnullorwhitespace(outputdir) ? null : outputdir;
                filesplitter.splitfile(source, outputdir, mb * 1024l * 1024l);
            }
            else if (choice == "2")
            {
                console.write("分块文件目录: ");
                string partdir = console.readline();
                console.write("原文件名 (如 largefile.zip): ");
                string originalname = console.readline();
                console.write("合并后输出路径: ");
                string output = console.readline();
                console.write("是否校验完整性 (y/n, 默认 y): ");
                bool verify = console.readline()?.tolower() != "n";
                filemerger.mergefiles(partdir, originalname, output, verify);
            }
            else
            {
                console.writeline("无效输入");
            }
        }
        catch (exception ex)
        {
            console.writeline($"错误: {ex.message}");
            if (ex.innerexception != null)
                console.writeline($"详细信息: {ex.innerexception.message}");
        }
        console.writeline("按任意键退出...");
        console.readkey();
    }
}

四、关键技术点

4.1 二进制流高效处理

  • 使用 filestreamseek方法定位文件位置,避免全文件加载至内存。
  • 通过 copyto方法(.net 4.0+)实现流的高效复制,减少缓冲区操作。

4.2 异常处理与校验

  • 文件不存在:抛出 filenotfoundexception并提示路径。
  • 权限不足:捕获 unauthorizedaccessexception,建议以管理员身份运行。
  • md5 校验:合并后对比源文件与合并文件的 md5 值,确保数据一致性(示例中已记录源文件 md5 至 .info文件)。

4.3 跨平台兼容性

  • 使用 path.combine处理路径分隔符(\/),适配 windows/linux/macos。
  • 通过 .net core 运行时实现跨平台部署(需安装对应 sdk)。

五、性能优化建议

缓冲区调优默认缓冲区为 4kb,大文件可手动设置更大缓冲区:

using (var partstream = new filestream(partpath, filemode.open, fileaccess.read, fileshare.read, 81920))

并行处理(分割时)多个分块可并行写入(需注意磁盘 i/o 竞争):

parallel.for(0, partcount, i => { /* 写入 part i */ });

异步版本使用 readasync / writeasync 提升 ui 响应性(适用于 gui 版本)。

六、扩展功能建议(可后续迭代)

功能描述实现难度
图形界面(wpf / winui)拖拽文件、进度条显示、取消操作
加密分割分块使用 aes 加密,合并时解密
压缩分割分割前使用 gzip 压缩,减少存储
云存储集成分割后自动上传至 s3 / ftp / azure blob
断点续传式合并支持部分分块缺失时提示
多格式支持支持 .part.001.7z.001 等常见分卷格式

七、最佳实践与注意事项

  • 磁盘空间要求分割时输出目录需要至少等于源文件大小;合并时目标目录需要至少等于所有分块大小之和。
  • 文件权限确保程序对源目录和输出目录具有读写权限(linux 下注意 chmod 755)。
  • 元数据文件重要性强烈建议保留 .info 文件,否则合并时无法自动校验完整性。
  • 并发安全多线程版本需注意同一文件不可被多个线程同时写入。
  • 单元测试建议对核心函数编写单元测试,覆盖边界情况(空文件、单分块、序号乱序等)。

八、总结

本文实现了一个功能完整、生产可用的文件分割与合并工具,核心特点如下:

  • ✅ 分割:按固定大小拆分,生成分块文件 + 可选元数据
  • ✅ 合并:自动识别分块序号,拼接 + 完整性校验
  • ✅ 健壮性:异常处理、路径兼容、序号连续性校验
  • ✅ 可扩展:预留了加密、压缩、gui 等扩展接口

该工具可直接集成到备份系统、文件传输服务或作为独立命令行工具使用。如需完整项目代码(含 .csproj、单元测试、wpf gui 示例),可进一步提供。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com