使用protobuf-net.Grpc将WCF服务迁移到gRPC非常简单。在这篇博文中,我们将看看它到底有多简单。微软关于将WCF服务迁移到gRPC的官方指南只提到了Gooogle.Protobuf方式,如果你有很多数据契约需要迁移到.proto格式,这可能会很耗时。然而,通过使用protobuf-net.Grpc我们能够重用旧的WCF数据契约和服务契约,而只需要做最小的代码更改。

迁移数据契约和服务契约

在本节中,我们将使用一个简单的请求响应的组合服务,它可以让你下载给定交易者的单个投资组合或所有投资组合。服务和数据契约的定义如下:

[ServiceContract]
public interface IPortfolioService
{
[OperationContract]
Task<Portfolio> Get(Guid traderId, int portfolioId); [OperationContract]
Task<List<Portfolio>> GetAll(Guid traderId);
}
[DataContract]
public class Portfolio
{
[DataMember]
public int Id { get; set; } [DataMember]
public Guid TraderId { get; set; } [DataMember]
public List<PortfolioItem> Items { get; set; }
} [DataContract]
public class PortfolioItem
{
[DataMember]
public int Id { get; set; } [DataMember]
public int ShareId { get; set; } [DataMember]
public int Holding { get; set; } [DataMember]
public decimal Cost { get; set; }
}

在将数据契约和服务契约迁移到gRPC之前,我建议为契约创建一个新的类库。这些契约可以通过项目引用或包引用在服务器和客户端之间很容易地共享,这取决于你的WCF解决方案的结构。一旦我们创建了类库,我们将源文件进行复制并开始迁移到gRPC。

不像使用Google.Protobuf迁移到gRPC那样,数据契约只需要最小的更改。我们需要做的唯一一件事是在DataMember属性中定义Order属性。这相当于在创建.proto格式的消息时定义字段号。这些字段号用于标识消息二进制格式中的字段,并且在使用消息后不应更改。

[DataContract]
public class Portfolio
{
[DataMember(Order = 1)]
public int Id { get; set; } [DataMember(Order = 2)]
public Guid TraderId { get; set; } [DataMember(Order = 3)]
public List<PortfolioItem> Items { get; set; }
} [DataContract]
public class PortfolioItem
{
[DataMember(Order = 1)]
public int Id { get; set; } [DataMember(Order = 2)]
public int ShareId { get; set; } [DataMember(Order = 3)]
public int Holding { get; set; } [DataMember(Order = 4)]
public decimal Cost { get; set; }
}

由于gRPC和WCF之间的差异,服务契约将需要更多的修改。gRPC服务中的RPC方法必须只定义一种消息类型作为请求参数,并且只返回一条消息。我们不能接受标量类型(即基本类型)作为请求参数,也不能返回标量类型。我们需要将所有原始参数合并到一个消息中(即DataContract)。这也解释了Guid参数类型,因为它可能被序列化为字符串,这取决于你如何配置protobuf-net。我们也不能接受消息列表(或标量)或返回消息列表(或标量)。记住这些规则后,我们需要修改我们的服务契约,使其看起来像下面这样:

[ServiceContract]
public interface IPortfolioService
{
[OperationContract]
Task<Portfolio> Get(GetPortfolioRequest request); [OperationContract]
Task<PortfolioCollection> GetAll(GetAllPortfoliosRequest request);
}

服务契约中的上述更改迫使我们创建一些额外的数据契约。因此,我们创建如下:

[DataContract]
public class GetPortfolioRequest
{
[DataMember(Order = 1)]
public Guid TraderId { get; set; } [DataMember(Order = 2)]
public int PortfolioId { get; set; }
} [DataContract]
public class GetAllPortfoliosRequest
{
[DataMember(Order = 1)]
public Guid TraderId { get; set; }
} [DataContract]
public class PortfolioCollection
{
[DataMember(Order = 1)]
public List<Portfolio> Items { get; set; }
}

基本上是这样。现在我们已经将我们的WCF服务契约和数据契约迁移到gRPC。下一步是将数据层迁移到.net Core。

将PortfolioData库迁移到.net Core

