.NET Core下使用gRpc公开服务(SSL/TLS)

时间:2023-03-09 17:32:30
.NET Core下使用gRpc公开服务(SSL/TLS)

一、前言

前一阵子关于.NET的各大公众号都发表了关于gRpc的消息,而随之而来的就是一波关于.NET Core下如何使用的教程,但是在这众多的教程中基本都是泛泛而谈,难以实际在实际环境中使用,而该篇教程以gRpc为主,但是使用了其SSL/TLS,这样更加符合实际的生产使用,期间也会配套的讲解Docker、openssl等。

二、服务端

a.准备工作

笔者的项目分为三个部分分别如下所示:

Sino.GrpcService.Host(控制台):宿主程序

Sino.GrpcService.Impl(类库):实现协议

Sino.GrpcService.Protocol(类库):生成协议

最终的项目如下图所示:

.NET Core下使用gRpc公开服务(SSL/TLS)

每个项目的project.json如下所示:

 {
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true,
"copyToOutput": [ "server.crt", "server.key", "appSettings.json", "appSettings.*.json" ]
},
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0"
},
"Sino.GrpcService.Impl": "1.0.0-*",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.Binder": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0"
},
"frameworks": {
"netcoreapp1.0": {
"imports": [ "dnxcore50", "net452" ]
}
},
"publishOptions": {
"include": [ "server.crt", "server.key", "appSettings.json", "appSettings.*.json" ]
}
}

其中“buildOptions”和“publishOptions”中我们将后面我们需要的证书包含到输出和发布中,其中我们还利用了“Configuration”相关组件去读取配置信息。

Sino.GrpcService.Impl:

 {
"version": "1.0.0-*",
"dependencies": {
"Autofac": "4.1.1",
"Google.Protobuf": "3.1.0",
"Grpc.Core": "1.0.1-pre1",
"NETStandard.Library": "1.6.0",
"Sino.GrpcService.Protocol": "1.0.0-*",
"MongoDB.Driver": "2.3.0",
"Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
},
"frameworks": {
"netstandard1.6": {
"imports": "dnxcore50"
}
}
}

其中我们安装了“MongoDb.Driver”,为了能够贴近真实的情况,笔者这里采用MongoDb作为数据源来提供数据,当然读者为了能够快速上手可以硬编码一些数据。

Sino.GrpcService.Protocol:

 {
"version": "1.0.0-*",
"dependencies": {
"Google.Protobuf": "3.1.0",
"Grpc.Core": "1.0.1-pre1",
"NETStandard.Library": "1.6.0"
},
"frameworks": {
"netstandard1.6": {
"imports": "dnxcore50"
},
"net452": {}
}
}

至此项目的初始化结束。

b.编写协议

首先我们打开Sino.GrpcService.Protocol项目,在其中新建一个msg.proto文件,打开msg.proto文件,我们将在其中编写基于proto3语言的协议,以便后面自动生成到各语言,如果读者需要更深入的学习可以打开该网站Proto3语言指南

