C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能

  初识gRPC还是一位做JAVA的同事在项目中用到了它,为了C#的客户端程序和java的服务器程序进行通信和数据交换,当时还是对方编译成C#,我直接调用。

  后来,自己下来做了C#版本gRPC编写,搜了很多资料,但许多都是从入门开始?调用说“Say Hi!”这种官方标准的入门示例,然后遇到各种问题……

  关于gRPC和Protobuf介绍,就不介绍了,网络上一搜一大把,随便一抓都是标准的官方,所以直接从使用说起。

  gPRC源代码:https://github.com/grpc/grpc;

  protobuf的代码仓库:

github仓库地址:https://github.com/google/protobuf

Google下载protobuff下载地址:https://developers.google.com/protocol-buffers/docs/downloads

1、新建解决方案

  分别在VS中新建解决方案:GrpcTest;再在解决方案中新建三个项目:GrpcClient、GrpcServer、GrpcService,对应的分别是客户端(wpf窗体程序)、服务端(控制台程序)、gRPC服务者(控制台程序)。在GrpcClient和GrpcServer项目中添加对GrpcService的引用。

  在VS中对3个项目添加工具包引用:右键点击“解决方案gRPCDemo”,点击“管理解决方案的NuGet程序包”,在浏览中分别搜索"Grpc"、"Grpc.Tools"、"Google.Protobuf",然后点击右面项目,全选,再点击安装(也可以用视图 -> 窗口 -> 程序包管理器控制台 中的"Install-Package Grpc"进行这一步,这里不提供这种方法,有兴趣自己百度)。

2、proto文件的语法

  对于使用gRPC的通信框架,需要使用到对应的通信文件。在gRPC中,使用到的是proto格式的文件,对应的自然有其相应的语法。本文不详细阐述该文件的语法,感兴趣可以去官网看标准的语法,这儿有一个链接,中文翻译比较全的https://www.codercto.com/a/45372.html。需要对其文章内的1.3进行补充下:

  • required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的。
  • optional:消息格式中该字段可以有0个或1个值(不超过1个)。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。

  本示例项目实现文件传输,因此在项目GrpcService中添加一个FileTransfer.proto文件,文件内容如下:

syntax = "proto3";
package GrpcService;

service FileTransfer{
 rpc FileDownload (FileRequest) returns (stream FileReply);
 rpc FileUpload (stream FileReply) returns(stream FileReturn);
}

//请求下载文件时,所需下载文件的文件名称集合
message FileRequest{
 repeated string FileNames=1;//文件名集合
 //repeated重复字段 类似链表;optional可有可无的字段;required必要设置字段
 string Mark = 2;//携带的包
}

//下载和上传文件时的应答数据
message FileReply{
 string FileName=1;//文件名
 int32 Block = 2;//标记---第几个数据
 bytes Content = 3;//数据
 string Mark = 4;//携带的包
 }

//数据上传时的返回值
message FileReturn{
 string FileName=1;//文件名
 string Mark = 2;//携带的包
}