接下来,我们将把PortfolioData库迁移到.net Core,就像微软指南中描述的那样。但是,我们不需要复制模型(Portfolio.cs和PortfolioItem.cs),因为它们已经在我们在上一节中创建的类库中定义了。相反,我们将向该共享库添加一个项目引用。下一步是将WCF服务迁移到ASP.Net Core应用程序。

将WCF服务迁移到ASP.Net Core应用程序

我们需要做的第一件事是创建一个ASP.Net Core应用程序。因此,要么启动你最喜欢的IDE,创建一个基本的ASP.NET Core application或从命令行运行dotnet new web。接下来,我们需要添加一个对protobuf-net.Grpc的包。使用你最喜欢的包管理器安装它,或者简单地运行dotnet add package protobuf-net.Grpc.AspNetCore。我们还需要向上一节中创建的PortfolioData库添加一个项目引用。

现在我们已经准备好了项目,并且添加了所有的依赖项,我们可以继续并创建portfolio服务。创建一个具有以下内容的新类。

public class PortfolioService : IPortfolioService
{
private readonly IPortfolioRepository _repository; public PortfolioService(IPortfolioRepository repository)
{
_repository = repository;
} public async Task<Portfolio> Get(GetPortfolioRequest request)
{
var portfolio = await _repository.GetAsync(request.TraderId, request.PortfolioId); return portfolio;
} public async Task<PortfolioCollection> GetAll(GetAllPortfoliosRequest request)
{
var portfolios = await _repository.GetAllAsync(request.TraderId); var response = new PortfolioCollection
{
Items = portfolios
}; return response;
}
}

上面的服务看起来与WCF服务实现非常相似,除了输入参数类型和返回参数类型之外。

最后但并非最不重要的,我们需要将protobuf-net.Grpc接入ASP.Net Core管道,并在DI容器中注册。在启Startup.cs,我们将做以下补充:

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPortfolioRepository, PortfolioRepository>();
services.AddCodeFirstGrpc();
} public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseRouting(); app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<PortfolioService>();
});
}
}

现在我们已经有了gRPC服务。我们列表上的下一件事是创建客户端应用程序。

创建gRPC客户端应用程序

对于我们的客户端应用程序,我们将继续创建一个控制台应用程序。要么使用你最喜欢的IDE创建一个控制台,要么直接从命令行运行dotnet new console。接下来,我们需要添加对protobuf-net.Grpc和Grpc.Net.Client的NuGet包。使用你最喜欢的包管理器安装它们,或者简单地运行dotnet add package protobuf-net.Grpc和dotnet add package Grpc.Net.Client。我们还需要向我们在第一节中创建的共享库添加一个项目引用。

在我们的Program.cs中,我们将添加以下代码来创建gRPC客户端并与gRPC服务通信。

class Program
{
private const string ServerAddress = "https://localhost:5001"; static async Task Main()
{
var channel = GrpcChannel.ForAddress(ServerAddress);
var portfolios = channel.CreateGrpcService<IPortfolioService>(); try
{
var request = new GetPortfolioRequest
{
TraderId = Guid.Parse("68CB16F7-42BD-4330-A191-FA5904D2E5A0"),
PortfolioId = 42
};
var response = await portfolios.Get(request); Console.WriteLine($"Portfolio contains {response.Items.Count} items.");
}
catch (RpcException e)
{
Console.WriteLine(e.ToString());
}
}
}

现在我们可以测试我们的实现,首先启动ASP.NET Core应用程序,然后启动控制台应用程序。

将WCF双工服务迁移到gRPC

现在我们已经介绍了使用protobuf-net.Grpc 将WCF服务迁移到gRPC的基本知识,我们可以看看一些更复杂的例子。

在本节中,我们将查看SimpleStockPriceTicker,这是一个双工服务,客户端启动连接,服务器使用回调接口在更新可用时发送更新。WCF服务有一个没有返回类型的方法,因为它使用回调接口ISimpleStockTickerCallback实时向客户端发送数据。

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(ISimpleStockTickerCallback))]
public interface ISimpleStockTickerService
{
[OperationContract(IsOneWay = true)]
void Subscribe(string[] symbols);
} [ServiceContract]
public interface ISimpleStockTickerCallback
{
[OperationContract(IsOneWay = true)]
void Update(string symbol, decimal price);
}