这里我们定义我们当前使用的是proto3语言并且包名(生成为C#则为命名空间)为:

syntax = "proto3";
package Sino.GrpcService;

笔者为该服务定义了1个服务,且有4种方法:

service MsgService{
rpc GetList(GetMsgListRequest) returns (GetMsgListReply){}
rpc GetOne(GetMsgOneRequest) returns (GetMsgOneReply){}
rpc Edit(EditMsgRequest) returns (EditMsgReply){}
rpc Remove(RemoveMsgRequest) returns (RemoveMsgReply){}
}

对应到其中每个方法的接收参数和返回参数的定义如下:

 message GetMsgListRequest {
int64 UserId = ;
string Title = ;
int64 StartTime = ;
int64 EndTime = ;
} message GetMsgListReply {
message MsgItem {
string Id = ;
string Title = ;
string Content = ;
int64 UserId = ;
int64 Time = ;
}
repeated MsgItem Items = ;
int64 Count = ;
bool IsSuccess = ;
string ErrorMsg = ;
} message GetMsgOneRequest {
string Id = ;
} message GetMsgOneReply {
string Id = ;
string Title = ;
string Content = ;
int64 UserId = ;
int64 Time = ;
bool IsSuccess = ;
string ErrorMsg = ;
} message EditMsgRequest {
string Id = ;
string Title = ;
string Content = ;
} message EditMsgReply {
bool IsSuccess = ;
string ErrorMsg = ;
} message RemoveMsgRequest {
string Id = ;
} message RemoveMsgReply {
bool IsSuccess = ;
string ErrorMsg = ;
}

到这为止我们就完成了协议的编写。

c.将协议生成为C#代码

相对于网站的很多关于C#使用gRpc的教程都是基于.NET项目框架下的,所以可以安装gRpc.Tools,但是.NET Core安装后是找不到工具的,所以读者可以新建一个.NET项目安装该类库,然后将其中的工具复制到Sino.GrpcService.Protocol中,这里读者需要根据你当前的系统去选择,复制完成之后在该项目中新建一个名为“ProtocGenerate.cmd”的文件,在其中输入以下指令:

protoc -I . --csharp_out . --grpc_out . --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe msg.proto

然后读者直接双击运行,就会看到项目下生成了“Msg.cs”和“MsgGrpc.cs”两个文件,这样就完成了所有协议部分的工作了,最终的项目结构如下所示:

.NET Core下使用gRpc公开服务(SSL/TLS)

d.编写实现代码

有了协议层之后我们就可以开始编写实现了,因为笔者这里使用了MongoDb提供数据所以下文篇幅会较长。

首先打开Sino.GrpcService.Impl项目在其中新建Model文件,然后在该文件夹下新建MsgDM.cs文件,该文件主要是定义MongoDb存储的数据结构,具体内容如下所示:

  /// <summary>
/// 消息体
/// </summary>
public sealed class MsgDM
{
/// <summary>
/// 编号
/// </summary>
public ObjectId Id { get; set; } /// <summary>
/// 标题
/// </summary>
public string Title { get; set; } /// <summary>
/// 内容
/// </summary>
public string Content { get; set; } /// <summary>
/// 用户编号
/// </summary>
public long UserId { get; set; } /// <summary>
/// 时间
/// </summary>
public long Time { get; set; }
}

紧接着我们新建Repositories文件夹,在其中新建四个文件分别为“IDataContext.cs”、“DataContext.cs”、“IMsgRepository.cs”和“MsgRepository.cs”。打开IDataContext.cs文件在其中编写如下内容:

    /// <summary>
/// 数据库上下文
/// </summary>
public interface IDataContext
{
IMongoDatabase Database { get; set; }
}

打开DataContext.cs文件进行数据库初始化相关工作:

    public class DataContext : IDataContext
{
public IMongoDatabase Database { get; set; } public DataContext(IConfigurationRoot config)
{
var client = new MongoClient(config.GetConnectionString("mongodb"));
Database = client.GetDatabase("aSQ0cWkEshl8NiVn");
}
}

打开IMsgRepository.cs,我们需要在其中定义仓储提供的操作:

/// <summary>
/// 消息仓储
/// </summary>
public interface IMsgRepository
{
/// <summary>
/// 获取列表
/// </summary>
Task<List<MsgDM>> GetList(long userId, string title, long startTime, long endTime); /// <summary>
/// 获取实体
/// </summary>
Task<MsgDM> Get(string id); /// <summary>
/// 更新实体
/// </summary>
Task<bool> Update(MsgDM data); /// <summary>
/// 添加实体
/// </summary>
Task<string> Insert(MsgDM data); /// <summary>
/// 删除实体
/// </summary>
Task<bool> Delete(string id);
}

对应的我们还需要打开MsgRepository.cs文件实现该接口:

 public class MsgRepository : IMsgRepository
{
private IDataContext _dataContext;
private IMongoCollection<MsgDM> _collection; public MsgRepository(IDataContext dataContext)
{
_dataContext = dataContext;
_collection = _dataContext.Database.GetCollection<MsgDM>("msg");
} public async Task<bool> Delete(string id)
{
var filter = Builders<MsgDM>.Filter.Eq(x => x.Id, new ObjectId(id));
var result = await _collection.DeleteOneAsync(filter);
return result.DeletedCount == ;
} public Task<MsgDM> Get(string id)
{
var objectId = new ObjectId(id);
var result = (from item in _collection.AsQueryable()
where item.Id == objectId
select item).FirstOrDefault();
return Task.FromResult(result);
} public Task<List<MsgDM>> GetList(long userId, string title, long startTime, long endTime)
{
IQueryable<MsgDM> filter = _collection.AsQueryable();
if (userId != )
filter = filter.Where(x => x.UserId == userId);
if (!string.IsNullOrEmpty(title))
filter = filter.Where(x => x.Title.Contains(title));
if (startTime != )
filter = filter.Where(x => x.Time > startTime);
if (endTime != )
filter = filter.Where(x => x.Time < startTime); return Task.FromResult(filter.ToList());
} public async Task<string> Insert(MsgDM data)
{
await _collection.InsertOneAsync(data);
return data.Id.ToString();
} public async Task<bool> Update(MsgDM data)
{
var filter = Builders<MsgDM>.Filter.Eq(x => x.Id, data.Id);
var update = Builders<MsgDM>.Update.Set(x => x.Title, data.Title).Set(x => x.Content, data.Content); var result = await _collection.UpdateOneAsync(Builders<MsgDM>.Filter.Eq(x => x.Id, data.Id), update); return result.ModifiedCount == ;
}
}

完成了上面关于数据库的工作,下面我们就进入正题,开始实现gRpc服务了,首先我们在项目根目录下新建MsgServiceImpl.cs文件,在其中实现我们协议中的服务:

  public class MsgServiceImpl : MsgService.MsgServiceBase
{
private IMsgRepository _msgRepository; public MsgServiceImpl(IMsgRepository msgRepository)
{
_msgRepository = msgRepository;
} public override async Task<GetMsgListReply> GetList(GetMsgListRequest request, ServerCallContext context)
{
var result = new GetMsgListReply();
var list = await _msgRepository.GetList(request.UserId, request.Title, request.StartTime, request.EndTime);
result.IsSuccess = true;
result.Items.AddRange(list.Select(x => new GetMsgListReply.Types.MsgItem
{
UserId = x.UserId,
Title = x.Title,
Time = x.Time,
Content = x.Content
}).ToList());
return result;
} public override async Task<EditMsgReply> Edit(EditMsgRequest request, ServerCallContext context)
{
var result = new EditMsgReply();
result.IsSuccess = await _msgRepository.Update(new MsgDM
{
Id = new MongoDB.Bson.ObjectId(request.Id),
Title = request.Title,
Content = request.Content
}); return result;
} public override async Task<GetMsgOneReply> GetOne(GetMsgOneRequest request, ServerCallContext context)
{
var msg = await _msgRepository.Get(request.Id); return new GetMsgOneReply
{
IsSuccess = true,
Id = msg.Id.ToString(),
UserId = msg.UserId,
Title = msg.Title,
Content = msg.Content,
Time = msg.Time
};
} public override async Task<RemoveMsgReply> Remove(RemoveMsgRequest request, ServerCallContext context)
{
var result = new RemoveMsgReply();
result.IsSuccess = await _msgRepository.Delete(request.Id); return result;
}
}

三、证书生成

a.安装openssl

首先读者需要从该网站下载openssl安装程序:

Openssl下载

笔者的系统是Win10 64所以下载的是“Win64 OpenSSL v1.1.0b”。

b.制作证书

网上有很多的教程,但是对于新手来说直接给绕晕了,有的有ca、client和service有的没有,这里笔者提供一个全面的cmd脚本(默认CA是自己):

 @echo off
set OPENSSL_CONF=c:\OpenSSL-Win64\bin\openssl.cfg echo Generate CA key:
openssl genrsa -passout pass: -des3 -out ca.key echo Generate CA certificate:
openssl req -passin pass: -new -x509 -days -key ca.key -out ca.crt -subj "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root" echo Generate server key:
openssl genrsa -passout pass: -des3 -out server.key echo Generate server signing request:
openssl req -passin pass: -new -key server.key -out server.csr -subj "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root" echo Self-sign server certificate:
openssl x509 -req -passin pass: -days -in server.csr -CA ca.crt -CAkey ca.key -set_serial -out server.crt echo Remove passphrase from server key:
openssl rsa -passin pass: -in server.key -out server.key echo Generate client key
openssl genrsa -passout pass: -des3 -out client.key echo Generate client signing request:
openssl req -passin pass: -new -key client.key -out client.csr -subj "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root" echo Self-sign client certificate:
openssl x509 -passin pass: -req -days -in client.csr -CA ca.crt -CAkey ca.key -set_serial -out client.crt echo Remove passphrase from client key:
openssl rsa -passin pass: -in client.key -out client.key

以上的脚本也会生成我们下面Demo中使用的证书。

四、完善服务端

用了上面的证书之后我们需要继续把服务端启动gRpc服务部分的代码书写完毕,这里笔者是采用命令行形式运行的,所以gRpc的启动是独立放在一个文件文件中,如下RpcConfiguration所示:

 public static class RpcConfiguration
{
private static Server _server;
private static IContainer _container; public static void Start(IConfigurationRoot config)
{
var builder = new ContainerBuilder(); builder.RegisterInstance(config).As<IConfigurationRoot>();
builder.RegisterInstance(new DataContext(config)).As<IDataContext>();
builder.RegisterAssemblyTypes(typeof(IDataContext).GetTypeInfo().Assembly).Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces(); _container = builder.Build();
var servercert = File.ReadAllText(@"server.crt");
var serverkey = File.ReadAllText(@"server.key");
var keypair = new KeyCertificatePair(servercert, serverkey);
var sslCredentials = new SslServerCredentials(new List<KeyCertificatePair>() { keypair });
_server = new Server
{
Services = { MsgService.BindService(new MsgServiceImpl(_container.Resolve<IMsgRepository>())) },
Ports = { new ServerPort("0.0.0.0", , sslCredentials) }
};
_server.Start();
_server.ShutdownTask.Wait();
} public static void Stop()
{
_server?.ShutdownAsync().Wait();
}
}

其中我们使用了server.crtserver.key这两个证书,所以在Host项目中需要将这个两个证书文件copy到项目根目录下,如果需要发布的时候包含则需要在project.json中配置如下节:

  "publishOptions": {
"include": [ "server.crt", "server.key", "appSettings.json", "appSettings.*.json" ]
}

最后我们需要在Program中启动对应的gRpc即可。

五、客户端编写

完成了服务端的编写剩下的就是客户端的编写,当然客户端的编写相对容易很多,笔者这里直接把Sino.GrpcService.Protocol项目包含到客户端解决方案中了(在正式开发中建议采用nuget包进行管理),为了简单起见,所以只调用了其中一个服务接口:

public static class MsgServiceClient
{
private static Channel _channel;
private static MsgService.MsgServiceClient _client; static MsgServiceClient()
{
var cacert = File.ReadAllText("server.crt");
var ssl = new SslCredentials(cacert);
var channOptions = new List<ChannelOption>
{
new ChannelOption(ChannelOptions.SslTargetNameOverride,"root")
};
_channel = new Channel("grpcservice.t0.daoapp.io:61130", ssl, channOptions);
_client = new MsgService.MsgServiceClient(_channel);
} public static GetMsgListReply GetList(int userId, string title, long startTime, long endTime)
{
return _client.GetList(new GetMsgListRequest
{
UserId = userId,
Title = title,
StartTime = startTime,
EndTime = endTime
});
}
}

需要注意下其中“ChannelOptions.SslTargetNameOverride”这部分是必须的,因为我们是自己生成的证书,所以域名是root,如果是生产环境可以不需要。

六、利用Docker运行

a.安装Docker For Windows

这里需要win10的系统,这样可以直接在ps中直接利用docker指令了。

b.编写Dockerfile

因为1.1版本出来了,但是经过本人的验证,如果你的应用不升级是无法使用该镜像的,默认使用1.1,所以这里我们的Dockerfile需要指定下特定的版本,否则是无法构建的,我们首先在解决方案的根目录下新建Dockerfile文件,然后在其中放入以下命令:

 FROM microsoft/dotnet:1.0-sdk-projectjson

 ADD ./ /usr/local/src
WORKDIR /usr/local/src/Sino.GrpcService.Host/ RUN cd /usr/local/src/
RUN dotnet restore -v http://api.nuget.org/v3/index.json
RUN dotnet build EXPOSE CMD ["dotnet","run"]

c.生成镜像并运行

我们打开ps,然后cd到解决方案的文件夹下利用:

docker build -t gRpcService:1.0 .

开始构建,基于国内的情况建议大家将docker默认拉取镜像的地址调整下。生成好之后,利用以下指令去启动即可:

docker run -d –name -p : gRpcService gRpcService:1.0

当然客户端连接的地址和端口也要根据-p指定的情况去调整。

七、其他

对应的源码可以访问以下地址:

https://github.com/Vip56/Sino.GrpcService

https://github.com/Vip56/Sino.GrpcClient

如果需要询问相关问题的可以短消息给我。