在本文中,我们将带你了解从反射看protobuf的部分实现在这篇文章中,我们将为您详细介绍从反射看protobuf的部分实现的方方面面,并解答protobuf反射常见的疑惑,同时我们还将给您一些技巧,
在本文中,我们将带你了解从反射看protobuf的部分实现在这篇文章中,我们将为您详细介绍从反射看protobuf的部分实现的方方面面,并解答protobuf 反射常见的疑惑,同时我们还将给您一些技巧,以帮助您实现更有效的C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能、C++ProtoBuf的安装与使用、cocos creator: js中实现protobuf的打包和解析、com.google.protobuf.Internal.ProtobufList的实例源码。
本文目录一览:- 从反射看protobuf的部分实现(protobuf 反射)
- C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能
- C++ProtoBuf的安装与使用
- cocos creator: js中实现protobuf的打包和解析
- com.google.protobuf.Internal.ProtobufList的实例源码
从反射看protobuf的部分实现(protobuf 反射)
一、一个message的meta中包含的内容
所谓反射(reflection),最直观的理解就是可以在运行中通过一个字符串的名称获得一个内存地址。在protobuf中,这一点通过Reflection对象完成,尽管这个类的接口
virtual int32 GetInt32 (const Message& message,
const FieldDescriptor* field) const = 0;
并不是通过字符串名称来获得,但是由于可以通过field的字符串名称找到一个field的描述符,所以这个这里说可以通过一个字符串名称获得一个内存地址也没有毛病。
对于我们使用的消息,这个具体的接口实现在protobuf-master\src\google\protobuf\generated_message_reflection.cc文件中实现,从这个文件的具体实现来看,它是通过protobuf-master\src\google\protobuf\generated_message_reflection.h一个便宜表来实现
// Offset of any field.
uint32 GetFieldOffset(const FieldDescriptor* field) const {
if (field->containing_oneof()) {
size_t offset =
static_cast<size_t>(field->containing_type()->field_count() +
field->containing_oneof()->index());
return OffsetValue(offsets_[offset], field->type());
} else {
return GetFieldOffsetNonOneof(field);
}
}
这个偏移表的来源也就是在使用protobuf生成的pb文件中包含的offsets数组
const ::PROTOBUF_NAMESPACE_ID::uint32 TableStruct_demo_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
PROTOBUF_FIELD_OFFSET(::tutorial::Person_PhoneNumber, _has_bits_),
PROTOBUF_FIELD_OFFSET(::tutorial::Person_PhoneNumber, _internal_metadata_),
……
}
而其中的宏protobuf-master\src\google\protobuf\port_def.inc
// Note that we calculate relative to the pointer value 16 here since if we
// just use zero, GCC complains about dereferencing a NULL pointer. We
// choose 16 rather than some other number just in case the compiler would
// be confused by an unaligned pointer.
#define PROTOBUF_FIELD_OFFSET(TYPE, FIELD) \
static_cast< ::google::protobuf::uint32>(reinterpret_cast<const char*>( \
&reinterpret_cast<const TYPE*>(16)->FIELD) - \
reinterpret_cast<const char*>(16))
二、从描述字符串到Proto结构的转换
下面的proto文件描述了一个proto文件中Fielde信息,protoc编译生成的字符串类型的描述符使用的就是这种形式的描述文件
protobuf-master\src\google\protobuf\descriptor.proto
// Describes a field within a message.
message FieldDescriptorProto {
enum Type {
// 0 is reserved for errors.
// Order is weird for historical reasons.
TYPE_DOUBLE = 1;
TYPE_FLOAT = 2;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if
// negative values are likely.
TYPE_INT64 = 3;
TYPE_UINT64 = 4;
// Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if
// negative values are likely.
TYPE_INT32 = 5;
TYPE_FIXED64 = 6;
TYPE_FIXED32 = 7;
TYPE_BOOL = 8;
TYPE_STRING = 9;
// Tag-delimited aggregate.
// Group type is deprecated and not supported in proto3. However, Proto3
// implementations should still be able to parse the group wire format and
// treat group fields as unknown fields.
TYPE_GROUP = 10;
TYPE_MESSAGE = 11; // Length-delimited aggregate.
// New in version 2.
TYPE_BYTES = 12;
TYPE_UINT32 = 13;
TYPE_ENUM = 14;
TYPE_SFIXED32 = 15;
TYPE_SFIXED64 = 16;
TYPE_SINT32 = 17; // Uses ZigZag encoding.
TYPE_SINT64 = 18; // Uses ZigZag encoding.
};
enum Label {
// 0 is reserved for errors
LABEL_OPTIONAL = 1;
LABEL_REQUIRED = 2;
LABEL_REPEATED = 3;
};
optional string name = 1;
optional int32 number = 3;
optional Label label = 4;
// If type_name is set, this need not be set. If both this and type_name
// are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP.
optional Type type = 5;
// For message and enum types, this is the name of the type. If the name
// starts with a ''.'', it is fully-qualified. Otherwise, C++-like scoping
// rules are used to find the type (i.e. first the nested types within this
// message are searched, then within the parent, on up to the root
// namespace).
optional string type_name = 6;
// For extensions, this is the name of the type being extended. It is
// resolved in the same manner as type_name.
optional string extendee = 2;
// For numeric types, contains the original text representation of the value.
// For booleans, "true" or "false".
// For strings, contains the default text contents (not escaped in any way).
// For bytes, contains the C escaped value. All bytes >= 128 are escaped.
// TODO(kenton): Base-64 encode?
optional string default_value = 7;
// If set, gives the index of a oneof in the containing type''s oneof_decl
// list. This field is a member of that oneof.
optional int32 oneof_index = 9;
// JSON name of this field. The value is set by protocol compiler. If the
// user has set a "json_name" option on this field, that option''s value
// will be used. Otherwise, it''s deduced from the field''s name by converting
// it to camelCase.
optional string json_name = 10;
optional FieldOptions options = 8;
}
三、从proto文件到内存数据结构的转换
protobuf-master\src\google\protobuf\descriptor.cc
void BuildField(const FieldDescriptorProto& proto,
const Descriptor* parent,
FieldDescriptor* result) {
BuildFieldOrExtension(proto, parent, result, false);
}
四、当我们使用index()接口时在使用什么
其实是一个内存结构相对于数组基地址的偏移量,也就是一个数组index
protobuf-master\src\google\protobuf\descriptor.h
// To save space, index() is computed by looking at the descriptor''s position
// in the parent''s array of children.
inline int FieldDescriptor::index() const {
if (!is_extension_) {
return static_cast<int>(this - containing_type()->fields_);
} else if (extension_scope_ != NULL) {
return static_cast<int>(this - extension_scope_->extensions_);
} else {
return static_cast<int>(this - file_->extensions_);
}
}
五、一个message的Meta什么时候初始化
由于GetDescriptor和GetReflection都是首先调用GetDescriptor接口,所以通常在派生类接口中按需注册即可:
// Get a non-owning pointer to a Descriptor for this message''s type. This
// describes what fields the message contains, the types of those fields, etc.
// This object remains property of the Message.
const Descriptor* GetDescriptor() const { return GetMetadata().descriptor; }
// Get a non-owning pointer to the Reflection interface for this Message,
// which can be used to read and modify the fields of the Message dynamically
// (in other words, without knowing the message type at compile time). This
// object remains property of the Message.
//
// This method remains virtual in case a subclass does not implement
// reflection and wants to override the default behavior.
virtual const Reflection* GetReflection() const final {
return GetMetadata().reflection;
}
例如下面是google项目自带的电话簿派生类实现的接口
::PROTOBUF_NAMESPACE_ID::Metadata Person::GetMetadata() const {
::PROTOBUF_NAMESPACE_ID::internal::AssignDescriptors(&::assign_descriptors_table_demo_2eproto);
return ::file_level_metadata_demo_2eproto[kIndexInFileMessages];
六、一个简单的demo
从这个例子看,这种反射并没有太大实际用处,以为类型是需要在编译时确定的,也就是不同的数据类型需要使用不同的接口来操作
tsecer@protobuf: cat addressbook.proto
syntax = "proto2";
package tutorial;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
tsecer@protobuf: cat relection.cc
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// file.
int main(int argc, char* argv[]) {
tutorial::Person person;
person.set_id(1111);
const google::protobuf::Reflection *pstRefl = person.GetReflection();
const google::protobuf::Descriptor *pstDesc = person.GetDescriptor();
const google::protobuf::FieldDescriptor *pstField = pstDesc->FindFieldByName("id");
printf("id before is %d\n", person.id());
pstRefl->SetInt32(&person, pstField, 2222);
printf("id after is %d\n", person.id());
return 0;
}
tsecer@protobuf: protoc --cpp_out=. addressbook.proto
tsecer@protobuf: g++ relection.cc addressbook.pb.cc -lprotobuf --std=c++11
./tsecer@protobuf: ./a.out
id before is 1111
id after is 2222
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#文件传输内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
- golang在GRPC中设置client的超时时间
- golang 微服务之gRPC与Protobuf的使用
- golang grpc 负载均衡的方法
- 详解golang consul-grpc 服务注册与发现
- go grpc安装使用教程
- go实现grpc四种数据流模式
C++ProtoBuf的安装与使用
安装(Ubuntu 16.04)
- sudo apt-get install autoconf automake libtool curl make g++ unzip
- git clone https://github.com/google/protobuf.git
- cd protobuf
- git submodule update --init --recursive
- ./autogen.sh
- ./configure
- make
- make check
- sudo make install
- sudo ldconfig # refresh shared library cache
- protoc --version
如果能查看proto的版本,则代表安装成功,否则失败。
简介
本文主要介绍proto3的使用以及个人理解。关于proto版本的不同以及深入理解,可以参考下面链接。
https://www.jianshu.com/p/5e65f33c4b15?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation //数据传输格式
proto2
https://www.jianshu.com/p/e89594ecc1db
proto3
https://blog.csdn.net/u011518120/article/details/54604615
用法
proto3
/*****PbTest.proto******/
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
Person people = 1;
}
protoc --cpp_out=. PbTest.proto //先编译proto文件生成.cc和.h文件
若对proto文件进行修改,则需要重新使用protoc进行编译,生成新的cc和h文件。
#include <iostream>
#include "PbTest.pb.h"
void BookAdd(tutorial::Person *person)
{
person->set_email("fun@qq.com");
person->set_name("fun_name");
person->set_id(1111);
tutorial::Person::PhoneNumber *phone_num=person->add_phones();
phone_num->set_number("1999");
}
int main() {
tutorial::Person test;
test.set_id(2111);
test.set_name("main_name");
test.set_email("main@qq.com");
tutorial::Person::PhoneNumber phone_num;
phone_num.set_number("2119");
phone_num.set_type(tutorial::Person::WORK);
// tutorial::Person_PhoneNumber phone; //等价上面的phone_num
// phone.set_number("2119");
// phone.set_type(tutorial::Person_PhoneType::Person_PhoneType_HOME);
// std::cout<<phone.number()<<std::endl;
// std::cout<<phone.type()<<std::endl;
tutorial::AddressBook book;
BookAdd(book.mutable_people());
// book.mutable_people()->set_name("main2test"); //与BookAdd函数调用等价
std::cout<<"main id :"<<test.id()<<std::endl;
std::cout<<"main name :"<<test.name()<<std::endl;
std::cout<<"main email :"<<test.email()<<std::endl;
std::cout<<"main phone :"<<phone_num.number()<<std::endl;
std::cout<<"main phone type :"<<phone_num.type()<<std::endl;
const tutorial::Person &person=book.people();
std::cout<<"AddBook id :"<<person.id()<<std::endl;
std::cout<<"AddBook name :"<<person.name()<<std::endl;
std::cout<<"AddBook email :"<<person.email()<<std::endl;
const tutorial::Person::PhoneNumber &num_phone=person.phones(0);
std::cout<<"AddBook phone :"<<num_phone.number()<<std::endl;
std::cout<<"AddBook phone type:"<<num_phone.type()<<std::endl;
return 0;
}
g++ main.cc PbTest.pb.cc -lprotobuf -lpthread
输出结果
main id :2111
main name :main_name
main email :main@qq.com
main phone :2119
main phone type :2
AddBook id :1111
AddBook name :fun_name
AddBook email :fun@qq.com
AddBook phone :1999
AddBook phone type:0
总结
关于proto3的使用过程中,与proto2比较起来,3去掉了字段的限制,以及默认值。虽然说3去掉了[default=value]的操作,但是3在枚举类型中,必须要从0开始,并且枚举的默认值也为0。对于bool类型,默认是false。正因为由于3有了默认值的操作,所以在判断是用户赋予的值还是默认值,则需要话费一些功夫去判断。(由于本人不常用,有需要者,可以百度。) 如果需要使用oneof字段时,它的原理有点类似与共享(union)结构体。如果数据结构复杂的话,也可以采用c++中的map来存储key-value结构。value也可以是proto中的message类型。 踩坑:如果字段前面有repeated修饰的话,对其进行修改的时候则需要通过proto对象中的add_()方法对内部嵌套的字段进行赋值。若没有的话,则可以采用 obj.mutable_()->set_()来进行赋值。
若需要参考proto2代码,则可以参考:
https://github.com/protobuf-c/protobuf-c/wiki/Examples https://github.com/SmallBlackEgg/proto/
原文出处:https://www.cnblogs.com/SmallBlackEgg/p/11628932.html
cocos creator: js中实现protobuf的打包和解析
首先在Google搜索中找到些相关资料,这里吐槽下百度,用百度真心搜索不到有用的资料,大部分均是广告,不知道作为一个搜索引擎,情何以堪。综合谷歌搜索到的各项资料,将本菜鸟cocos2dx_js集成方式贴出:
1.需要protobuf.js(http://pan.baidu.com/s/1dFfpXsd),bytebuffer.js(http://pan.baidu.com/s/1hs36tUG),long.js(http://pan.baidu.com/s/1slBv57n)文件,此处给出我使用的版本。
2.将此三个文件复制到脚本文件夹下
3.引用 protobuf.js,bytebuffer,js
var ByteBuffer = require("bytebuffer"); var ProtoBuf = require("protobuf");4.将protobuf原文件(.proto)复制到项目assets/resources目录下,如果没有resources目录,新建一个即可
5.使用protobuf.js解析proto文件,因为我的原文件放在assets/resources/protobuf/guaji.proto,此处不需要加后缀,cocos2d会自动查找 ,ProtoBuf.protoFromString方法会解析文件生成一个protobuf对象工厂。
var protoFile = "protobuf/guaji"; cc.loader.loadRes(protoFile,function (err,bgTexture2D){ cc.log("loadfinish"); Builder = ProtoBuf.protoFromString(bgTexture2D); });6.拿到工厂之后使用工厂对象生成原文件中数据对象,如果源文件中存在package包名,在创建protobuf对象的时候记得加上包名,此处我的包名为(game.service.data),拿到protobuf对象之后记得使用new创建实例,然后使用set方法即可设置数据
var Message = Builder.build("game.service.data.MsgLoginData"); var loginMessage = new Message(); loginMessage.set("UserAccount","webTest"); loginMessage.set("UserPassWord","123456");7.使用BUFF数据解析生成protobuf数据实例messageBuff为ArrayBuffer对象实例,使用get方法,或直接使用取属性符号(.)即可获取数据
var Message = Builder.build("game.service.data.MsgLoginResult"); var msg = Message.decode(messageBuff);
到此js和protobuf集成基本完成
下篇解释跟websocket集成使用
com.google.protobuf.Internal.ProtobufList的实例源码
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { if (!mine.equals(other)) { throw NOT_EQUALS; } return mine; }
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { int size = mine.size(); int otherSize = other.size(); if (size > 0 && otherSize > 0) { if (!mine.isModifiable()) { mine = mine.mutablecopyWithCapacity(size + otherSize); } mine.addAll(other); } return size > 0 ? mine : other; }
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { if (!mine.equals(other)) { throw NOT_EQUALS; } return mine; }
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { int size = mine.size(); int otherSize = other.size(); if (size > 0 && otherSize > 0) { if (!mine.isModifiable()) { mine = mine.mutablecopyWithCapacity(size + otherSize); } mine.addAll(other); } return size > 0 ? mine : other; }
protected static <E> ProtobufList<E> newProtobufList() { return new ProtobufArrayList<E>(); }
protected static <E> ProtobufList<E> newProtobufList(List<E> tocopy) { return new ProtobufArrayList<E>(tocopy); }
protected static <E> ProtobufList<E> emptyProtobufList() { return ProtobufArrayList.emptyList(); }
protected static <E> ProtobufList<E> emptyProtobufList() { return ProtobufArrayList.emptyList(); }
protected static <E> ProtobufList<E> mutablecopy(ProtobufList<E> list) { int size = list.size(); return list.mutablecopyWithCapacity( size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2); }
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { hashCode = (53 * hashCode) + mine.hashCode(); return mine; }
protected static <E> ProtobufList<E> emptyProtobufList() { return ProtobufArrayList.emptyList(); }
protected static <E> ProtobufList<E> mutablecopy(ProtobufList<E> list) { int size = list.size(); return list.mutablecopyWithCapacity( size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2); }
@Override public <T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other) { hashCode = (53 * hashCode) + mine.hashCode(); return mine; }
<T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other);
<T> ProtobufList<T> visitList(ProtobufList<T> mine,ProtobufList<T> other);
关于从反射看protobuf的部分实现和protobuf 反射的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能、C++ProtoBuf的安装与使用、cocos creator: js中实现protobuf的打包和解析、com.google.protobuf.Internal.ProtobufList的实例源码等相关知识的信息别忘了在本站进行查找喔。
本文标签: