---
id: dmtptransferfile
title: 传输文件
---

import Tag from "@site/src/components/Tag.js";

### 定义

命名空间：TouchSocket.Dmtp.FileTransfer <br/>
程序集：[TouchSocket.Dmtp.dll](https://www.nuget.org/packages/TouchSocket.Dmtp)<br/>
程序集：[TouchSocketPro.Dmtp.dll（多线程传输）](https://www.nuget.org/packages/TouchSocketPro.Dmtp)

## 一、说明

文件传输是每个框架都需要的功能，也是检验一个框架性能的非常重要的指标。

本组件则是基于Dmtp，开辟了全双工对点文件传输。即，当客户端连接服务器以后，客户端可以向服务器请求、推送文件，服务器也能向客户端请求，推送文件。甚至，客户端之间，也可以互相请求，推送文件。

其特点包括：

- 全双工对点文件传输。即：客户端、服务器、其他客户端三者之间，可以互相推送、请求文件。
- 高性能、低GC。整个传输过程，将内存池用到极致，极大的减少不必要的GC。本地电脑实测传输速度达到1.2Gb/秒。
- 全平台支持。Windows、Android、Unity3D（除webgl）全部支持。
- 支持任意大小的文件传输（实测100Gb没有问题）。
- 支持断点续传。
- 支持传输限速。
- 支持文件多链路、多线程传输 <Tag>Pro</Tag>。


## 二、性能

可以看到，下图正在上传一个Window的系统镜像文件，大约4.2Gb，传输速度已达到800Mb/s，GC基本上没有释放，性能非常强悍（中间有稍微停顿，因为程序在获取文件MD5值）。

<img src={require('@site/static/img/docs/transferfile-1.gif').default} />

## 三、产品应用场景

- 常规C/S应用使用场景：开发使用非常方便，连接验证，数据业务，文件传输等一系列功能完全集成。
- Unity游戏场景：性能卓越，功能丰富，使用方便。 <a name="h50Dz"></a>


## 四、传输流程及名词介绍

### 4.1 响应流程

1. 请求端（可能是客户端，也可能是服务器）调用Pull(请求)或Push（推送）。
2. 响应方（可能是服务器，也可能是客户端）触发**FileTransferring**。
3. 返回文件信息，然后检验是否续传等，然后开始传输。
4. 传输完成或异常。
5. 响应方**可能**触发**FileTransferred**。
6. 请求端函数返回，`FileOperator`控制器状态改变。

:::caution 注意

响应方必须在订阅的**OnFileTransferring**函数中，同意每一个传输（e.IsPermitOperation = true），不然会直接拒绝请求。当然如果是有意拒绝，则可以通过e.Message返回拒绝的信息。

:::  

:::caution 注意

响应方订阅的**OnFileTransferred**事件的触发并**不意味着完成传输**，具体结果还要通过**Result**属性值进行判断。

:::  

:::tip 提示

`请求端`返回的**result**，在成功时，可以100%表明传输的文件已在磁盘上。如果想进一步确定文件的准确性，还需要自行再验证MD5或者其他Hash算法。

:::  


### 4.2 传输控制器

`FileOperator`是本次文件传输的操作器，主要用于获取传输进度、速度、状态以及取消传输等操作。

可配置参数：

#### （1）ResourcePath

资源路径，在上传时，表示发起端的文件路径。在下载时，表示请求的文件路径。当该值为相对路径时，会与响应对点的RootPath组合路径。当为绝对路径时，则会直接访问路径文件。

:::tip 提示

如果是下载行为，响应方可在在订阅的**OnFileTransferring**函数中，随意重定向请求的文件路径（e.ResourcePath）。

:::  

:::danger 危险

当以绝对路径访问时，对方可能会请求到服务器电脑的**所有文件**，所以最好在**OnFileTransferring**里面进行安全的判断后再放行（e.IsPermitOperation = true;）。

:::  

#### （2）SavePath

保存路径，在上传时，表示需要保存的文件路径。在下载时，表示本地保存的文件路径。同样，当该值为相对路径时，会与接收对点的RootPath组合路径。当为绝对路径时，则会直接生效。

:::tip 提示

如果是上传行为，响应方可在在订阅的**OnFileTransferring**函数中，随意重定向文件的保存路径（e.SavePath）。

:::  

#### （3）ResourceInfo
已存在的资源信息，当上个传输失败时，可以保存其ResourceInfo，然后重新传输时赋值，即可尝试断点续传。

:::caution 注意

当执行续传时，本次传输与前次传输间隔时间不应该超过响应默认值（60秒）。

:::  

#### （4）CompletedLength
已完成流长度。

#### （5）Speed 函数
从上次获取到此次获得的速度。一般请每秒钟调用一次获取速度值。

:::caution 注意

当获取传输速度时，其值和获取时间完全相关。例如：假如实际每秒传输速度为100，当每隔一秒获取时，则为100.当每隔100毫秒获取时，则为10。

:::  

#### （6）Progress
传输进度，范围0-1。

#### （7） Result
获取传输状态以及状态信息。当ResultCode为Default时，意味着传输正在进行。

#### （8） Token
CancellationToken类型的可取消令箭。

#### （9） Metadata
string类型的键值对，用于和接收方交互数据。

<a name="JeQp8"></a>

## 五、 传输文件

传输文件功能，是`DmtpFileTransferFeature`功能插件基于Dmtp协议提供的功能。所以，`Dmtp服务器`和`客户端`都必须配置添加该功能插件。即调用`UseDmtpFileTransfer`。

```csharp {3}
config.ConfigurePlugins(a =>
{
    a.UseDmtpFileTransfer()//必须添加文件传输插件
    //.SetRootPath("C:\\新建文件夹")//设置RootPath
    .SetMaxSmallFileLength(1024 * 1024);//设置小文件的最大限制长度
    a.Add<MyPlugin>();
})
```

声明`MyPlugin`插件，是为了方便处理`OnDmtpFileTransferring`和`OnDmtpFileTransferred`函数。

```csharp showLineNumbers
internal class MyPlugin : PluginBase, IDmtpFileTransferringPlugin, IDmtpFileTransferredPlugin
{
    private readonly ILog m_logger;

    public MyPlugin(ILog logger)
    {
        this.m_logger = logger;
    }

    /// <summary>
    /// 该方法，会在每个文件被请求（推送）结束时触发。传输不一定成功，具体信息需要从e.Result判断状态。
    /// 其次，该方法也不一定会被执行，例如：在传输过程中，直接断网，则该方法将不会执行。
    /// </summary>
    /// <param name="client"></param>
    /// <param name="e"></param>
    /// <returns></returns>
    public async Task OnDmtpFileTransferred(IDmtpActorObject client, FileTransferredEventArgs e)
    {
        //传输结束，但是不一定成功，甚至该方法都不一定会被触发，具体信息需要从e.Result判断状态。
        this.m_logger.Info($"传输文件结束，请求类型={e.TransferType}，文件名={e.ResourcePath}，请求状态={e.Result}");
        await e.InvokeNext();
    }


    /// <summary>
    /// 该方法，会在每个文件被请求（推送）时第一时间触发。
    /// 当请求文件时，可以重新指定请求的文件路径，即对e.ResourcePath直接赋值。
    /// 当推送文件时，可以重新指定保存文件路径，即对e.SavePath直接赋值。
    /// 
    /// 注意：当文件夹不存在时，需要手动创建。
    /// </summary>
    /// <param name="client"></param>
    /// <param name="e"></param>
    /// <returns></returns>
    public  async Task OnDmtpFileTransferring(IDmtpActorObject client, FileTransferringEventArgs e)
    {
        foreach (var item in e.Metadata.Keys)
        {
            Console.WriteLine($"Key={item},Value={e.Metadata[item]}");
        }
        e.IsPermitOperation = true;//每次传输都需要设置true，表示允许传输
        //有可能是上传，也有可能是下载
        this.m_logger.Info($"请求传输文件，请求类型={e.TransferType}，请求文件名={e.ResourcePath}");
        await e.InvokeNext();
    }
}
```

### 5.1 客户端向服务器请求文件


【客户端代码】

```csharp showLineNumbers
var filePath = "ClientPullFileFromService.Test";
var saveFilePath = "SaveClientPullFileFromService.Test";

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    SavePath = saveFilePath,//客户端本地保存路径
    ResourcePath = filePath,//请求文件的资源路径
    Metadata = metadata,//传递到服务器的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512//分包大小，当网络较差时，应该适当减小该值
};

fileOperator.MaxSpeed=1024 * 1024;//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    client.Logger.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PullFileAsync
IResult result = client.GetDmtpFileTransferActor().PullFile(fileOperator);
```

### 5.2 客户端向服务器推送文件

```csharp showLineNumbers
var filePath = "ClientPushFileFromService.Test";
var saveFilePath = "SaveClientPushFileFromService.Test";

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    SavePath = saveFilePath,//服务器本地保存路径
    ResourcePath = filePath,//客户端本地即将上传文件的资源路径
    Metadata = metadata,//传递到服务器的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512//分包大小，当网络较差时，应该适当减小该值
};

fileOperator.MaxSpeed=1024 * 1024;//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    client.Logger.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PushFileAsync
IResult result = client.GetDmtpFileTransferActor().PushFile(fileOperator);
```

### 5.3 服务器向客户端请求文件

服务器主动向客户端请求文件，必须通过Id，找到其`SessionClient`的派生类。

```csharp showLineNumbers
string targetId="targetId";
if (!service.TryGetClient(targetId, out var SessionClient))
{
    throw new Exception($"没有找到Id={targetId}的客户端");
}

var filePath = "ServicePullFileFromClient.Test";
var saveFilePath = "SaveServicePullFileFromClient.Test";

var metadata = new Metadata();//传递到客户端的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    SavePath = saveFilePath,//服务器本地保存路径
    ResourcePath = filePath,//请求客户端文件的资源路径
    Metadata = metadata,//传递到客户端的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512//分包大小，当网络较差时，应该适当减小该值
};

fileOperator.MaxSpeed=1024 * 1024;//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    SessionClient.Logger.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PullFileAsync
IResult result = SessionClient.GetDmtpFileTransferActor().PullFile(fileOperator);
```

### 5.4 服务器向客户端推送文件

服务器主动向客户端推送文件，必须通过Id，找到其`SessionClient`的派生类。

```csharp showLineNumbers
if (!service.TryGetClient(targetId, out var SessionClient))
{
    throw new Exception($"没有找到Id={targetId}的客户端");
}

var filePath = "ServicePushFileFromClient.Test";
var saveFilePath = "SaveServicePushFileFromClient.Test";

var metadata = new Metadata();//传递到客户端的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    SavePath = saveFilePath,//客户端本地保存路径
    ResourcePath = filePath,//服务器文件的资源路径
    Metadata = metadata,//传递到客户端的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512//分包大小，当网络较差时，应该适当减小该值
};

fileOperator.MaxSpeed=1024 * 1024;//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    SessionClient.Logger.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PushFileAsync
IResult result = SessionClient.GetDmtpFileTransferActor().PushFile(fileOperator);
```

### 5.5 客户端之间传输文件

该功能支持客户端之间传输文件，使用方法基本一致，只需要在请求`PullFile`或者`PushFile`时额外增加目标Id即可。

此外，**响应中介（一般是服务器）**需要添加`路由策略`和`同意路由`。

【添加路由策略】

```csharp {3}
.ConfigureContainer(a =>
{
    a.AddDmtpRouteService();//添加路由策略
})
```

【同意路由】

```csharp {5}
internal class MyPlugin : PluginBase, IDmtpRoutingPlugin
{
    public  async Task OnDmtpRouting(IDmtpActorObject client, PackageRouterEventArgs e)
    {
        e.IsPermitOperation = true;//允许路由
        await e.InvokeNext();
    }
}
```

:::caution 注意

如果不添加`DmtpRouteService`策略，将不会转发任何路由请求，即意味着请求会被直接响应。

:::  

其他`PullFile`或者`PushFile`操作均和上述一致。

## 六、断点续传

本文件传输，支持断点续传。能够在传输过程中，暂停（实际上就是取消）传输，下次继续传输。

断点续传的使用步骤如下：

1. 当本次传输失败、或主动取消时，可以保存`fileOperator.ResourceInfo`对象属性。
2. 当下次再次请求传输时，可以将已保存的`ResourceInfo`对象，先赋值给`fileOperator.ResourceInfo`属性。那么如果保存路径一致，且文件大小符合续传要求，则可以继续传输。

```csharp showLineNumbers
//关于断点续传
//在执行完PullFile(fileOperator)或PushFile(fileOperator)时。只要返回的结果不是Success。
//那么就意味着传输没有完成。
//而续传的机制就是，在执行传输之前，如果fileOperator.ResourceInfo为空，则开始新的传输。如果不为空，则尝试续传。
//而对于失败的传输，未完成的信息都在fileOperator.ResourceInfo中。
//所以我们可以使用一个变量（或字典）来存fileOperator.ResourceInfo的值。
//亦或者可以把ResourceInfo的值持久化。
//然后在重新发起请求传输值前，先对fileOperator.ResourceInfo做有效赋值。即可尝试断点传输。

byte[] cacheBytes;//这就是持久化后的数据。你可以将此数据写入到文件或数据库。
using (var byteBlock=new ByteBlock())
{
    fileOperator.ResourceInfo.Save(byteBlock);

    cacheBytes = byteBlock.ToArray();
}

//然后想要续传的时候。先把缓存数据转为FileResourceInfo。
using (var byteBlock=new ByteBlock(cacheBytes))
{
    var resourceInfo = new FileResourceInfo(byteBlock);
    //然后把resourceInfo赋值给新建的FileOperator的ResourceInfo属性。
}
```

:::caution 注意

本断点续传不验证文件唯一性，假如在续传前后文件大小不一致，则会导致续传失败。如果文件大小一致，但是内容已发送变化，则可能得到一个错误的文件。所以建议在续传时要么确定文件不会再变化，要么需要在传输完成后进行文件唯一性验证（例如MD5）。

:::  

:::tip 提示

文件续传适用于所有传输类型，无论是客户端对服务器，还是服务器对客户端，还是客户端对客户端。

:::  

:::tip 关于续传持久化

一般来说，续传信息是不需要持久化的。只需要使用一个变量（或字典存一下即可）。但是如果考虑进程退出，或程序崩溃等情况，则需要持久化续传信息。

:::  

## 七、取消传输

传输取消，指的是，在传输过程中，用户主动取消传输。


### 7.1 Token传递

使用方法非常简单，因为`FileOperator`中允许传入一个可取消令箭。所以只需要通过`CancellationTokenSource`，将`Token`传入，然后直接使用`CancellationTokenSource`取消即可。

```csharp {1,7,13} showLineNumbers
var tokenSource = new CancellationTokenSource();

_=Task.Run(async () => 
{
    //此处模拟五秒后自动取消传输
    await Task.Delay(5000);
    tokenSource.Cancel();
});

var fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    ...
    Token = tokenSource.Token
};

IResult result = SessionClient.GetDmtpFileTransferActor().PullFile(fileOperator);
```

### 7.1 使用CancellationFileOperator 

`CancellationFileOperator`是继承`FileOperator`并且已经实现取消传输的操作器。原理和`Token`一致，这里只是做了一次封装。

```csharp {1,7} showLineNumbers
var fileOperator = new CancellationFileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    ...
};

//模拟五秒后自动取消传输，或者直接Cancel
fileOperator.CancelAfter(TimeSpan.FromSeconds(5));

IResult result = SessionClient.GetDmtpFileTransferActor().PullFile(fileOperator);
```

:::tip 提示

`取消传输`和`断点续传`结合，就能模拟`暂停传输`功能。这样，用户就可以在传输过程中，随时暂停和继续传输。并且没有暂停时长限制。

:::  


## 八、小文件传输

小文件传输是指，当传输文件小于设定大小（默认1024*1024字节）时的传输。

为什么要设立小文件传输？与常规文件传输相比，优点在哪里？

常规传输，建立一个传输通道，大约需要传输两端，往返通信4-6次。这在本地局域网中，显得无所谓。但是在互联网环境中，一次ping延迟平均50ms，那么建立一个传输，就大约需要200-300ms。这也就意味着，即使一个文件只有一字节，也需要200ms-300。所以，这明显是不合理的。所以又新增了小文件传输，只要文件在1Mb以内，仅往返1次，就可以完成传输。


### 8.1 拉取小文件

1. 直接调用PullSmallFile或者PullSmallFileAsync，获取到实际的文件数据。
2. 通过Save方法，将数据写入文件。也可以自行保存。

```csharp showLineNumbers
var filePath = "PullSmallFileFromService.Test";
var saveFilePath = "SavePullSmallFileFromService.Test";

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

//此方法会阻塞，直到传输结束，也可以使用PullSmallFileAsync
PullSmallFileResult result = client.GetDmtpFileTransferActor().PullSmallFile(filePath, metadata);
byte[] data = result.Value;//此处即是下载的小文件的实际数据
result.Save(saveFilePath,overwrite:true);//将数据保存到指定路径。
```

### 8.2 推送小文件

【推送文件】

1. 直接调用PushSmallFile或者PushSmallFileAsync。
2. 返回值即表示是否成功。

```csharp showLineNumbers
var filePath = "PushSmallFileFromService.Test";
var saveFilePath = "SavePushSmallFileFromService.Test";
if (!File.Exists(filePath))//创建测试文件
{
    using (var stream = File.OpenWrite(filePath))
    {
        stream.SetLength(1024);
    }
}

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

//此方法会阻塞，直到传输结束，也可以使用PullSmallFileAsync
var result = client.GetDmtpFileTransferActor().PushSmallFile(saveFilePath,new FileInfo(filePath), metadata);
if (result.IsSuccess())
{
    //成功
}
```

:::tip 提示

小文件传输也支持服务器主推至客户端，和客户端之间相互传输。

:::  

## 九、多线程文件传输 <Tag>Pro</Tag>

多线程文件传输，顾名思义，就是多个连接链路，共同传输一个文件。

多线程传输的优点是什么？和常规文件传输相比，场景有哪些不同？

首先，常规文件传输是基于单个连接链路的，所以，单个连接的传输速率上限，就是常规传输的上限。一般来说，局域网当中，单个连接即可占满所有带宽，所以这时候多线程传输和常规传输并无差别。但是，在云服务器，或者在有流量均衡算法的网络中，每个连接的最大速率不是带宽的最大速率，那么这时候，两个差距是比较大的。

例如，我自己租的一个单核云服务器，它的单个连接速率只有1Mb，但是弹性带宽却有10Mb。宏观表象就是，一个客户端连接时，可以用1Mb带宽，两个客户端连接时，就可以用2Mb。那么这时候，多线程传输就显得格外重要了。


### 9.1 创建多链路连接器

因为是多链路传输，所以，就必须建立多个客户端的连接到服务器。这里使用已经封装好的通信模型ClientFactory。

ClientFactory的通信模型使用的是一个主通信端+多个传输客户端。

对于客户端的配置，请详细参考[创建DmtpClient工厂](./dmtpclient.mdx)

```csharp showLineNumbers
var clientFactory = new TcpDmtpClientFactory()
{
    MinCount = 5,
    MaxCount = 10,
    OnGetTransferConfig = () => //配置辅助通信
    {
        return new TouchSocketConfig()
        .SetRemoteIPHost("127.0.0.1:7789")
        .SetDmtpOption(new DmtpOption()
        {
             VerifyToken = "File"//连接验证口令。
        })
        .ConfigurePlugins(a =>
        {
            a.UseDmtpFileTransfer();
        });
    }
};
clientFactory.MainConfig//配置主通信
             .SetRemoteIPHost("127.0.0.1:7789")
             .SetDmtpOption(new DmtpOption()
             {
                 VerifyToken = "Rpc"//连接验证口令。
             })
             .ConfigurePlugins(a =>
             {
                 a.UseDmtpFileTransfer();
             });
```

### 9.2 多线程请求文件

```csharp showLineNumbers
using var clientFactory = CreateClientFactory();
Result resultCon = clientFactory.CheckStatus();//检验连接状态，一般当主通行器连接时，即认为在连接状态。
if (!resultCon.IsSuccess())
{
    //没有连接
    return;
}
ConsoleLogger.Default.Info("开始从服务器下载文件");
var filePath = "MultithreadingClientPullFileFromService.Test";
var saveFilePath = "SaveMultithreadingClientPullFileFromService.Test";

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new MultithreadingFileOperator//实例化本次传输的控制器，用于获取传输进度、速度、
{
    SavePath = saveFilePath,//客户端本地保存路径
    ResourcePath = filePath,//请求文件的资源路径
    Metadata = metadata,//传递到服务器的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512,//分包大小，当网络较差时，应该适当减小该值
    MultithreadingCount = 10//多线程数量
};

fileOperator.SetMaxSpeed(1024*1024);//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    ConsoleLogger.Default.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PullFileAsync
IResult result = clientFactory.PullFile(fileOperator);

ConsoleLogger.Default.Info($"从服务器下载文件结束，{result}");
```

### 9.3 多线程推送文件

```csharp showLineNumbers
using var clientFactory = CreateClientFactory();
Result resultCon = clientFactory.CheckStatus();//检验连接状态，一般当主通行器连接时，即认为在连接状态。

if (!resultCon.IsSuccess())
{
    //没有连接
    return;
}
ConsoleLogger.Default.Info("开始向服务器推送文件");
var filePath = "MultithreadingClientPushFileFromService.Test";
var saveFilePath = "SaveMultithreadingClientPushFileFromService.Test";

var metadata = new Metadata();//传递到服务器的元数据
metadata.Add("1", "1");
metadata.Add("2", "2");

var fileOperator = new MultithreadingFileOperator//实例化本次传输的控制器，用于获取传输进度、速度、状态等
{
    SavePath = saveFilePath,//客户端本地保存路径
    ResourcePath = filePath,//请求文件的资源路径
    Metadata = metadata,//传递到服务器的元数据
    Timeout = TimeSpan.FromSeconds(60),//传输超时时长
    TryCount = 10,//当遇到失败时，尝试次数
    FileSectionSize = 1024 * 512,//分包大小，当网络较差时，应该适当减小该值
    MultithreadingCount = 10//多线程数量
};

fileOperator.SetMaxSpeed(1024*1024);//设置最大限速为1Mb。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
var loopAction = LoopAction.CreateLoopAction(-1, 1000, (loop) =>
{
    if (fileOperator.Result.ResultCode != ResultCode.Default)
    {
        loop.Dispose();
    }
    ConsoleLogger.Default.Info($"进度：{fileOperator.Progress}，速度：{fileOperator.Speed()}");
});

loopAction.RunAsync();

//此方法会阻塞，直到传输结束，也可以使用PushFileAsync
IResult result = clientFactory.PushFile(fileOperator);
```

### 9.4 客户端之间传输文件

该功能也支持客户端之间互相传输。使用方法基本一致，需要额外指定目标Id，以及**获取传输的Id集合**即可。

多线程的客户端之间传输文件，不像其他操作类型那么简单。因为除了需要指定目的Id外，还需要指定获取目标Id的，传输客户端的Id集合，不然，获取数据的时候，仍然会是单线程工作的。


【获取目标传输客户端的Id集合】
在TcpDmtpClientFactory属性中，有个OnFindTransferIds。通过实现该属性，使其能够获取到对应客户端的传输客户端Id集合（下列代码为模拟值，要具体实现该功能，还得自行实现）。

```csharp showLineNumbers
clientFactory.SetFindTransferIds((client, targetId) =>
{
    //此处的操作不唯一，可能需要rpc实现。
    //其目的比较简单，就是获取到targetId对应的主客户端的所有传输客户端的Id集合。
    //这样就实现了多个客户端向多个客户端传输文件的目的。

    return new string[] { targetId };//此处为模拟结果。
});
```

:::info 信息

多线程传输，目前暂不支持服务器主动向客户端传输。

::: 

[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Dmtp/FileTransferConsoleApp)