1. 程式人生 > >.NET Core下使用gRpc公開服務(SSL/TLS)

.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安裝程式:

b.製作證書

網上有很多的教程,但是對於新手來說直接給繞暈了,有的有ca、client和service有的沒有,這裡筆者提供一個全面的cmd指令碼(預設CA是自己)

 1 @echo off
 2 set OPENSSL_CONF=c:\OpenSSL-Win64\bin\openssl.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