3、编译proto文件为C#代码

  proto文件仅仅只是定义了相关的数据,如果需要在代码中使用该格式,就需要将它编译成C#代码文件。

    PS:网上可以找到的编译,需要下载相关的代码,见博文。其他的也较为繁琐,所以按照自己理解的来写了。注意,我的项目是放在D盘根目录下的。

  首先打开cmd窗口,然后在窗口中输入:D:\GrpcTest\packages\Grpc.Tools.2.32.0\tools\windows_x86\protoc.exe -ID:\GrpcTest\GrpcService --csharp_out D:\GrpcTest\GrpcService D:\GrpcTest\GrpcService\FileTransfer.proto --grpc_out D:\GrpcTest\GrpcService --plugin=protoc-gen-grpc=D:\GrpcTest\packages\Grpc.Tools.2.32.0\tools\windows_x86\grpc_csharp_plugin.exe

  输入上文后,按enter键,回车编译。

  命令解读:

  • D:\GrpcTest\packages\Grpc.Tools.2.32.0\tools\windows_x86\protoc.exe :调用的编译程序路径,注意版本不同路径稍有不一样。
  • -ID:\GrpcTest\GrpcService :-I 指定一个或者多个目录,用来搜索.proto文件的。所以上面那行的D:\GrpcTest\GrpcService\FileTransfer.proto 已经可以换成FileTransfer.proto了,因为-I已经指定了。注意:如果不指定,那就是当前目录。
  • --csharp_out D:\GrpcTest\GrpcService D:\GrpcTest\GrpcService\FileTransfer.proto :(--csharp_out)生成C#代码、存放路径、文件。当然还能cpp_out、java_out、javanano_out、js_out、objc_out、php_out、python_out、ruby_out 这时候你就应该知道,可以支持多语言的,才用的,生成一些文件,然后给各个语言平台调用。参数1(D:\GrpcTest\GrpcService)是输出路径,参数2(D:\GrpcTest\GrpcService\FileTransfer.proto)是proto的文件名或者路径。
  • --grpc_out D:\GrpcTest\GrpcService :grpc_out是跟服务相关,创建,调用,绑定,实现相关。生成的玩意叫xxxGrpc.cs。与前面的区别是csharp_out是输出类似于咱们平时写的实体类,接口,定义之类的。生成的文件叫xxx.cs
  • --plugin=protoc-gen-grpc=D:\GrpcTest\packages\Grpc.Tools.2.32.0\tools\windows_x86\grpc_csharp_plugin.exe :这个就是csharp的插件,python有python的,java有java的。

  编译后,会在新增两个文件(文件位置与你的输出位置有关),并将两个文件加入到GrpcService项目中去:

    

4、编写服务端的文件传输服务

  在GrpcServer项目中,新建一个FileImpl并继承自GrpcService.FileTransfer.FileTransferBase,然后复写其方法FileDownload和FileUpload方法,以供客户端进行调用。

