初识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. 两年银行经验的阿里、头条社招面经分享(已拿offer)

    lz是非科班自学的java,毕业后进入卡中心,现在是2年开发经验.20年年初先后面了头条.拼多多和阿里(淘宝和支付宝),并成功拿到阿里和头条两家的offer.   面试前我主要是在牛客网看大家的面经进 ...

  2. Oracle学习(九)存过、游标、RECORD整合

    一.代码 /*1.定义存过*/ CREATE OR REPLACE PROCEDURE PRO_RE_USER_ORD_GOODS( --NOW_DATE格式:YYYYMMDD 如:20190801( ...

  3. 如何使用 Python 進行字串格式化

    前言: Python有几种方法可以显示程序的输出:数据可以以人类可读的形式打印出来,或者写入文件以供将来使用. 在开发应用程式时我们往往会需要把变数进行字串格式化,也就是说把字串中的变数替换成变量值. ...

  4. 每日爬虫JS小逆之5分钟旅游网MD5一锅端

    来吧骚年,每天花5分钟锻炼一下自己的JS调试也是极好的,对后期调试滑块验证码还原.拖动很有帮助,坚持下去,我们能赢.建议亲自试试哦,如果对大家有帮助的话不妨关注一下知识图谱与大数据公众号,当然不关注也 ...

  5. Django-Scrapy生成后端json接口

    Django-Scrapy生成后端json接口: 网上的关于django-scrapy的介绍比较少,该博客只在本人查资料的过程中学习的,如果不对之处,希望指出改正: 以后的博客可能不会再出关于djan ...

  6. 008 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 02 Java 中的关键字

    008 01 Android 零基础入门 01 Java基础语法 02 Java常量与变量 02 Java 中的关键字 关键字 关键字就是一些有特殊意义的词 之前学习的程序中涉及到的关键字 Java中 ...

  7. 01 How does C Programming work ? C语言如何工作?

    where is C used ? C 语言的应用场景 C is widely used C语言被广泛应用于: For creating desktop applications 用于创建桌面应用程序 ...

  8. 达梦产品技术支持培训-day8-DM8数据库备份与还原-实操

    1.DM8的备份还原方法 Disql 工具:联机数据备份与还原,包括库备份.表空间备份与还原.表备份与还原:  DMRMAN 工具:脱机数据库备份还原与恢复: 客户端工具 MANAGER和CONSOL ...

  9. ASP。使用依赖注入的asp.net Core 2.0用户角色库动态菜单管理

    下载source code - 2.2 MB 介绍 在开始这篇文章之前,请阅读我的前一篇文章: 开始使用ASP.NET Core 2.0身份和角色管理 在上一篇文章中,我们详细讨论了如何使用ASP.N ...

  10. RHSA-2019:0201-低危: systemd 安全更新

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...