初识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)实现文件传输的更多相关文章

  1. Google Protocol Buffers简介

    什么是 protocol buffers ? Protocol buffers 是一种灵活.高效的序列化结构数据的自动机制--想想XML,但是它更小,更快,更简单.你只需要把你需要怎样结构化你的数据定 ...

  2. Google Protocol Buffers 入门

    Google Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化.它很适合做数据存储或 RPC 数据交换格式.可用于通讯协议.数据存储等领域的 ...

  3. Google Protocol Buffers介绍

    简要介绍和总结protobuf的一些关键点,从我之前做的ppt里摘录而成,希望能节省protobuf初学者的入门时间.这是一个简单的Demo. Protobuf 简介 Protobuf全称Google ...

  4. Google Protocol Buffers 快速入门(带生成C#源码的方法)

    Google Protocol Buffers是google出品的一个协议生成工具,特点就是跨平台,效率高,速度快,对我们自己的程序定义和使用私有协议很有帮助. Protocol Buffers入门: ...

  5. C# 使用Google Protocol Buffers

    Google Protocol Buffers 使用3.0版本 下载protoc.exe 下载链接 https://github.com/protocolbuffers/protobuf/releas ...

  6. TFTP(Trivial File Transfer Protocol,简单文件传输协议)

    TFTP(Trivial File Transfer Protocol,简单文件传输协议),是 TCP/IP 协议族中用来在客户机和服务器之间进行简单文件传输的协议,开销很小.这时候有人可能会纳闷,既 ...

  7. DELPHI、FLASH、AS3、FLEX使用Protobuf(google Protocol Buffers)的具体方法

    最近因为工作需要,需要在不同的开发环境中应用Protobuf,特此,我专门研究了一下.为了防止自己忘记这些事情,现在记录在这里!需要的朋友可以借鉴一些,因为这些东西在GOOGLE和百度上搜索起来真的很 ...

  8. Google Protocol Buffers 反序列化 转

    http://www.cnblogs.com/royenhome/archive/2010/10/30/1865256.html   本文作为结束篇,会稍微介绍下怎么反序列化GoogleBuffer数 ...

  9. linux网络环境下socket套接字编程(UDP文件传输)

    今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中, ...

随机推荐

  1. mariadb 4

    连接查询,视图,事物,索引,外键(第四章)   连接查询 --创建学生表 create table students ( id int unsigned not null auto_increment ...

  2. Linux常用命令详解(2)

    aliasunaliasunamesuhostnamehistorywhichwcwwhowhoamipingkillseqdudffreedate 命令详解 1. alias 设置.’查看别名 实例 ...

  3. 安装Linux的CentOS操作系统 - 初学者系列 - 学习者系列文章

    Linux系统对于一些熟悉Windows操作系统的用户来说可能比较陌生,但是它也是一种多用户.多任务的操作系统,现在也发展成为了多种版本的操作系统了.如果想对该系统进行学习,请下载这个学习文档:htt ...

  4. python身体指数BMI

    问题需求 既要输出国际标准也要输出国内标准

  5. 种子爆破&[GWCTF 2019]枯燥的抽奖

    伪随机数的爆破,种子爆破 做到了一道题,就是有个伪随机数爆破的漏洞,当时尽管是看到了这两个敏感的函数,但是先去看其他的了,没有看到什么漏洞,所以我当时是准备直接强行爆破,之后看到使用伪随机数爆破的方式 ...

  6. zabbix关键字含义

    Zabbix server :zabbix控制中心,收集数据,写入数据库都是他的工作 Zabbix Agent:部署在被监控服务器上的一个进程,负责和zabbix server 交互,执行命令. Ho ...

  7. burp suite 之 Repeater(中继器)

    Repeater:对HTTP请求的修改,通常用于上传攻击 通过抓包后将请求包通过选项卡发送到 Repeater下 右下角 显示 页面大小为18809字节 可以自定义添加主机 大小写敏感 正则表达式 文 ...

  8. 深度预警:深入理解HBase的系统架构

    HBase的构成 物理上来说,HBase是由三种类型的服务器以主从模式构成的.这三种服务器分别是:Region server,HBase HMaster,ZooKeeper. 其中Region ser ...

  9. P4715 【深基16.例1】淘汰赛

    P4715 [深基16.例1]淘汰赛 题目描述 有 2^n(n≤7) 个国家参加世界杯决赛圈且进入淘汰赛环节.我经知道各个国家的能力值,且都不相等.能力值高的国家和能力值低的国家踢比赛时高者获胜.1 ...

  10. Python-joypy和 R-ggridges 峰峦图制作

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 以下文章来源于DataCharm,作者 宁海涛 转载地址 https://www ...