/// <summary>
/// 文件传输类
/// </summary>
class FileImpl:GrpcService.FileTransfer.FileTransferBase
{
 /// <summary>
 /// 文件下载
 /// </summary>
 /// <param name="request">下载请求</param>
 /// <param name="responseStream">文件写入流</param>
 /// <param name="context">站点上下文</param>
 /// <returns></returns>
 public override async Task FileDownload(FileRequest request, global::Grpc.Core.IServerStreamWriter<FileReply> responseStream, global::Grpc.Core.ServerCallContext context)
 {
 List<string> lstSuccFiles = new List<string>();//传输成功的文件
 DateTime startTime = DateTime.Now;//传输文件的起始时间
 int chunkSize = 1024 * 1024;//每次读取的数据
 var buffer = new byte[chunkSize];//数据缓冲区
 FileStream fs = null;//文件流
 try
 {
  //reply.Block数字的含义是服务器和客户端约定的
  for (int i = 0; i < request.FileNames.Count; i++)
  {
  string fileName = request.FileNames[i];//文件名
  string filePath = Path.GetFullPath($".//Files\\{fileName}");//文件路径
  FileReply reply = new FileReply
  {
   FileName = fileName,
   Mark = request.Mark
  };//应答数据
  Console.WriteLine($"{request.Mark},下载文件:{filePath}");//写入日志,下载文件
  if (File.Exists(filePath))
  {
   fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, chunkSize, useAsync: true);

   //fs.Length 可以告诉客户端所传文件大小
   int readTimes = 0;//读取次数
   while (true)
   {
   int readSise = fs.Read(buffer, 0, buffer.Length);//读取数据
   if (readSise > 0)//读取到了数据,有数据需要发送
   {
    reply.Block = ++readTimes;
    reply.Content = Google.Protobuf.ByteString.CopyFrom(buffer, 0, readSise);
    await responseStream.WriteAsync(reply);
   }
   else//没有数据了,就告诉对方,读取完了
   {
    reply.Block = 0;
    reply.Content = Google.Protobuf.ByteString.Empty;
    await responseStream.WriteAsync(reply);
    lstSuccFiles.Add(fileName);
    Console.WriteLine($"{request.Mark},完成发送文件:{filePath}");//日志,记录发送成功
    break;//跳出去
   }
   }
   fs?.Close();
  }
  else
  {
   Console.WriteLine($"文件【{filePath}】不存在。");//写入日志,文件不存在
   reply.Block = -1;//-1的标记为文件不存在
   await responseStream.WriteAsync(reply);//告诉客户端,文件状态
  }
  }
  //告诉客户端,文件传输完成
  await responseStream.WriteAsync(new FileReply
  {
  FileName = string.Empty,
  Block = -2,//告诉客户端,文件已经传输完成
  Content = Google.Protobuf.ByteString.Empty,
  Mark = request.Mark
  });
 }
 catch(Exception ex)
 {
  Console.WriteLine($"{request.Mark},发生异常({ex.GetType()}):{ex.Message}");
 }
 finally
 {
  fs?.Dispose();
 }
 Console.WriteLine($"{request.Mark},文件传输完成。共计【{lstSuccFiles.Count / request.FileNames.Count}】,耗时:{DateTime.Now - startTime}");
 }

 /// <summary>
 /// 上传文件
 /// </summary>
 /// <param name="requestStream">请求流</param>
 /// <param name="responseStream">响应流</param>
 /// <param name="context">站点上下文</param>
 /// <returns></returns>
 public override async Task FileUpload(global::Grpc.Core.IAsyncStreamReader<FileReply> requestStream, global::Grpc.Core.IServerStreamWriter<FileReturn> responseStream, global::Grpc.Core.ServerCallContext context)
 {
 List<string> lstFilesName = new List<string>();//文件名
 List<FileReply> lstContents = new List<FileReply>();//数据集合

 FileStream fs = null;
 DateTime startTime = DateTime.Now;//开始时间
 string mark = string.Empty;
 string savePath = string.Empty;
 try
 {
  //reply.Block数字的含义是服务器和客户端约定的
  while (await requestStream.MoveNext())//读取数据
  {
  var reply = requestStream.Current;
  mark = reply.Mark;
  if (reply.Block == -2)//传输完成
  {
   Console.WriteLine($"{mark},完成上传文件。共计【{lstFilesName.Count}】个,耗时:{DateTime.Now-startTime}");
   break;
  }
  else if (reply.Block == -1)//取消了传输
  {
   Console.WriteLine($"文件【{reply.FileName}】取消传输!");//写入日志
   lstContents.Clear();
   fs?.Close();//释放文件流
   if (!string.IsNullOrEmpty(savePath) && File.Exists(savePath))//如果传输不成功,删除该文件
   {
   File.Delete(savePath);
   }
   savePath = string.Empty;
   break;
  }
  else if(reply.Block==0)//文件传输完成
  {
   if (lstContents.Any())//如果还有数据,就写入文件
   {
   lstContents.OrderBy(c => c.Block).ToList().ForEach(c => c.Content.WriteTo(fs));
   lstContents.Clear();
   }
   lstFilesName.Add(savePath);//传输成功的文件
   fs?.Close();//释放文件流
   savePath = string.Empty;

   //告知客户端,已经完成传输
   await responseStream.WriteAsync(new FileReturn
   {
   FileName= reply.FileName,
   Mark=mark
   });
  }
  else
  {
   if(string.IsNullOrEmpty(savePath))//有新文件来了
   {
   savePath = Path.GetFullPath($".//Files\\{reply.FileName}");//文件路径
   fs = new FileStream(savePath, FileMode.Create, FileAccess.ReadWrite);
   Console.WriteLine($"{mark},上传文件:{savePath},{DateTime.UtcNow.ToString("HH:mm:ss:ffff")}");
   }
   lstContents.Add(reply);//加入链表
   if (lstContents.Count() >= 20)//每个包1M,20M为一个集合,一起写入数据。
   {
   lstContents.OrderBy(c => c.Block).ToList().ForEach(c => c.Content.WriteTo(fs));
   lstContents.Clear();
   }
  }
  }
 }
 catch(Exception ex)
 {
  Console.WriteLine($"{mark},发生异常({ex.GetType()}):{ex.Message}");
 }
 finally
 {
  fs?.Dispose();
 }
 }
}

  在main函数中添加服务:

