舊 WCF 項目遷移到 asp.net core + gRPC 的嘗試

一個月前,公司的運行WCF的windows服務器down掉了,由於 AWS 沒有通知,沒有能第一時間發現問題。
所以,客戶提出將WCF服務由C#改為JAVA,在Linux上面運行;一方面,AWS對Linux有較多的監控措施,另一方面,假如出現問題,可以設置自動重啟等服務。

老舊的WCF服務

目前WCF服務,主要提供windows桌面軟件的數據接口,應該有五六年的歷史了。我進入公司後,WCF服務的代碼,一直由我一個人來維護。存在很多歷史遺留問題,也有不同版本的共存。

如果java重寫的話,其中的業務邏輯代碼,難免會出現各種各樣的bug,增加開發和測試的工作量。聽說,要移植到linux服務上後,第一時間想到的就是跨平台.net core
.net core 經過了四年的發展,到目前的 3.1 LST版本,已經是非常成熟的跨平台解決方案了。

之後,我就在網上查找,有沒有WCF的.net core 版本,查詢到的信息總結如下:

  1. Core WCF不打算做WCF到.NET Core的100%兼容的移植;
  2. 對於新應用程序,WCF這種SOAP技術不建議使用;
  3. 對於老的應用程序,建議將這些保留在.NET Framework上;
  4. 如果您真的想將一個舊的應用程序遷移到.NET Core並且想繼續使用WCF和WF, 社區的開源項目也是可以的,但是上生產的時間表就要到了2020年.NET 5;
  5. 開源社區,也強烈建議目前不要用於生產環境。

很遺憾,想不改動代碼就遷移到 Linux 上面,基本是不可能的了。
我的最理想情況,盡量少的手寫代碼,最好可以像WCF一樣,自動生成代理類,像訪問本地代碼一樣,來調用接口。之後,就發現了asp.net core + gRPC這種形式。

了解gRPC

gRPC 的好處非常多:高性能傳輸數據小,支持多語言生成工具使用HTTP2協議,這些好處網上都有大量詳細的介紹,本文不做贅述。
其實我最看重的部分還是:客戶端和服務端代碼,都可以通過一個 proto 協議文件來自動生成

而微軟官方,也建議用 ASP.NET Core gRPC。 《適用於 WCF 開發人員的 ASP.NET Core gRPC》

gRPC 的 proto 文件

為了了解 proto 文件的寫法,硬着頭皮看谷歌英文文檔, proto3 勉強了解大概。《Language Guide (proto3)》,下面列出一些,我在使用過程中的經驗總結:

  1. 一個RPC服務必須有且僅有一個入參一個出參;假如不需要的話,可以設置為空的對象google.protobuf.Empty
  2. 基本類型( string, int32 等)不能作為PRC服務的參數,可使用谷歌提供的封裝對象,如:google.protobuf.StringValuegoogle.protobuf.Int32Value 詳見 google/protobuf/wrappers.proto文件;
  3. proto3 不允許null值,這是由於 Protobuf 二進制序列化,空和null不能區分,利用google.protobuf.StringValue 則可以實現null值;同第2點;
  4. string name=1;這個數字必須寫,用作 Protobuf 二進制序列化,並且常用的屬性最好放在前12;PS: 太不習慣了,總以為是在賦值操作;
  5. 枚舉類型必須從0開始,即:enum Weekday {Sunday=0;Monday=2;}
  6. 時間類型google.protobuf.Timestamp,必須是 UTC 時間;
  7. 消息體 message 不能繼承,可多層嵌套,可以導入 import;
// 我的例子
syntax = "proto3";

option csharp_namespace = "GrpcServiceTest.Protos";

import "Protos/ClientInfoModel.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

package UserManagement;
service UserManagement {
    rpc UserReset(google.protobuf.Empty) returns (google.protobuf.Empty);
    rpc UserLogin(LoginRequestV2) returns(LoginResponseV2);
}

message LoginRequestV2 {
    string UserName = 1;
    string Password = 2;
}

message LoginResponseV2 {
    int32 TAG = 1;
    string Message = 2;
    UserModelV2 UserInfo = 3;

    message UserModelV2 {
        int64 UserID = 1;
        string UserName = 2;
        google.protobuf.StringValue Address = 3;
        google.protobuf.Timestamp LastLoginTime = 4;
        repeated PrivGroupPluginModelV2 PrivGroupPlugins = 5;
        bool IsDeleted = 6;

        message PrivGroupPluginModelV2{
        int64 Id=1;
        google.protobuf.Timestamp CreateDateTime=2;
        google.protobuf.Timestamp ModifyDateTime=3;
        int64 PluginId=4;
        int64 PrivGroupPluginID=5;
        }
    }
}

