markdown
這篇是記錄在 Windows 10 環境中的 ASP.Net Core 的 WebAPI 使用 protobuf 取代 json 的資料傳輸
[本文範例程式碼](https://github.com/WenWei/ProtobufWebSample)
系統中需已預安裝:
- 安裝 .NET Core 2.2
- 安裝 protoc.exe
- 安裝 Browserify
## protoc.exe 安裝
到 GitHub 的 [Protocol Buffers Releases](https://github.com/protocolbuffers/protobuf/releases) [下載 protoc-3.8.0-win64.zip](https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-win64.zip)
我是解壓到 C:\bin,這裡的 c:\bin 是已加到環境變數 PATH 中。
```
C:\bin
├───include
│ └───google
│ └───protobuf
└───protoc.exe
```
## Browserify 安裝
```
npm install browserify -g
```
## 專案檔案結構
此文建立的範例專案名稱: ProtobufWebSample 省略部份檔案和目錄後結構如下:
```
~/ProtobufWebSample
├───ProtobufWeb
│ ├───Controllers
│ │ └───EchoController.cs
│ └───ProtobufWeb.csproj
└───protos
├───EchoData.proto
└───gen.bat
```
## 新增 asp.net core 的 WebAPI 專案
mkdir ProtobufWebSample
cd ProtobufWebSample
mkdir ProtobufWeb
cd ProtobufWeb
dotnet new webapi
## 安裝 Google.Protobuf 套件
在 `ProtobufWebSample/ProtobufWeb` 資料夾安裝 `Google.Protobuf` 套件
dotnet add package Google.Protobuf
## 建立 EchoData.proto
```
syntax = "proto3";
option csharp_namespace = "ProtobufWeb.ProtoGen";
message EchoData {
string text = 1;
int32 age = 2;
}
```
## 使用 protoc.exe 將 EchoData.proto 產生 C# 類別和 JavaScript 程式碼
### 產生 C# 程式碼
protoc.exe --proto_path=路徑\ProtobufWebSample\protos --js_out=路徑\ProtobufWebSample\protos\gen\csharp 路徑\ProtobufWebSample\protos\EchoData.proto
### 產生 JavaScript 程式碼
protoc.exe --proto_path=路徑\ProtobufWebSample\protos --csharp_out=路徑\ProtobufWebSample\protos\gen\js路徑\ProtobufWebSample\protos\EchoData.proto
因為要在瀏覽器中使用,所以要透過 browserify 將 google-protobuf.js 和產生出來的 EchoData_pb.js 綁在一起輸出為 bundle.js,瀏覽器使用時只要引用此 `bundle.js`即可。
`ProtobufWebSample/protos/gen.bat`
```
@echo on
SET PWD=%~dp0
set PROTOC=C:\bin\protoc.exe
set CSHARP_OUT=%PWD%gen\csharp
set JS_OUT=%PWD%gen\js
rd /S /Q %CSHARP_OUT%
md %CSHARP_OUT%
rd /S /Q %JS_OUT%
md %JS_OUT%
%PROTOC% --proto_path=C:/bin/include/google/protobuf --proto_path=%PWD%^
--csharp_out=%CSHARP_OUT%^
%PWD%EchoData.proto
%PROTOC% --proto_path=C:/bin/include/google/protobuf --proto_path=%PWD%^
--js_out=import_style=commonjs,binary:%JS_OUT%^
%PWD%EchoData.proto
echo var echodataProto = require('./EchoData_pb'); > %JS_OUT%\exports.js
echo module.exports = { >> %JS_OUT%\exports.js
echo EchoDataProto: echodataProto >> %JS_OUT%\exports.js
echo } >> %JS_OUT%\exports.js
cd %JS_OUT%
call npm install google-protobuf
browserify exports.js > bundle.js
cd %PWD%
```
## 建立使用 EchoData 傳遞的 API EchoController
將 `ProtobufWebSample/protos/gen/csharp/EchoData.cs` 移動到Web專案中
```
mkdir ProtobufWebSample/ProtobufWeb/ProtoGen
copy ProtobufWebSample/protos/gen/csharp/EchoData.cs ProtobufWebSample/ProtobufWeb/ProtoGen/EchoData.cs
```
新增 `ProtobufWebSample/ProtobufWeb/Controllers/EchoController.cs`,內容如下:
```
using Microsoft.AspNetCore.Mvc;
using ProtobufWeb.ProtoGen;
namespace ProtobufWeb.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EchoController : ControllerBase
{
// POST api/Echo
[HttpPost]
public ActionResult
Post([FromBody] EchoData value)
{
value.Text = $"ECHO: {value.Text}";
return value;
}
}
}
```
## 要能接收和傳送 protobuf 格式,要建立 formatter
`ProtobufWeb/Formatters/ProtobufFormatter.cs`
```
namespace ProtobufWeb.Formatters
{
using Google.Protobuf;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using System.Threading.Tasks;
using System.Collections.Generic;
using System;
public static class ServicesConfiguration
{
public static void AddProtobufFormatter(this IServiceCollection services)
{
services.Configure(options =>
{
options.InputFormatters.Add(new ProtobufInputFormatter(new ProtobufFormatterOptions()));
options.OutputFormatters.Add(new ProtobufOutputFormatter(new ProtobufFormatterOptions()));
options.FormatterMappings.SetMediaTypeMappingForFormat("protobuf", Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/x-protobuf"));
});
}
}
public class ProtobufFormatterOptions
{
public HashSet SupportedContentTypes { get; set; } = new HashSet { "application/x-protobuf", "application/protobuf", "application/x-google-protobuf" };
public HashSet SupportedExtensions { get; set; } = new HashSet { "proto" };
public bool SuppressReadBuffering { get; set; } = false;
}
public class ProtobufInputFormatter : InputFormatter
{
private readonly ProtobufFormatterOptions _options;
public ProtobufInputFormatter(ProtobufFormatterOptions protobufFormatterOptions)
{
_options = protobufFormatterOptions ?? throw new ArgumentNullException(nameof(protobufFormatterOptions));
foreach (var contentType in protobufFormatterOptions.SupportedContentTypes)
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(contentType));
}
}
public override Task ReadRequestBodyAsync(InputFormatterContext context)
{
try
{
var request = context.HttpContext.Request;
var obj = (IMessage)Activator.CreateInstance(context.ModelType);
obj.MergeFrom(request.Body);
return InputFormatterResult.SuccessAsync(obj);
}
catch (Exception ex)
{
Console.WriteLine("Exception: " + ex);
return InputFormatterResult.FailureAsync();
}
}
}
public class ProtobufOutputFormatter : OutputFormatter
{
private readonly ProtobufFormatterOptions _options;
public string ContentType { get; private set; }
public ProtobufOutputFormatter(ProtobufFormatterOptions protobufFormatterOptions)
{
ContentType = "application/x-protobuf";
_options = protobufFormatterOptions ?? throw new ArgumentNullException(nameof(protobufFormatterOptions));
foreach (var contentType in protobufFormatterOptions.SupportedContentTypes)
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue(contentType));
}
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var response = context.HttpContext.Response;
// Proto-encode
var protoObj = context.Object as IMessage;
var serialized = protoObj.ToByteArray();
return response.Body.WriteAsync(serialized, 0, serialized.Length);
}
}
}
```
到 startup.cs 加入 `services.AddProtobufFormatter();`,startup.cs 完整內容如下:
```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ProtobufWeb.Formatters;
namespace ProtobufWeb
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddProtobufFormatter();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
```
## 建立測試頁 `ProtobufWebSample/ProtobufWeb/wwwroot/index.html`
```
mkdir wwwroot
mkdir wwwroot/scripts
```
將 bundle.js 移到 wwwroot/scripts 資料夾
建立 index.html 內容如下
```
Document
```
## 執行網站,啟動 開發者工具查看結果
啟動網站
```
dotnet run
```
瀏覽器開啟 http://localhost:5001/index.html
使用開發者工具查看 Network 欄位的傳送結果
## 參考資料
* [Protocol Buffers - C# Generated Code](https://developers.google.com/protocol-buffers/docs/reference/csharp-generated)
* [Protocol Buffers - JavaScript Generated Code](https://developers.google.com/protocol-buffers/docs/reference/javascript-generated)
* [protobuf 前端怎么使用 - 蝈蝈250 - 2017/11/21](https://blog.csdn.net/vteihlll/article/details/78592976)
沒有留言:
張貼留言