在调用 gRPC 服务时,避免重复写FromAddress(url)代码,而是通过配置文件反射加载 gRPC 服务。
2. 硬编码写法假设我们在 proto 文件中定义了叫 ServiceRPC 的 service,.net gRPC 会自动创建一个 ServiceRPCClient 类型,该类的构造函数中需要一个 channel,如下代码所示。
using Grpc.Net.Client;
namespace XXX
{
public class Program
{
var channel = GrpcChannel.ForAddress("http://localhost:5000");
var service = new ServiceRPC.ServiceRPCClient(channel);
}
}
上述写法的弊端:
- 代码中需要出现url,url即便通过配置文件加载,当服务多起来后也会配置重复化、碎片化;
- 代码结构高度一致,冗余度高;
- 不容易保证 service 对象是单例的,一个进程中可能出现多处创建代码。
当有服务的注册、发现、高可用性、监控等大型应用需要时,可以采用 consul 一类的工具,本文的方法是假设服务均为 .net gRPC,服务的注册通过修改配置文件完成,不涉及服务发现、高可用性、监控等。
4. 服务的可配置加载 4.1 配置对象分为 Channel 和 ServiceDef 两层。
Channel 有 Name 属性(服务的进程名) 和 Address属性(如 http://localhost:5000);
ServiceDef 有 Name 属性(proto文件中定义的服务名);
另外,Channel 下包含多个 ServiceDef,Channel缓存了GrpcChannel对象,ServiceDef缓存了Instance实例,此实例为单例,只创建一次。
public class ServiceDef
{
public ServiceDef(string name, Channel channel)
{
Name = name;
Channel = channel;
}
public string Name { get; }
public Type Type { get; set; }
public object Instance { get; set; }
public Channel Channel { get; }
}
public class Channel
{
public Channel(string name, string address)
{
Name = name;
Address = address;
Services = new List();
}
public string Name { get; }
public string Address { get; }
public List Services { get; }
public GrpcChannel GrpcChannel { get; set; }
}
4.2 配置文件格式
这里我采用了 xml 文件,用 json 也一样。
4.3 XML配置文件读取
private static Dictionary4.4 反射创建服务实例GetChannels(string configFile) { Dictionary result = new Dictionary (); string ConfigPath = AppDomain.CurrentDomain.BaseDirectory + configFile; if (!File.Exists(ConfigPath)) ConfigPath = configFile; XmlDocument doc = new XmlDocument(); try { doc.Load(ConfigPath); } catch { return null; } XmlNode appnode = doc.SelectSingleNode("configuration/gRPC"); if (appnode != null) { foreach (XmlNode channelNode in appnode.ChildNodes) { var name = channelNode.Attributes["name"].InnerText; var address = channelNode.Attributes["address"].InnerText; var channel = new Channel(name, address); result[name] = channel; foreach (XmlNode serviceNode in channelNode.ChildNodes) { if (serviceNode.Name == "service") { name = serviceNode.Attributes["name"].InnerText; var serviceDef = new ServiceDef(name, channel); channel.Services.Add(serviceDef); } } } } return result; }
private static object GetService(ServiceDef def, Type typ)
{
if (def.Instance != null) return def.Instance;
string url = def.Channel.Address;
if(def.Channel.GrpcChannel == null)
{
def.Channel.GrpcChannel = GrpcChannel.ForAddress(url);
}
var obj = Activator.CreateInstance(typ, def.Channel.GrpcChannel);
def.Instance = obj;
return obj;
}
4.5 客户端调用的Get方法
注意,代码中 typename 形如 ServiceRPCClient,Client这6个字符需要去掉才是 proto 文件中定义的 service 的名字。
public static T Get4.6 客户端调用方法() { //创建对象 var typename = typeof(T).Name; var servicename = typename.Remove(typename.Length - 6, 6);//remove "Client" ServiceDef def = null; if (_ServiceDefs == null) { throw new Exception("Config file failed to initialze"); } if (_ServiceDefs.ContainsKey(servicename)) { def = _ServiceDefs[servicename]; } if (def != null) { var obj = GetService(def, typeof(T)); //返回查询到的服务对象 return (T)obj; } else { throw new Exception($"Can not get Service, no service config in config file, type:{typeof(T)}!"); } }
客户端调用只需要一句话,不涉及url,做到了极简。
var service = RPCFactory.Get5. 总结();
在多 gRPC 服务的场景中,服务的 url 和 实例的创建需要硬编码,或者在调用进程的配置文件中配置 url 带来配置碎片化和重复的问题。本文提供了一种简单的 gRPC 服务集中配置和统一创建的方法,使调用服务的客户端只要一句话。
6.补充:调用不了 gRPC http 服务gRPC 服务只提供 http 地址,没有提供 https 地址时,调用报错:
Grpc.Core.RpcException: Status(StatusCode="Internal", Detail="Error starting gRPC call. HttpRequestException: An error occurred while sending the request. IOException: The response ended prematurely.", DebugException="System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> System.IO.IOException: The response ended prematurely.
根据微软官方文档,需要在 GetService 之前(如首次调用GetChannels方法时)加上:
// This switch must be set before creating the GrpcChannel/HttpClient.
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);