根據 proto 生成代碼

用vs2019,選擇gRPC Service項目模板,創建項目。它會自動加上nuget包Grpc.AspNetCore。如果沒有的話,則需要自己安裝nuget包:Grpc.coreGoogle.ProtobufGrpc.Tools
由 proto 文件生成代碼有兩種方式:

  1. 通過vs右鍵 proto文件,選擇 屬性Property,選擇Build Action中的Protobuf complier,會看到 gRPC Stub Classes,有三個選項 Server Only , Clent Only 和 Both 按需選擇;
    VS生成
  2. 編輯項目文件 csproj,編輯 Protobuf 屬性,這種方法還可以使用路徑宏通配符等,相當方便,強烈推薦
<ItemGroup>
    <Protobuf Include="Protos/*.proto" OutputDir="%(ProjectDir)ServerGrpc" GrpcServices="Server" />
</ItemGroup>

vs生成展示

asp.net core 3.1

現在,恰好趕上了net core 3.1的這個 LST版本 ( long-term-support )的發佈,而 NET Core 3.0 生命周期終結於 2020年3月3日,下個大一統版本 NET 5 ,正式版本還要等到明年。至於為什麼沒有 NET 4.0版本,官方解釋,為了避免於 .NET Framework 4.X 產生歧義。

一步步的按照官方文檔的指引,跟着做就可以了。《使用 ASP.NET Core 的 gRPC 服務》《教程:在 ASP.NET Core 中創建 gRPC 客戶端和服務器》

仔細回想了一下,這部分確實沒有什麼值得說的,官方文檔已經非常的詳細了。唯一不同的感受就是,net core 需要什麼功能的話,需要通過nuget來安裝;這點與 net framework 大有不同,framework 更像是,一次幫你全部裝好。

Entity Framework Core

舊的WCF項目,數據庫訪問使用的是 Entity Framework + Linq + MySql。需要安裝的 Nuget 包:

  • MySql.Data.EntityFrameworkCore Mysql的EF核心庫;
  • Microsoft.EntityFrameworkCore.Proxies 《Lazy loading》 懶加載的插件;
  • Microsoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.Tools 這兩個插件,用於生成代碼;

另外,還需要下載安裝 mysql-connector-net-8.0.21.msi 來訪問數據庫。其中有一個 Scaffold-DbContextbug 99419 TINYINT(1) 轉化為 byte,而不是預期的 bool。這個問題將會在 8.0.22 版本中修復,目前只能手動修改。
EF當然是 Database First 了,生成EF代碼需要在Package Manager Console用到 Scaffold-DbContext 命令,有三點需要注意:

  • Start up 啟始項目一定要是引用它的項目,並且編譯成功的;
  • Default project 生成後,代碼存放的項目;
  • 如果生成失敗,提示:「Your startup project ‘XXXX’ doesn’t reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.」。編輯項目文件 csproj 移除 <PrivateAssets>All</PrivateAssets> 從 “Microsoft.EntityFrameworkCore.Design”和”Microsoft.EntityFrameworkCore.Tools”中;

EF remove PrivateAssets

我的命令: Scaffold-DbContext -Connection "server=10.50.40.50;port=3306;user=myuser;password=123456;database=dbname" -Provider MySql.Data.EntityFrameworkCore -OutputDir "EFModel" -ContextDir "Context" -Project "DataAccess" -Context "BaseEntities" -UseDatabaseNames -Force

其他建議:

  • Library類庫最好是 netstandard 方便移植;
  • 新建一個類來繼承BaseEntities,覆蓋 OnConfiguring 方法,可配置的數據庫連接字符串;
public class Entities : BaseEntities
{
    private static string _lstDBString;

    public static void SetDefaultDBString(string _dbString)
    {
        if (string.IsNullOrEmpty(_lstDBString))
        {
            _lstDBString = _dbString;
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseLazyLoadingProxies().UseMySQL(_lstDBString);
        }
    }
}
  • 最好採用 asp.net core 的框架注入;鑒於項目的原因,假如強行採用的話,改動比較大,只好放棄;
public void ConfigureServices(IServiceCollection services)
{
    string _dbString = Configuration.GetConnectionString("LstDatabase");
    services.AddDbContext<DataAccess.Context.Entities>(
        options => options.UseLazyLoadingProxies().UseMySQL(_dbString));
    services.AddGrpc();
}
  • 數據庫鏈接字符串有多種存放的方式,有更加安全的方式;而我採用簡單方式存放在 appsettings.json
{
    "ConnectionStrings": {
        "LstDatabase": "server=127.0.0.1;port=3306;user=myuser;password=123456;database=dbname"
    },
    "log4net": "log4net.config",
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*"
}