class Program
{
 static void Main(string[] args)
 {
 //提供服务
 Server server = new Server()
 {
  Services = {GrpcService.FileTransfer.BindService(new FileImpl())},
  Ports = {new ServerPort("127.0.0.1",50000,ServerCredentials.Insecure)}
 };
 //服务开始
 server.Start();

 while(Console.ReadLine().Trim().ToLower()!="exit")
 {

 }
 //结束服务
 server.ShutdownAsync();
 }
}

5、编写客户端的文件传输功能

  首先定义一个文件传输结果类TransferResult<T>,用于存放文件的传输结果。

/// <summary>
/// 传输结果
/// </summary>
/// <typeparam name="T"></typeparam>
class TransferResult<T>
{
 /// <summary>
 /// 传输是否成功
 /// </summary>
 public bool IsSuccessful { get; set; }
 /// <summary>
 /// 消息
 /// </summary>
 public string Message { get; set; }

 /// <summary>
 /// 标记类型
 /// </summary>
 public T Tag { get; set; } = default;
}

  然后在GrpcClinet项目中添加一个FileTransfer的类,并实现相关方法:

class FileTransfer
{

 /// <summary>
 /// 获取通信客户端
 /// </summary>
 /// <returns>通信频道、客户端</returns>
 static (Channel, GrpcService.FileTransfer.FileTransferClient) GetClient()
 {
 //侦听IP和端口要和服务器一致
 Channel channel = new Channel("127.0.0.1", 50000, ChannelCredentials.Insecure);
 var client = new GrpcService.FileTransfer.FileTransferClient(channel);
 return (channel, client);
 }

 /// <summary>
 /// 下载文件
 /// </summary>
 /// <param name="fileNames">需要下载的文件集合</param>
 /// <param name="mark">标记</param>
 /// <param name="saveDirectoryPath">保存路径</param>
 /// <param name="cancellationToken">异步取消命令</param>
 /// <returns>下载任务(是否成功、原因、失败文件名)</returns>
 public static async Task<TransferResult<List<string>>> FileDownload(List<string> fileNames, string mark, string saveDirectoryPath, System.Threading.CancellationToken cancellationToken = new System.Threading.CancellationToken())
 {
 var result = new TransferResult<List<string>>() { Message = $"文件保存路径不正确:{saveDirectoryPath}" };
 if (!System.IO.Directory.Exists(saveDirectoryPath))
 {
  return await Task.Run(() => result);//文件路径不存在
 }
 if (fileNames.Count == 0)
 {
  result.Message = "未包含任何文件";
  return await Task.Run(() => result);//文件路径不存在
 }
 result.Message = "未能连接到服务器";
 FileRequest request = new FileRequest() { Mark = mark };//请求数据
 request.FileNames.AddRange(fileNames);//将需要下载的文件名赋值
 var lstSuccFiles = new List<string>();//传输成功的文件
 string savePath = string.Empty;//保存路径
 System.IO.FileStream fs = null;
 Channel channel = null;//申明通信频道
 GrpcService.FileTransfer.FileTransferClient client = null;
 DateTime startTime = DateTime.Now;
 try
 {
  (channel, client) = GetClient();
  using (var call = client.FileDownload(request))
  {
  List<FileReply> lstContents = new List<FileReply>();//存放接收的数据
  var reaponseStream = call.ResponseStream;
  //reaponseStream.Current.Block数字的含义是服务器和客户端约定的
  while (await reaponseStream.MoveNext(cancellationToken))//开始接收数据
  {
   if (cancellationToken.IsCancellationRequested)
   {
   break;
   }
   if (reaponseStream.Current.Block == -2)//说明文件已经传输完成了
   {
   result.Message = $"完成下载任务【{lstSuccFiles.Count}/{fileNames.Count}】,耗时:{DateTime.Now - startTime}";
   result.IsSuccessful = true;
   break;
   }
   else if (reaponseStream.Current.Block == -1)//当前文件传输错误
   {
   Console.WriteLine($"文件【{reaponseStream.Current.FileName}】传输失败!");//写入日志
   lstContents.Clear();
   fs?.Close();//释放文件流
   if (!string.IsNullOrEmpty(savePath) && File.Exists(savePath))//如果传输不成功,删除该文件
   {
    File.Delete(savePath);
   }
   savePath = string.Empty;
   }
   else if (reaponseStream.Current.Block == 0)//当前文件传输完成
   {
   if (lstContents.Any())//如果还有数据,就写入文件
   {
    lstContents.OrderBy(c => c.Block).ToList().ForEach(c => c.Content.WriteTo(fs));
    lstContents.Clear();
   }
   lstSuccFiles.Add(reaponseStream.Current.FileName);//传输成功的文件
   fs?.Close();//释放文件流
   savePath = string.Empty;
   }
   else//有文件数据过来
   {
   if (string.IsNullOrEmpty(savePath))//如果字节流为空,则说明时新的文件数据来了
   {
    savePath = Path.Combine(saveDirectoryPath, reaponseStream.Current.FileName);
    fs = new FileStream(savePath, FileMode.Create, FileAccess.ReadWrite);
   }
   lstContents.Add(reaponseStream.Current);//加入链表
   if (lstContents.Count() >= 20)//每个包1M,20M为一个集合,一起写入数据。
   {
    lstContents.OrderBy(c => c.Block).ToList().ForEach(c => c.Content.WriteTo(fs));
    lstContents.Clear();
   }
   }
  }
  }
  fs?.Close();//释放文件流
  if (!result.IsSuccessful &&!string.IsNullOrEmpty(savePath)&& File.Exists(savePath))//如果传输不成功,那么久删除该文件
  {
  File.Delete(savePath);
  }
 }
 catch (Exception ex)
 {
  if (cancellationToken.IsCancellationRequested)
  {
  fs?.Close();//释放文件流
  result.IsSuccessful = false;
  result.Message = $"用户取消下载。已完成下载【{lstSuccFiles.Count}/{fileNames.Count}】,耗时:{DateTime.Now - startTime}";
  }
  else
  {
  result.Message = $"文件传输发生异常:{ex.Message}";
  }
 }
 finally
 {
  fs?.Dispose();
 }
 result.Tag = fileNames.Except(lstSuccFiles).ToList();//获取失败文件集合
 //关闭通信、并返回结果
 return await channel?.ShutdownAsync().ContinueWith(t => result);
 }

