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

## 一、说明

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

TouchRpc开辟了对点文件传输。即，当客户端连接服务器以后，两者可以任意，随时的互相发送文件。不仅如此，即使是客户端之间，可以发送文件。

下列示例仅演示由**TcpTouchRpcClient**到**TcpTouchRpcService**（实际上是**TcpTouchRpcSocketClient**）的操作。

对点之间可以任意pull（拉取）、push（推送）文件。**接收对点**可以订阅**FileTransfering**和**FileTransfered**事件，来获取相关信息，发起对点直接通过传输控制器或返回值获取传输信息。

## 二、性能

可以看到，下图正在上传一个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>

## 四、服务架构

- 其传输架构是基于Channel工作的。所以当在同一时间，可进行多个传输并行，且数据互不影响。
- 在文件传输时，每个连接端和服务器均是平等权利的，所以RRQM将其命名为对点。任意两个对点之间均可Pull（拉取或下载）或Push（推送或上传）文件，例如下图中，Client1、SocketClient1、Client2、SocketClient2四个互相为对点，均可自由传输文件。


## 五、传输文件

由**TcpTouchRpcClient**向**TcpTouchRpcService**发起Pull请求时，相当于由客户端从服务器下载文件。

下列示例仅演示Pull请求，Push请求操作一致。

### 5.1 响应流程

1. 发起Pull请求。
2. 接收对点（即此处的服务器）触发**FileTransfering**事件。
3. 返回文件信息，然后检验是否续传等，然后开始接收。
4. 接收完成或异常。
5. 接收对点（即此处的服务器）触发**FileTransfered**事件。
6. 请求端（此处的客户端）函数返回，控制器状态改变。

### 5.2 请求配置

FileOperator是本次传输的请求操作器，主要用于获取传输进度、速度、状态以及取消传输等操作。**接收方的控制器从FileTransfering事件的参数e中获得。**

可配置参数：

#### （1）ResourcePath

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

:::tip 提示

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

:::  

:::danger 危险

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

:::  

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

:::tip 提示

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

:::  

#### （3）Flags
可通过叠加位域的形式，尝试断点续传。

#### （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>

### 5.3 示例代码

**【服务器】**

```csharp
var service = new TouchSocketConfig()//配置
    .SetListenIPHosts(new IPHost[] { new IPHost(7789) })
    .UsePlugin()
    .ConfigureContainer(a =>
    {
        a.AddConsoleLogger();
        a.AddFileLogger();
    })
    .ConfigurePlugins(a =>
    {
        a.Add<MyPlugin>();
    }).UsePlugin() //
    .SetVerifyToken("File")//连接验证口令。
    .BuildWithTcpTouchRpcService();//此处build相当于new TcpTouchRpcService，然后Setup，然后Start。
service.Logger.Info("服务器成功启动");
```

```csharp
class MyPlugin : TouchRpcPluginBase<TcpTouchRpcSocketClient>
{
    protected override void OnFileTransfering(TcpTouchRpcSocketClient client, FileOperationEventArgs e)
    {
        e.IsPermitOperation = true;//运行操作

        //有可能是上传，也有可能是下载
        client.Logger.Info($"有客户端请求传输文件，ID={client.ID}，请求类型={e.TransferType}，请求文件名={e.ResourcePath}");
    }

    protected override void OnFileTransfered(TcpTouchRpcSocketClient client, FileTransferStatusEventArgs e)
    {
        //传输结束，但是不一定成功，需要从e.Result判断状态。
        client.Logger.Info($"客户端传输文件结束，ID={client.ID}，请求类型={e.TransferType}，文件名={e.ResourcePath}，请求状态={e.Result}");
    }

    protected override void OnHandshaked(TcpTouchRpcSocketClient client, VerifyOptionEventArgs e)
    {
        client.Logger.Info($"有客户端成功验证，ID={client.ID}");
    }

    protected override void OnDisconnected(TcpTouchRpcSocketClient client, ClientDisconnectedEventArgs e)
    {
        client.Logger.Info($"有客户端断开，ID={client.ID}");
        base.OnDisconnected(client, e);
    }
}
```

:::caution 注意

响应方必须在订阅的**OnFileTransfering**函数中，同意每一个传输（e.IsPermitOperation = true），不然请求方会直接失败。

:::  

:::caution 注意

响应方订阅的**FileTransfered**事件的触发并**不意味着完成传输**，具体结果还要通过**Result**属性值进行判断。 同时当**Result**为成功时，也有极小的概率失败。总之响应端无法100%得知传输的最终结果。

:::  

【客户端】

```csharp
TcpTouchRpcClient client = new TouchSocketConfig()
    .SetRemoteIPHost("127.0.0.1:7789")
    .SetVerifyToken("File")
    .UsePlugin()
    .ConfigureContainer(a => 
    {
        a.AddConsoleLogger();
        a.AddFileLogger();
    })
    .ConfigurePlugins(a =>
    {
        a.UseTouchRpcHeartbeat<TcpTouchRpcClient>();
    })
    .BuildWithTcpTouchRpcClient();

client.Logger.Info("连接成功");

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

FileOperator fileOperator = new FileOperator()//实例化本次传输的控制器，用于获取传输进度、速度、状态等。
{
    Flags = TransferFlags.BreakpointResume,//尝试断点续传，使用断点续传时，会验证MD5值
    SavePath = $@"Windows.iso",//保存路径
    ResourcePath = @"D:\System\Windows.iso",//请求路径
    Metadata= metadata//传递到服务器的元数据
};

fileOperator.Timeout = TimeSpan.FromSeconds(60);//当传输大文件，且启用断点续传时，服务器可能会先计算MD5，而延时响应，所以需要设置超时时间。

//此处的作用相当于Timer，定时每秒输出当前的传输进度和速度。
LoopAction 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.PullFile(fileOperator);

client.Logger.Info(result.ToString());
```

:::tip 提示

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

:::  

<a name="qZvBF"></a>

## 六、客户端之间传输文件

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

此外，**服务器**也需要同意路由。需要注意的是，使用该方式文件传输时，还会发起通道路由，所以，需要允许的路由应该还额外增加**通道类型**。

```csharp
internal class MyTouchRpcPlugin : TouchRpcPluginBase
{
    protected override void OnRouting(ITouchRpc client, PackageRouterEventArgs e)
    {
        if (e.RouterType== RouteType.PushFile||e.RouterType== RouteType.PullFile||e.RouterType== RouteType.CreateChannel)
        {
            e.IsPermitOperation = true;
        }
        base.OnRouting(client, e);
    }
}
```
