zoukankan      html  css  js  c++  java
  • .NET Core下使用gRpc公开服务(SSL/TLS)

    一、前言

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

    二、服务端

    a.准备工作

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

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

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

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

    最终的项目如下图所示:

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

     1 {
     2   "version": "1.0.0-*",
     3   "buildOptions": {
     4     "emitEntryPoint": true,
     5     "copyToOutput": [ "server.crt", "server.key", "appSettings.json", "appSettings.*.json" ]
     6   },
     7   "dependencies": {
     8     "Microsoft.NETCore.App": {
     9       "type": "platform",
    10       "version": "1.0.0"
    11     },
    12     "Sino.GrpcService.Impl": "1.0.0-*",
    13     "Microsoft.Extensions.Configuration.Json": "1.0.0",
    14     "Microsoft.Extensions.Configuration.Binder": "1.0.0",
    15     "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0"
    16   },
    17   "frameworks": {
    18     "netcoreapp1.0": {
    19       "imports": [ "dnxcore50", "net452" ]
    20     }
    21   },
    22   "publishOptions": {
    23     "include": [ "server.crt", "server.key", "appSettings.json", "appSettings.*.json" ]
    24   }
    25 }
    View Code

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

    Sino.GrpcService.Impl:

     1 {
     2   "version": "1.0.0-*",
     3   "dependencies": {
     4     "Autofac": "4.1.1",
     5     "Google.Protobuf": "3.1.0",
     6     "Grpc.Core": "1.0.1-pre1",
     7     "NETStandard.Library": "1.6.0",
     8     "Sino.GrpcService.Protocol": "1.0.0-*",
     9     "MongoDB.Driver": "2.3.0",
    10     "Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
    11   },
    12   "frameworks": {
    13     "netstandard1.6": {
    14       "imports": "dnxcore50"
    15     }
    16   }
    17 }
    View Code

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

    Sino.GrpcService.Protocol:

     1 {
     2   "version": "1.0.0-*",
     3   "dependencies": {
     4     "Google.Protobuf": "3.1.0",
     5     "Grpc.Core": "1.0.1-pre1",
     6     "NETStandard.Library": "1.6.0"
     7   },
     8   "frameworks": {
     9     "netstandard1.6": {
    10       "imports": "dnxcore50"
    11     },
    12     "net452": {}
    13   }
    14 }
    View Code

    至此项目的初始化结束。

    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){}
    }

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

     1 message GetMsgListRequest {
     2   int64 UserId = 1;
     3   string Title = 2;
     4   int64 StartTime = 3;
     5   int64 EndTime = 4;
     6 }
     7 
     8 message GetMsgListReply {
     9   message MsgItem {
    10     string Id = 1;
    11     string Title = 2;
    12     string Content = 3;
    13     int64 UserId = 4;
    14     int64 Time = 5;
    15   }
    16   repeated MsgItem Items = 1;
    17   int64 Count = 2;
    18   bool IsSuccess = 3;
    19   string ErrorMsg = 4;
    20 }
    21 
    22 message GetMsgOneRequest {
    23   string Id = 1;
    24 }
    25 
    26 message GetMsgOneReply {
    27   string Id = 1;
    28   string Title = 2;
    29   string Content = 3;
    30   int64 UserId = 4;
    31   int64 Time = 5;
    32   bool IsSuccess = 6;
    33   string ErrorMsg = 7;
    34 }
    35 
    36 message EditMsgRequest {
    37   string Id = 1;
    38   string Title = 2;
    39   string Content = 3;
    40 }
    41 
    42 message EditMsgReply {
    43   bool IsSuccess = 1;
    44   string ErrorMsg = 2;
    45 }
    46 
    47 message RemoveMsgRequest {
    48   string Id = 1;
    49 }
    50 
    51 message RemoveMsgReply {
    52   bool IsSuccess = 1;
    53   string ErrorMsg = 2;
    54 }
    View Code

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

    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”两个文件,这样就完成了所有协议部分的工作了,最终的项目结构如下所示:

    d.编写实现代码

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

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

     1  /// <summary>
     2     /// 消息体
     3     /// </summary>
     4     public sealed class MsgDM
     5     {
     6         /// <summary>
     7         /// 编号
     8         /// </summary>
     9         public ObjectId Id { get; set; }
    10 
    11         /// <summary>
    12         /// 标题
    13         /// </summary>
    14         public string Title { get; set; }
    15 
    16         /// <summary>
    17         /// 内容
    18         /// </summary>
    19         public string Content { get; set; }
    20 
    21         /// <summary>
    22         /// 用户编号
    23         /// </summary>
    24         public long UserId { get; set; }
    25 
    26         /// <summary>
    27         /// 时间
    28         /// </summary>
    29         public long Time { get; set; }
    30     }
    View Code

    紧接着我们新建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文件实现该接口:

     1 public class MsgRepository : IMsgRepository
     2     {
     3         private IDataContext _dataContext;
     4         private IMongoCollection<MsgDM> _collection;
     5 
     6         public MsgRepository(IDataContext dataContext)
     7         {
     8             _dataContext = dataContext;
     9             _collection = _dataContext.Database.GetCollection<MsgDM>("msg");
    10         }
    11 
    12         public async Task<bool> Delete(string id)
    13         {
    14             var filter = Builders<MsgDM>.Filter.Eq(x => x.Id, new ObjectId(id));
    15             var result = await _collection.DeleteOneAsync(filter);
    16             return result.DeletedCount == 1;
    17         }
    18 
    19         public Task<MsgDM> Get(string id)
    20         {
    21             var objectId = new ObjectId(id);
    22             var result = (from item in _collection.AsQueryable()
    23                           where item.Id == objectId
    24                           select item).FirstOrDefault();
    25             return Task.FromResult(result);
    26         }
    27 
    28         public Task<List<MsgDM>> GetList(long userId, string title, long startTime, long endTime)
    29         {
    30             IQueryable<MsgDM> filter = _collection.AsQueryable();
    31             if (userId != 0)
    32                 filter = filter.Where(x => x.UserId == userId);
    33             if (!string.IsNullOrEmpty(title))
    34                 filter = filter.Where(x => x.Title.Contains(title));
    35             if (startTime != 0)
    36                 filter = filter.Where(x => x.Time > startTime);
    37             if (endTime != 0)
    38                 filter = filter.Where(x => x.Time < startTime);
    39 
    40             return Task.FromResult(filter.ToList());
    41         }
    42 
    43         public async Task<string> Insert(MsgDM data)
    44         {
    45             await _collection.InsertOneAsync(data);
    46             return data.Id.ToString();
    47         }
    48 
    49         public async Task<bool> Update(MsgDM data)
    50         {
    51             var filter = Builders<MsgDM>.Filter.Eq(x => x.Id, data.Id);
    52             var update = Builders<MsgDM>.Update.Set(x => x.Title, data.Title).Set(x => x.Content, data.Content);
    53 
    54             var result = await _collection.UpdateOneAsync(Builders<MsgDM>.Filter.Eq(x => x.Id, data.Id), update);
    55 
    56             return result.ModifiedCount == 1;
    57         }
    58     }
    View Code

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

     1  public class MsgServiceImpl : MsgService.MsgServiceBase
     2     {
     3         private IMsgRepository _msgRepository;
     4 
     5         public MsgServiceImpl(IMsgRepository msgRepository)
     6         {
     7             _msgRepository = msgRepository;
     8         }
     9 
    10         public override async Task<GetMsgListReply> GetList(GetMsgListRequest request, ServerCallContext context)
    11         {
    12             var result = new GetMsgListReply();
    13             var list = await _msgRepository.GetList(request.UserId, request.Title, request.StartTime, request.EndTime);
    14             result.IsSuccess = true;
    15             result.Items.AddRange(list.Select(x => new GetMsgListReply.Types.MsgItem
    16             {
    17                 UserId = x.UserId,
    18                 Title = x.Title,
    19                 Time = x.Time,
    20                 Content = x.Content
    21             }).ToList());
    22             return result;
    23         }
    24 
    25         public override async Task<EditMsgReply> Edit(EditMsgRequest request, ServerCallContext context)
    26         {
    27             var result = new EditMsgReply();
    28             result.IsSuccess = await _msgRepository.Update(new MsgDM
    29             {
    30                 Id = new MongoDB.Bson.ObjectId(request.Id),
    31                 Title = request.Title,
    32                 Content = request.Content
    33             });
    34 
    35             return result;
    36         }
    37 
    38         public override async Task<GetMsgOneReply> GetOne(GetMsgOneRequest request, ServerCallContext context)
    39         {
    40             var msg = await _msgRepository.Get(request.Id);
    41 
    42             return new GetMsgOneReply
    43             {
    44                 IsSuccess = true,
    45                 Id = msg.Id.ToString(),
    46                 UserId = msg.UserId,
    47                 Title = msg.Title,
    48                 Content = msg.Content,
    49                 Time = msg.Time
    50             };
    51         }
    52 
    53         public override async Task<RemoveMsgReply> Remove(RemoveMsgRequest request, ServerCallContext context)
    54         {
    55             var result = new RemoveMsgReply();
    56             result.IsSuccess = await _msgRepository.Delete(request.Id);
    57 
    58             return result;
    59         }
    60     }
    View Code

    三、证书生成

    a.安装openssl

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

    Openssl下载

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

    b.制作证书

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

     1 @echo off
     2 set OPENSSL_CONF=c:OpenSSL-Win64inopenssl.cfg
     3 
     4 echo Generate CA key:
     5 openssl genrsa -passout pass:1111 -des3 -out ca.key 4096
     6 
     7 echo Generate CA certificate:
     8 openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root"
     9 
    10 echo Generate server key:
    11 openssl genrsa -passout pass:1111 -des3 -out server.key 4096
    12 
    13 echo Generate server signing request:
    14 openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root"
    15 
    16 echo Self-sign server certificate:
    17 openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
    18 
    19 echo Remove passphrase from server key:
    20 openssl rsa -passin pass:1111 -in server.key -out server.key
    21 
    22 echo Generate client key
    23 openssl genrsa -passout pass:1111 -des3 -out client.key 4096
    24 
    25 echo Generate client signing request:
    26 openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=CN/ST=JS/L=ZJ/O=sino/OU=test/CN=root"
    27 
    28 echo Self-sign client certificate:
    29 openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
    30 
    31 echo Remove passphrase from client key:
    32 openssl rsa -passin pass:1111 -in client.key -out client.key

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

    四、完善服务端

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

     1 public static class RpcConfiguration
     2     {
     3         private static Server _server;
     4         private static IContainer _container;
     5 
     6         public static void Start(IConfigurationRoot config)
     7         {
     8             var builder = new ContainerBuilder();
     9 
    10             builder.RegisterInstance(config).As<IConfigurationRoot>();
    11             builder.RegisterInstance(new DataContext(config)).As<IDataContext>();
    12             builder.RegisterAssemblyTypes(typeof(IDataContext).GetTypeInfo().Assembly).Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces();
    13 
    14             _container = builder.Build();
    15             var servercert = File.ReadAllText(@"server.crt");
    16             var serverkey = File.ReadAllText(@"server.key");
    17             var keypair = new KeyCertificatePair(servercert, serverkey);
    18             var sslCredentials = new SslServerCredentials(new List<KeyCertificatePair>() { keypair });
    19             _server = new Server
    20             {
    21                 Services = { MsgService.BindService(new MsgServiceImpl(_container.Resolve<IMsgRepository>())) },
    22                 Ports = { new ServerPort("0.0.0.0", 9007, sslCredentials) }
    23             };
    24             _server.Start();
    25             _server.ShutdownTask.Wait();
    26         }
    27 
    28         public static void Stop()
    29         {
    30             _server?.ShutdownAsync().Wait();
    31         }
    32     }
    View Code

    其中我们使用了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文件,然后在其中放入以下命令:

     1 FROM microsoft/dotnet:1.0-sdk-projectjson
     2 
     3 ADD ./ /usr/local/src
     4 WORKDIR /usr/local/src/Sino.GrpcService.Host/
     5 
     6 RUN cd /usr/local/src/
     7 RUN dotnet restore -v http://api.nuget.org/v3/index.json
     8 RUN dotnet build
     9 
    10 EXPOSE 9007
    11 
    12 CMD ["dotnet","run"]

    c.生成镜像并运行

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

    docker build -t gRpcService:1.0 .

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

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

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

    七、其他

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

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

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

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

  • 相关阅读:
    Sort it
    set 集合容器
    convertToString与ToString的区别
    string基本字符系列容器
    辗转相除法
    进程的总结
    进程池进阶
    进程池
    生产者消费者模型
    IPC :进程之间的通信
  • 原文地址:https://www.cnblogs.com/yaozhenfa/p/gRpc_with_ssl.html
Copyright © 2011-2022 走看看