 /// <summary>
 /// 文件上传
 /// </summary>
 /// <param name="filesPath">文件路径</param>
 /// <param name="mark">标记</param>
 /// <param name="cancellationToken">异步取消命令</param>
 /// <returns>下载任务(是否成功、原因、成功的文件名)</returns>
 public static async Task<TransferResult<List<string>>> FileUpload(List<string> filesPath, string mark, System.Threading.CancellationToken cancellationToken=new System.Threading.CancellationToken())
 {
 var result = new TransferResult<List<string>> { Message = "没有文件需要下载" };
 if (filesPath.Count == 0)
 {
  return await Task.Run(() => result);//没有文件需要下载
 }
 result.Message = "未能连接到服务器。";
 var lstSuccFiles = new List<string>();//传输成功的文件
 int chunkSize = 1024 * 1024;
 byte[] buffer = new byte[chunkSize];//每次发送的大小
 FileStream fs = null;//文件流
 Channel channel = null;//申明通信频道
 GrpcService.FileTransfer.FileTransferClient client = null;
 DateTime startTime = DateTime.Now;
 try
 {
  (channel, client) = GetClient();
  using(var stream=client.FileUpload())//连接上传文件的客户端
  {
  //reply.Block数字的含义是服务器和客户端约定的
  foreach (var filePath in filesPath)//遍历集合
  {
   if(cancellationToken.IsCancellationRequested)
   break;//取消了传输
   FileReply reply = new FileReply()
   {
   FileName=Path.GetFileName(filePath),
   Mark=mark
   };
   if(!File.Exists(filePath))//文件不存在,继续下一轮的发送
   {
   Console.WriteLine($"文件不存在:{filePath}");//写入日志
   continue;
   }
   fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, chunkSize, useAsync: true);
   int readTimes = 0;
   while(true)
   {
   if (cancellationToken.IsCancellationRequested)
   {
    reply.Block = -1;//取消了传输
    reply.Content = Google.Protobuf.ByteString.Empty;
    await stream.RequestStream.WriteAsync(reply);//发送取消传输的命令
    break;//取消了传输
   }
   int readSize = fs.Read(buffer, 0, buffer.Length);//读取数据
   if(readSize>0)
   {
    reply.Block = ++readTimes;//更新标记,发送数据
    reply.Content = Google.Protobuf.ByteString.CopyFrom(buffer, 0, readSize);
    await stream.RequestStream.WriteAsync(reply);
   }
   else
   {
    Console.WriteLine($"完成文件【{filePath}】的上传。");
    reply.Block = 0;//传送本次文件发送结束的标记
    reply.Content = Google.Protobuf.ByteString.Empty;
    await stream.RequestStream.WriteAsync(reply);//发送结束标记
    //等待服务器回传
    await stream.ResponseStream.MoveNext(cancellationToken);
    if(stream.ResponseStream.Current!=null&&stream.ResponseStream.Current.Mark==mark)
    {
    lstSuccFiles.Add(filePath);//记录成功的文件
    }
    break;//发送下一个文件
   }
   }
   fs?.Close();
  }
  if (!cancellationToken.IsCancellationRequested)
  {
   result.IsSuccessful = true;
   result.Message = $"完成文件上传。共计【{lstSuccFiles.Count}/{filesPath.Count}】,耗时:{DateTime.Now - startTime}";

   await stream.RequestStream.WriteAsync(new FileReply
   {
   Block = -2,//传输结束
   Mark = mark
   }) ;//发送结束标记
  }
  }
 }
 catch(Exception ex)
 {
  if (cancellationToken.IsCancellationRequested)
  {
  fs?.Close();//释放文件流
  result.IsSuccessful = false;
  result.Message = $"用户取消了上传文件。已完成【{lstSuccFiles.Count}/{filesPath.Count}】,耗时:{DateTime.Now - startTime}";
  }
  else
  {
  result.Message = $"文件上传发生异常({ex.GetType()}):{ex.Message}";
  }
 }
 finally
 {
  fs?.Dispose();
 }
 Console.WriteLine(result.Message);
 result.Tag = lstSuccFiles;
 //关闭通信、并返回结果
 return await channel?.ShutdownAsync().ContinueWith(t => result);
 }
}

  现在可以在客户端窗体内进行调用了:

