.net gRPC 服务的配置、加载

目标

    在调用 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);
    }    
}

    上述写法的弊端:

  1.  代码中需要出现url,url即便通过配置文件加载,当服务多起来后也会配置重复化、碎片化;
  2.  代码结构高度一致,冗余度高;
  3.  不容易保证 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 服务集中配置和统一创建的方法,使调用服务的客户端只要一句话。

上一篇:java版gRPC实战之四:客户端流


下一篇:docker容器的创建与管理过程