当将这个服务迁移到gRPC时,我们可以使用gRPC流。gRPC服务器流的工作方式与上面的WCF服务类似。例如,客户端发送一个请求,而服务器以一个消息流响应。在protobuf-net.Grpc中实现服务器流的惯用方法是从RPC方法返回IAsyncEnumerable<T>。通过这种方式,我们可以在客户端和服务器端为服务契约使用相同的接口。请注意protobuf-net.Grpc也支持Google.Protobuf模式(在服务器端使用IServerStreamWriter<T>,在客户端使用AsyncServerStreamingCall<T>),需要我们为客户端和服务端使用单独的接口方法。使用IAsyncEnumerable<T>作为流媒体将使我们的服务契约看起来像下面的代码。

[ServiceContract]
public interface IStockTickerService
{
[OperationContract]
IAsyncEnumerable<StockTickerUpdate> Subscribe(SubscribeRequest request, CallContext context = default);
}

请注意CallContext参数,它是客户端和服务器端的gRPC调用上下文。这允许我们在客户端和服务器端访问调用上下文,而不需要单独的接口。Gogogle.Protobuf生成的代码将在客户端使用调用,而在服务器端使用ServerCallContext。

因为WCF服务只使用基本类型作为参数,所以我们需要创建一组可以用作参数的数据契约。上面的服务附带的数据契约看起来像这样。注意,我们已经向响应消息添加了一个时间戳字段,这个字段在原始WCF服务中不存在。

[DataContract]
public class SubscribeRequest
{
[DataMember(Order = 1)]
public List<string> Symbols { get; set; } = new List<string>();
} [DataContract]
public class StockTickerUpdate
{
[DataMember(Order = 1)]
public string Symbol { get; set; } [DataMember(Order = 2)]
public decimal Price { get; set; } [DataMember(Order = 3)]
public DateTime Time { get; set; }
}

通过重用微软迁移指南中的IStockPriceSubscriberFactory,我们可以实现下面的服务。通过使用System.Threading.Channels,可以很容易地将事件流到一个异步可枚举对象。

public class StockTickerService : IStockTickerService, IDisposable
{
private readonly IStockPriceSubscriberFactory _subscriberFactory;
private readonly ILogger<StockTickerService> _logger;
private IStockPriceSubscriber _subscriber; public StockTickerService(IStockPriceSubscriberFactory subscriberFactory, ILogger<StockTickerService> logger)
{
_subscriberFactory = subscriberFactory;
_logger = logger;
} public IAsyncEnumerable<StockTickerUpdate> Subscribe(SubscribeRequest request, CallContext context = default)
{
var buffer = Channel.CreateUnbounded<StockTickerUpdate>(); _subscriber = _subscriberFactory.GetSubscriber(request.Symbols.ToArray());
_subscriber.Update += async (sender, args) =>
{
try
{
await buffer.Writer.WriteAsync(new StockTickerUpdate
{
Symbol = args.Symbol,
Price = args.Price,
Time = DateTime.UtcNow
});
}
catch (Exception e)
{
_logger.LogError($"Failed to write message: {e.Message}");
}
}; return buffer.AsAsyncEnumerable(context.CancellationToken);
} public void Dispose()
{
_subscriber?.Dispose();
}
}

WCF全双工服务允许双向异步、实时消息传递。在前面的示例中,客户机启动了一个请求并接收到一个更新流。在这个版本中,客户端流化请求消息,以便对订阅列表添加和删除,而不必创建新的订阅。WCF服务契约的定义如下。客户端使用Subscribe方法启动订阅,并使用AddSymbol和RemoveSymbol方法添加或删除。更新通过回调接口接收,这与前面的服务器流示例相同。

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IFullStockTickerCallback))]
public interface IFullStockTickerService
{
[OperationContract(IsOneWay = true)]
void Subscribe(); [OperationContract(IsOneWay = true)]
void AddSymbol(string symbol); [OperationContract(IsOneWay = true)]
void RemoveSymbol(string symbol);
} [ServiceContract]
public interface IFullStockTickerCallback
{
[OperationContract(IsOneWay = true)]
void Update(string symbol, decimal price);
}

使用protobuf-net.Gprc实现的等价服务契约的情况如下。该服务接受请求消息流并返回响应消息流。

