目标
在调用 gRPC 服务时,避免重复写FromAddress(url)代码,而是通过配置文件反射加载 gRPC 服务。
硬编码写法
假设我们在 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,服务的注册通过修改配置文件完成,不涉及服务发现、高可用性、监控等。
服务的可配置加载
配置对象
分为 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<ServiceDef>();
}
public string Name { get; }
public string Address { get; }
public List<ServiceDef> Services { get; }
public GrpcChannel GrpcChannel { get; set; }
}
配置文件格式
这里我采用了 xml 文件,用 json 也一样。
<?xml version="1.0" encoding="utf-8"?>
<gRPC>
<channel name = "Process" address="http://localhost:5000">
<service name="ServiceRPC"/>
<service name="ServiceRPC2"/>
</channel>
</gRPC>
</configuration>
XML配置文件读取
private static Dictionary<string, Channel> GetChannels(string configFile)
{
Dictionary<string, Channel> result = new Dictionary<string, Channel>();
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;
}
客户端调用的Get方法
注意,代码中 typename 形如 ServiceRPCClient,Client这6个字符需要去掉才是 proto 文件中定义的 service 的名字。
public static T Get<T>()
{
//创建对象
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.Get<ServiceRPC.ServiceRPCClient>();
总结
在多 gRPC 服务的场景中,服务的 url 和 实例的创建需要硬编码,或者在调用进程的配置文件中配置 url 带来配置碎片化和重复的问题。本文提供了一种简单的 gRPC 服务集中配置和统一创建的方法,使调用服务的客户端只要一句话。