這篇是記錄在 Windows 10 環境中的 ASP.Net Core 的 WebAPI 使用 protobuf 取代 json 的資料傳輸
本文範例程式碼
系統中需已預安裝:
- 安裝 .NET Core 2.2
- 安裝 protoc.exe
- 安裝 Browserify
protoc.exe 安裝
到 GitHub 的 Protocol Buffers Releases 下載 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<echodata> Post([FromBody] EchoData value)
{
value.Text = $"ECHO: {value.Text}";
return value;
}
}
}
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<mvcoptions>(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<string> SupportedContentTypes { get; set; } = new HashSet<string> { "application/x-protobuf", "application/protobuf", "application/x-google-protobuf" };
public HashSet<string> SupportedExtensions { get; set; } = new HashSet<string> { "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<inputformatterresult> 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 內容如下
<!--DOCTYPE html-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="scripts/bundle.js"></script>
<script>
var obj = new proto.EchoData();
obj.setText('hello world');
obj.setAge(10);
var data = search.serializeBinary();
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/Echo', true);
xhr.withCredentials = false;
xhr.responseType = 'arraybuffer';
xhr.setRequestHeader('Content-Type', 'application/protobuf');
xhr.setRequestHeader('Accept', 'application/protobuf');
xhr.addEventListener('readystatechange', (function (xhr) {
return function handleReady() {
if (xhr.readyState === xhr.DONE) {
xhr.removeEventListener('readystatechange', handleReady, false)
console.log('ready', xhr)
if (xhr.status >= 200 && xhr.status < 300) {
var data = proto.EchoData.deserializeBinary(xhr.response);
console.log('ok',data)
} else {
console.log('catch', xhr)
}
}
}
})(xhr), false);
xhr.send(data);
</script>
<script>
var xhr1 = new XMLHttpRequest();
xhr1.open('POST', '/api/Echo', true);
xhr1.withCredentials = false;
xhr1.responseType = 'json';
xhr1.setRequestHeader('Content-Type', 'application/json');
xhr1.setRequestHeader('Accept', 'application/json');
xhr1.addEventListener('readystatechange', (function (xhr) {
return function handleReady() {
if (xhr.readyState === xhr.DONE) {
xhr.removeEventListener('readystatechange', handleReady, false)
console.log('ready', xhr)
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response)
} else {
console.log('catch', xhr)
}
}
}
})(xhr1), false);
xhr1.send(JSON.stringify({
text: 'hello world 2',
age: 20
}));
</script>
執行網站,啟動 開發者工具查看結果
啟動網站
dotnet run
瀏覽器開啟 http://localhost:5001/index.html
使用開發者工具查看 Network 欄位的傳送結果
參考資料