[ServiceContract]
public interface IFullStockTicker
{
[OperationContract]
IAsyncEnumerable<StockTickerUpdate> Subscribe(IAsyncEnumerable<SymbolRequest> request, CallContext context = default);
}

下面定义了附带的数据契约。该请求包括一个action属性,该属性指定该符号是应该从订阅中添加还是删除。响应消息与前面的示例相同。

public enum SymbolRequestAction
{
Add = 0,
Remove = 1
} [DataContract]
public class SymbolRequest
{
[DataMember(Order = 1)]
public SymbolRequestAction Action { get; set; } [DataMember(Order = 2)]
public string Symbol { get; set; }
} [DataContract]
public class StockTickerUpdate
{
[DataMember(Order = 1)]
public string Symbol { get; set; } [DataMember(Order = 2)]
public decimal Price { get; set; } [DataMember(Order = 3)]
public DateTime Time { get; set; }
}

服务的实现如下所示。我们使用与前面示例相同的技术,通过IAsyncEnumerable<T>来流动事件,另外创建一个后台任务,它枚举请求流,并对单个请求进行响应。

public class FullStockTickerService : IFullStockTicker, IDisposable
{
private readonly IFullStockPriceSubscriberFactory _subscriberFactory;
private readonly ILogger<FullStockTickerService> _logger;
private IFullStockPriceSubscriber _subscriber;
private Task _processRequestTask;
private CancellationTokenSource _cts; public FullStockTickerService(IFullStockPriceSubscriberFactory subscriberFactory, ILogger<FullStockTickerService> logger)
{
_subscriberFactory = subscriberFactory;
_logger = logger;
_cts = new CancellationTokenSource();
} public IAsyncEnumerable<StockTickerUpdate> Subscribe(IAsyncEnumerable<SymbolRequest> request, CallContext context)
{
var cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, context.CancellationToken).Token;
var buffer = Channel.CreateUnbounded<StockTickerUpdate>(); _subscriber = _subscriberFactory.GetSubscriber();
_subscriber.Update += async (sender, args) =>
{
try
{
await buffer.Writer.WriteAsync(new StockTickerUpdate
{
Symbol = args.Symbol,
Price = args.Price,
Time = DateTime.UtcNow
});
}
catch (Exception e)
{
_logger.LogError($"Failed to write message: {e.Message}");
}
}; _processRequestTask = ProcessRequests(request, buffer.Writer, cancellationToken); return buffer.AsAsyncEnumerable(cancellationToken);
} private async Task ProcessRequests(IAsyncEnumerable<SymbolRequest> requests, ChannelWriter<StockTickerUpdate> writer, CancellationToken cancellationToken)
{
await foreach (var request in requests.WithCancellation(cancellationToken))
{
switch (request.Action)
{
case SymbolRequestAction.Add:
_subscriber.Add(request.Symbol);
break;
case SymbolRequestAction.Remove:
_subscriber.Remove(request.Symbol);
break;
default:
_logger.LogWarning($"Unknown Action '{request.Action}'.");
break;
}
} writer.Complete();
} public void Dispose()
{
_cts.Cancel();
_subscriber?.Dispose();
}
}

总结

恭喜你!你已经走到这一步了。现在你知道了将WCF服务迁移到gRPC的另一种方法。希望这种技术比用.proto格式重写现有的数据契约要快得多。

欢迎关注我的公众号,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。

原文链接:https://martinbjorkstrom.com/posts/2020-09-09-migrating-wcf-to-grpc

