.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": {View Code9 "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 }
其中“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.crt和server.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