private string GetFilePath()
{
 // Create OpenFileDialog
 Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();

 // Set filter for file extension and default file extension
 dlg.Title = "选择文件";
 dlg.Filter = "所有文件(*.*)|*.*";
 dlg.FileName = "选择文件夹.";
 dlg.FilterIndex = 1;
 dlg.ValidateNames = false;
 dlg.CheckFileExists = false;
 dlg.CheckPathExists = true;
 dlg.Multiselect = false;//允许同时选择多个文件 

 // Display OpenFileDialog by calling ShowDialog method
 Nullable<bool> result = dlg.ShowDialog();

 // Get the selected file name and display in a TextBox
 if (result == true)
 {
 // Open document
 return dlg.FileName;
 }

 return string.Empty;
}
// 打开文件
private void btnOpenUpload_Click(object sender, RoutedEventArgs e)
{
 lblUploadPath.Content = GetFilePath();
}
CancellationTokenSource uploadTokenSource;
//上传
private async void btnUpload_Click(object sender, RoutedEventArgs e)
{
 lblMessage.Content = string.Empty;

 uploadTokenSource = new CancellationTokenSource();
 List<string> fileNames = new List<string>();
 fileNames.Add(lblUploadPath.Content.ToString());
 var result = await ServerNet.FileTransfer.FileUpload(fileNames, "123", uploadTokenSource.Token);

 lblMessage.Content = result.Message;

 uploadTokenSource = null;
}
//取消上传
private void btnCancelUpload_Click(object sender, RoutedEventArgs e)
{
 uploadTokenSource?.Cancel();
}