将WCF迁移到gRPC的更多相关文章

  1. 浅议Grpc传输机制和WCF中的回调机制的代码迁移

    浅议Grpc传输机制和WCF中的回调机制的代码迁移 一.引子 如您所知,gRPC是目前比较常见的rpc框架,可以方便的作为服务与服务之间的通信基础设施,为构建微服务体系提供非常强有力的支持. 而基于. ...

  2. 旧 WCF 项目迁移到 asp.net core + gRPC 的尝试

    一个月前,公司的运行WCF的windows服务器down掉了,由于 AWS 没有通知,没有能第一时间发现问题. 所以,客户提出将WCF服务由C#改为JAVA,在Linux上面运行:一方面,AWS对Li ...

  3. 翻译:使用 CoreWCF 升级 WCF 服务到 .NET 6

    翻译:使用 CoreWCF 升级 WCF 服务到 .NET 6 原文地址:https://devblogs.microsoft.com/dotnet/upgrading-a-wcf-service-t ...

  4. 向ASP.NET Core迁移

    有人说.NET在国内的氛围越来越不行了,看博客园文章的浏览量也起不来.是不是要转Java呢? 没有必要扯起语言的纷争,Java也好C#都只是语言是工具,各有各的使用场景.以前是C#非开源以及不能在Li ...

  5. NET Core迁移

      向ASP.NET Core迁移   有人说.NET在国内的氛围越来越不行了,看博客园文章的浏览量也起不来.是不是要转Java呢? 没有必要扯起语言的纷争,Java也好C#都只是语言是工具,各有各的 ...

  6. .NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端

    .NET Core love gRPC 千呼万唤的 .NET Core 3.0 终于在 9 月份正式发布,在它的众多新特性中,除了性能得到了大大提高,比较受关注的应该是 ASP.NET Core 3. ...

  7. wcf callback在主线程被调用

    记得当时往wcf迁移了部分service以后,tester发现有时候系统会没有任何反应,cpu占用倒是没有问题,可就是做不了事情. 感觉是哪里死锁了,但也不是每次都能再现,后来发现如果把callbac ...

  8. Envoy如何打败Linkerd成为L7负载平衡器的最佳选择?

    本文转自:http://www.servicemesh.cn/?/article/41 作者:MIKE WHITE 翻译:姚炳雄 原文:Using Envoy to Load Balance gRPC ...

  9. Dubbo 如何成为连接异构微服务体系的最佳服务开发框架

    从编程开发的角度来说,Apache Dubbo (以下简称 Dubbo)首先是一款 RPC 服务框架,它最大的优势在于提供了面向接口代理的服务编程模型,对开发者屏蔽了底层的远程通信细节.同时 Dubb ...

随机推荐

  1. PyQt(Python+Qt)学习随笔:QDateTimeEdit日期时间编辑部件

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 Designer输入部件中,Date/Time E ...

  2. PyQt学习随笔:QTableWidget的visualRow、visualColumn、logicalRow、logicalColumn(可见行、逻辑行、可见列、逻辑列)相关概念及方法探究

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.概念 关于逻辑行logicalRow.列logicalColumn和可见行visualRow.列 ...

  3. 第10.10节 Python使用__init__.py自动加载包下内容

    在前面章节老猿介绍了包下模块及子包的加载的各种方式,并说明包的加载首先是自动加载包下的__init__.py文件.在<第10.6节 Python包的概念>中介绍了__init__.py文件 ...

  4. 性能测试学习之路 (四)jmeter 脚本开发实战(JDBC &JMS &接口脚本 & 轻量级接口自动化测试框架)

    1.业务级脚本开发 登录脚本->思路:在线程组下新建两个HTTP请求,一个是完成访问登录页,一个是完成登录的数据提交.   步骤如下: 1) 访问登录页 2) 提交登录数据的HTTP PS:对于 ...

  5. 分布式存储系统-HDFS

    1 HDFS 架构 HDFS作为分布式文件管理系统,Hadoop的基础.HDFS整体架构包括:NameNode.DataNode.Secondary NameNode,如图: HDFS采用主从式的分布 ...

  6. python学生管理名片

    name=['刘备','关羽','张飞','赵云','马超'] print('名片管理系统1.0\n1.增加一个新的名片\n2.删除一个名片\n3.修改一个名片\n4.查找一个名片\n5.退出名片管理 ...

  7. NOI Online #1 入门组 魔法

    全网都是矩阵快速幂,我只会倍增DP 其实这题与 AcWing 345. 牛站 还是比较像的,那题可以矩阵快速幂 / 倍增,这题也行. 先 \(Floyd\) 预处理两点之间不用魔法最短距离 \(d_{ ...

  8. NPM相关知识点

    1.Windows环境变量的配置 npm config set prefix "D:\Program Files\nodejs\node_global" npm config se ...

  9. 实验:非GTID 一主多从变级联架构

  10. 密码管理平台ratticdb的部署,在centos7上的部署

    一,前言 一直想用ratticdb这个有web界面的密码管理工具,百度了一下居然没有找到中文的部署文档,访问官网也是notfound.找到了官方的部署指南:https://github.com/til ...