一个月前,公司的运行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;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 我的例子
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 属性,这种方法还可以使用路径宏通配符等,相当方便,强烈推荐
1
2
3
<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类库最好是 Net Standard 方便移植;
  • 新建一个类来继承BaseEntities,覆盖 OnConfiguring 方法,可配置的数据库连接字符串;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 的框架注入;鉴于项目的原因,假如强行采用的话,改动比较大,只好放弃;
1
2
3
4
5
6
7
public void ConfigureServices(IServiceCollection services)
{
    string _dbString = Configuration.GetConnectionString("MyDatabase");
    services.AddDbContext<DataAccess.Context.Entities>(
        options => options.UseLazyLoadingProxies().UseMySQL(_dbString));
    services.AddGrpc();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "ConnectionStrings": {
        "MyDatabase": "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 "http://*:5000;http://*:5001"。打开浏览器测试访问,没毛病。

客户端的编写

在编写windows客户端的时候,遇到个问题:《.NET Core 中的 gRPC 客户端工厂集成》推荐的插件 Grpc.Net.ClientFactory 只能适用于 net core,而大部分客户的 windows7 系统不会安装 net core;如果想在 net framework 上使用 gRPC的话,只能用原生的方法来自己实现

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// net core 3.1
private void button2_Click(object sender, EventArgs e)
{
    // 取消不受信任
    var httpHandler = new HttpClientHandler();
    httpHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
    var channel = GrpcChannel.ForAddress("https://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 的gRPC桌面程序 不支持 http 的访问; net framework 的桌面程序使用gRPC原生版本,只能访问 http 端口 5000 ,不能访问 https 端口 5001 ,不能用 http 或者 https 这样的前缀(如: http://10.50.40.237:5000),localhost这种域名也无法解析

HTTP HTTPS 域名 IP
net core gRPC客户端 x
net core gRPC客户端 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 , RESTful 的方式也满足,逻辑部分的代码尽量不变。下一篇介绍,WCF 迁移到 asp.net core web api ,到目前为止,这个方案是我最为满意的。