//打开需要下载的文件
private void btnOpenDownload_Click(object sender, RoutedEventArgs e)
{
 txtDownloadPath.Text = GetFilePath();
}
//下载文件
private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
 lblMessage.Content = string.Empty;

 downloadTokenSource = new CancellationTokenSource();
 List<string> fileNames = new List<string>();
 fileNames.Add(System.IO.Path.GetFileName(txtDownloadPath.Text));
 var result= await ServerNet.FileTransfer.FileDownload(fileNames, "123", Environment.CurrentDirectory, downloadTokenSource.Token);

 lblMessage.Content = result.Message;

 downloadTokenSource = null;
}
CancellationTokenSource downloadTokenSource;
//下载取消
private void btnCancelDownload_Click(object sender, RoutedEventArgs e)
{
 downloadTokenSource?.Cancel();
}

6、源代码

  https://files.cnblogs.com/files/pilgrim/GrpcTest.rar

总结

到此这篇关于C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能的文章就介绍到这了,更多相关c#文件传输内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C# protobuf自动更新cs文件

    网上的教程大都是手动通过protoc编译, 比较难用 给当前工程添加"Google.Protobuf"和"Grpc.Tools"的引用(通过nuget), 然后添加proto文件, 编辑.csproj文件 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework>

  • C#使用Protocol Buffer(ProtoBuf)进行Unity中的Socket通信

    首先来说一下本文中例子所要实现的功能: 基于ProtoBuf序列化对象 使用Socket实现时时通信 数据包的编码和解码 下面来看具体的步骤: 一.Unity中使用ProtoBuf 导入DLL到Unity中, 创建网络传输的模型类: using System; using ProtoBuf; //添加特性,表示可以被ProtoBuf工具序列化 [ProtoContract] public class NetModel { //添加特性,表示该字段可以被序列化,1可以理解为下标 [ProtoMem

  • 使用C#实现RTP数据包传输 参照RFC3550

    闲暇时折腾IP网络视频监控系统,需要支持视频帧数据包在网络内的传输.未采用H.264或MPEG4等编码压缩方式,直接使用Bitmap图片.由于对帧的准确到达要求不好,所以采用UDP传输.如果发生网络丢包现象则直接将帧丢弃.为了记录数据包的传输顺序和帧的时间戳,所以研究了下RFC3550协议,采用RTP包封装视频帧.并未全面深究,所以未使用SSRC和CSRC,因为不确切了解其用意.不过目前的实现情况已经足够了. 复制代码 代码如下: /// <summary>   /// RTP(RFC3550

  • 详解C# Protobuf如何做到0分配内存的序列化

    题目很简单, 就是IMessage对象怎么变成Byte[] 答案1: msg.ToByteArray() 这肯定不符合我们的要求 答案2: using var memoryStream = new MemoryStream(); using var codedOutputStream = new CodedOutputStream(memoryStream); msg.WriteTo(codedOutputStream); codedOutputStream.Flush(); memoryStr

  • C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能

    初识gRPC还是一位做JAVA的同事在项目中用到了它,为了C#的客户端程序和java的服务器程序进行通信和数据交换,当时还是对方编译成C#,我直接调用. 后来,自己下来做了C#版本gRPC编写,搜了很多资料,但许多都是从入门开始?调用说"Say Hi!"这种官方标准的入门示例,然后遇到各种问题-- 关于gRPC和Protobuf介绍,就不介绍了,网络上一搜一大把,随便一抓都是标准的官方,所以直接从使用说起. gPRC源代码:https://github.com/grpc/grpc: p

  • 使用Protocol Buffers的C语言拓展提速Python程序的示例

    Protocol Buffers (类似XML的一种数据描述语言)最新版本2.3里,protoc-py_out命令只生成原生的Python代码. 尽管PB(Protocol Buffers)可以为C++语言生成快速解析和序列化代码,但是这种方式对于Python不适用,并且手动生成的已包装的代码需要非常大的维护工作.在讨论组里,这是一个常见的功能要求,由于一个必备的客户端组件-AppEngine(根据团队介绍名称为AppEngine),生成原生的Python代码有更高的优先级. 幸运的是, PB

  • Golang语言实现gRPC的具体使用

    目录 gRPC 的特点 使用 gRPC 定义服务端 使用 gRPC 的客户端 gRPC 是通信协议基于 HTTP/2,支持多语言的 RPC 框架,使用 Protobuf 作为它的接口设计语言(IDL),可以通过 protoc 工具生成 Golang 语言的结构体. RPC:Remote Procedure Call 的缩写,译为远程过程调用(也可译为远程方法调用或远程调用),它是计算机通信协议.该协议可以实现调用远程服务就像调用本地服务一样简单,无需关心跨网络,跨平台,跨语言等问题. gRPC

  • C语言实现Linux下的socket文件传输实例

    本文实例讲述了C语言实现Linux下的socket文件传输.分享给大家供大家参考.具体如下: server.c如下: //////////////////////////////////// //服务器代码 /////////////////////////////////// //本文件是服务器的代码 #include <netinet/in.h> // for sockaddr_in #include <sys/types.h> // for socket #include &

  • C语言使用libZPlay录制声音并写到文件的方法

    本文实例讲述了C语言使用libZPlay录制声音并写到文件的方法.分享给大家供大家参考.具体实现方法如下: /** * Record samples from line-in and save to out.mp3 * */ #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <olectl.h> #include <ole2.h> #include <stdio.h> #include <

  • Google Kaptcha 框架实现登录验证码功能(SSM 和 SpringBoot)

    一.效果图: 二.导入 jar 包 1.由于这是大神写好封装起来的一个框架,所有我们使用前得先下载相关的 jar 包 第一种:maven <!-- 验证码 --> <!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha --> <dependency>     <groupId>com.github.penggle</groupId>     <artifactI

  • C语言 不使用strcat函数实现连接两个字符串功能代码

    字符串连接函数:  字符串连接就是将一个字符串连接到另一个字符串的末尾,使其组合成一个新的字符串,在字符串处理函数中,strcat  函数具有字符串连接功能.下面是用C语言实现不使用是strcat 函数实现连接两个字符串的功能. 源代码: #include<stdio.h> void constring(char s[],char t[],char q[]); //函数声明 int main(void) { char s[100]; char q[100]; char t[200]; prin

  • go语言实现简易比特币系统之交易签名及校验功能

    介绍 签名的输入: 待签名的交易数据,包括输入和输出 引用的UTXO信息 私钥 签名的输出: 数字数字签名 公钥 签名的目的 证明交易所引用的UTXO的确属于付款人 证明交易的所有数据的确是付款人提供的,且未被修改过 签名中需要的数据 UTXO中的PubKeyHash,这描述了付款人 新生成UTXO中的PubKeyHash,这描述了收款人 由于每一笔交易都可能引用多个UTXO,因为多个UTXO可能存在于多条交易中.所以我们需要遍历所以的引用交易,并对他们逐个签名 签名过程 用解锁脚本解锁对应的U

  • C 语言编写一个计算器界面(可视化界面和多功能)

    引言 在大学期间,C 语言或者数据结构等课程,老师会要求学生编写一个小项目练手,或者期末编写一个小软件等课程设计.今天在电脑看到大一学C语言时,实验课上写的计算器功能,挺有意思.所以在此分享给学C语言的学弟学妹,记得收藏保留! 计算器 简单版本计算器,主要实现的是简单两个数的加法,减法,乘法,除法,求余功能.用户可以在主菜单选择需要计算的功能,然后根据用户输入的数字,进行计算,输出结果. 首先,我们定义五个方法,实现两个数的加法,减法,乘法,除法,求余功能.代码如下: // 加法 float a

  • C语言rewind与fseek函数之随机读写文件的用法详解

    前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据.但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写.这种读写方式称为随机读写,也就是说从文件的任意位置开始读写. 实现随机读写的关键是要按要求移动位置指针,这称为文件的定位. 文件定位函数rewind和fseek 移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek(). rewind() 用来将位置指针移动到文件开头,前面已经多次使用过,它的原型为

随机推荐