部署到 Ubuntu

生產環境運行的服務器是 Ubuntu 14.04.6 LTS,在《ubuntu Releases wiki》上描述,14版本在去年已經停止了標準支持,而 .net core 的 runtime 最低支持也是 Ubuntu 16.04.6 LTS,只好選擇最新的版本Ubuntu 20.04.1 LTS

安裝Ubuntu Server系統小插曲:IT支持部門的同事,幫忙重裝了兩遍系統,一次14.04桌面版,一次20.04服務器版;安裝20版本後,發現網卡沒有啟用,主機後面網線的燈都沒有亮起來。
由於我和他都不熟悉Ubuntu系統,網上查找辦法,然後用手機拍照,再來服務器上嘗試,搞了好一會兒,才連上網絡,SSH也居然沒有啟用😥。可能 Ubuntu 還是比較適合做桌面系統吧。

然後參考 《在 Ubuntu 上安裝 .NET Core SDK 或 .NET Core 運行時》,安裝 net core的環境,最初用的是 aspnetcore-runtime ,在測試的時候發現,gRPC需要 HTTPS。折騰了半天的 HTTPS,一會兒需要簽名,一會兒還要生成密鑰,一會兒還要放到指定的位置,可信任的證書還要去還要折騰😓😵。折騰了半天,腦殼一團漿糊。只好又安裝了 dotnet-sdk,這個是自帶開發的證書,反正是將就用把。

剩下的就比較簡單了,編譯發佈asp.net core,打包上傳到服務器,然後運行dotnet GrpcServiceLST.dll --urls "//*:5000;//*:5001"。打開瀏覽器測試訪問,沒毛病。

客戶端的編寫

在編寫windows客戶端的時候,遇到個問題:《.NET Core 中的 gRPC 客戶端工廠集成》推薦的插件 Grpc.Net.ClientFactory 只能適用於 net core,而大部分客戶的 windows7 系統不會安裝 net core;如果想在 net framework 上使用 gRPC的話,只能用原生的方法來自己實現

使用 proto 文件生成代碼的方法,與上面的一致,只需要把 Server Only 改為 Client Only ;代碼部分要注意,部署的 HTTPS 是不受信任的,需要額外處理一下。

/// net core 3.1
private void button2_Click(object sender, EventArgs e)
{
    // 取消不受信任
    var httpHandler = new HttpClientHandler();
    httpHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
    var channel = GrpcChannel.ForAddress("//10.50.40.237:5001", new GrpcChannelOptions { HttpHandler = httpHandler });
    var client = new UserManagement.UserManagementClient(channel);
    var _param = new GrpcServiceLST.Protos.LoginRequestV2()
    {
        UserName = "user",
        Password = "123456"
    };
    var reply = client.UserLoginOSDShadowEx(_param);
    MessageBox.Show("net core login: " + reply.Message);
}

/// framework 4.0
private void button1_Click(object sender, EventArgs e)
{
    var channel = new Channel("10.50.40.237:5000", ChannelCredentials.Insecure);
    var client = new UserManagement.UserManagementClient(channel);
    var _param = new GrpcServiceLST.Protos.LoginRequestV2()
    {
        UserName = "user",
        Password = "123456"
    };
    var _reply = client.UserLoginOSDShadowEx(_param);
    MessageBox.Show("framework login:" + _reply.Message);
}

經過測試發現,net core 不支持 http 的訪問; net framework 的原生版本,只能訪問 http 端口 5000 ,不能訪問 https 端口 5001 ,不能用 http 或者 https 這樣的前綴(如: //10.50.40.237:5000),localhost這種域名也無法解析

HTTP HTTPS 域名 IP
net core x
framework x x

最最要命的是,在 win7 系統上,安裝了 net core ,使用 Grpc.Net.ClientFactory 居然也不可以訪問。在github上面找到了答案, win7 不會支持 http2 ,並且 win7 微軟已經在2020 年1 月14 日停止提供支持。

issues : ASP.NET Core uses the operating system for HTTP/2 TLS support. macOS may support hosting servers with HTTP/2 TLS in the future, Windows 7 will not.

總結

這次WCF升級到 asp.net core + gRPC,遷移到 Linux 的部分,方案雖然可以運行。但是要放棄 win7 用戶是不太可能的,只好放棄 gRPC這種方案。

幸運的是,放棄 gPRC 的那一刻,我突然意識到,為什麼不用 web api ,REST Full 的方式也滿足,邏輯部分的代碼盡量不變。下一篇介紹,WCF 遷移到 asp.net core web api ,到目前為止,這個方案是我最為滿意的。