在微服务中,Consul是注册中心,服务提供者、服务消费者等都要注册到Consul中,这样就可以实现服务提供者与服务消费者的隔离。

除了Consul之外,还有Eureka、Zookeeper等类似软件.

用DNS举例来理解Consul的作用:

# Consul 服务器安装

consul 下载地址 https://www.consul.io/

运行 consul.exe agent -dev

这是开发环境测试,生产环境要建集群,要至少一台 Server,多台 Agent。

consul 的监控页面 http://127.0.0.1:8500/

# .Net Core 连接 Consul

使用 Nuget 包管理工具

Install-Package Consul

# Rest 服务的准备

先使用使用默认生成的 ValuesController 做测试 再提供一个 HealthController.cs

[Route("api/[controller]")]
public class HealthController : Controller
{
	[HttpGet]
	public IActionResult Get(){
		return Ok("ok");}
	}
}

服务器从命令行中读取 ip 和端口。

# 让Rest服务注册到 Consul 中

在 Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //......
    app.UseMvc();
    //部署到不同服务器的时候不能写成 127.0.0.1 或者 0.0.0.0
  	//因为这是让服务消费者调用的地址
    String ip = Configuration["ip"];
    int port = int.Parse(Configuration["port"]);
    var client = new ConsulClient(ConfigurationOverview);
    var result = client.Agent.ServiceRegister(new AgentServiceRegistration()
    {
        ID = "WebApplication4" + Guid.NewGuid(),
        Name = "WebApplication4",
        Address = ip,
        Port = port,
        Check = new AgentServiceCheck
        {
            //服务启动多久后注册
            DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),
            //健康检查时间间隔,或者称为心跳间隔
            Interval = TimeSpan.FromSeconds(10),
            //健康检查地址
            HTTP = $"http://{ip}:{port}/api/health",
            Timeout = TimeSpan.FromSeconds(5)
        }
    });
    //...... 
}

private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
    obj.Address = new Uri("http://127.0.0.1:8500");
    obj.Datacenter = "dc1";
}

注意不同实例一定要用不同的 Id,即使是相同服务的不同实例也要用不同的 Id,上面的代码用 Guid 做 Id,确保不重复。相同的服务用相同的 Name。Address、Port 是供服务消费者访问的服务器地址(或者 IP 地址)及端口号。Check 则是做服务健康检查的(解释一下)。

在注册服务的时候还可以通过 AgentServiceRegistration 的 Tags 属性设置额外的标签。

通过命令行启动两个实例

dotnet WebApplication4.dll --ip 127.0.0.1 --port 5001

dotnet WebApplication4.dll --ip 127.0.0.1 --port 5002

# 编写服务消费者

这里用控制台测试,真实项目中服务消费者通产哥也是另外一个 Web 应用。 nuget 安装:Consul、Newtonsoft.Json 首先编写一个 RestTemplate(模仿 Spring Cloud 中的)

public class RestTemplate
{
    private String consulServerUrl;
    public RestTemplate(String consulServerUrl = "http://127.0.0.1:8500")
    {
		this.consulServerUrl = consulServerUrl ;
    }
	/// <summary>
	/// 获取服务的第一个实现地址
	/// </summary>
	/// <param name="consulClient"></param>
	/// <param name="serviceName"></param>
	/// <returns></returns>
	private async Task<String> ResolveRootUrlAsync(String serviceName)
    {
        using (var consulClient = 
               new ConsulClient(c => c.Address = new Uri(consulServerUrl)))
        {
			var services = (await consulClient.Agent.Services()).Response; 
            var agentServices = services.Where(s =>
s.Value.Service.Equals(serviceName, StringComparison.CurrentCultureIgnoreCase)).Select(s => s.Value);
            //TODO:注入负载均衡策略
            var agentService = agentServices.ElementAt(Environment.TickCount %
agentServices.Count());
            //根据当前TickCount对服务器个数取模,“随机”取一个机器出来
            //避免“轮询” 的负载均衡策略需要计数加锁问题
            return agentService.Address + ":" + agentService.Port;
        }
    }
  
    private async Task<String> ResolveUrlAsync(String url)
    {
        Uri uri = new Uri(url);
        String serviceName = uri.Host;
        String realRootUrl = await ResolveRootUrlAsync(serviceName);
        return uri.Scheme + "://" + realRootUrl + uri.PathAndQuery;
    }
  
    public async Task<ResponseEntity<T>> GetForEntityAsync<T>(String url, HttpRequestHeaders requestHeaders = null)
    {
        using (HttpClient httpClient = new HttpClient())
        {
            HttpRequestMessage requestMsg = new HttpRequestMessage(); 
            if (requestHeaders != null)
            {
                foreach (var header in requestHeaders)
                {
                    requestHeaders.Add(header.Key, header.Value);
                }
            }
            requestMsg.Method = HttpMethod.Get;
            requestMsg.RequestUri = new Uri(await ResolveUrlAsync(url));
            var result = await httpClient.SendAsync(requestMsg); 
            ResponseEntity<T> respEntity = new ResponseEntity<T>(); 
            respEntity.StatusCode = result.StatusCode;
            String bodyStr = await result.Content.ReadAsStringAsync(); 
            respEntity.Body = JsonConvert.DeserializeObject<T>(bodyStr);
            respEntity.Headers = respEntity.Headers;
            return respEntity;
        }
    }
}
	class ResponseEntity<T>
	{
    	public HttpStatusCode StatusCode { get; set; }
    	public T Body { get; set; }
    	public HttpResponseHeaders Headers { get; set; }
	} 
}

编写控制台:

using Consul;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            RestTemplate rest = new RestTemplate();
            var data = 
           rest.GetForEntityAsync<string[]>"http://WebApplication4/api/Values/").Result;
            Console.WriteLine(data.StatusCode);
            Console.WriteLine(string.Join(",", data.Body));
            Console.ReadKey();
        }
    }
}

解析RestTemplate代码。主要作用:

    1. 根据url到Consul中根据服务的名字解析获取一个服务实例,把路径转换为实际连接的服务器;

负载均衡,这里用的是简单的随机负载均衡,这样服务的消费者就不用自己指定要访问那个服务提供 者了,解耦、负载均衡。

    1. 负载均衡还可以根据权重随机(不同服务器的性能不一样,这样注册服务的时候通过Tags来区

分),还可以根据消费者IP地址来选择服务实例(涉及到一致性Hash的优化)等。

    1. RestTemplate还负责把响应的json反序列化返回结果。

# 简化服务的注册

每次启动、注册服务都要指定一个端口,本地测试集群的时候可能要启动多个实例,很麻烦。 在 ASP.Net Core 中只要设定端口为 0,那么服务器会随机找一个可用的端口绑定(测试一下)。 但是没有找到读取到这个随机端口号的方法。 因此自己写:

/// <summary>
/// 产生一个介于 minPort-maxPort 之间的随机可用端口
/// </summary>
/// <param name="minPort"></param>
/// <param name="maxPort"></param>
/// <returns></returns>
public static int GetRandAvailablePort(int minPort=1024,int maxPort = 65535)
{
    Random rand = new Random();
    while(true)
    {
    	int port = rand.Next(minPort, maxPort);
		if (!IsPortInUsed(port))
		{
    		return port;
		}
	} 
}

/// <summary>
/// 判断 port 端口是否在使用中
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static bool IsPortInUsed(int port)
{
    //using System.Net.NetworkInformation;
    IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
	IPEndPoint[] ipsTCP = ipGlobalProperties.GetActiveTcpListeners();
	if(ipsTCP.Any(p=>p.Port==port))
	{
    	return true;
	}
	IPEndPoint[] ipsUDP = ipGlobalProperties.GetActiveUdpListeners(); 
 	if (ipsUDP.Any(p => p.Port == port))
	{
		return true;
    }
	TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpC
onnections();
	if (tcpConnInfoArray.Any(conn => conn.LocalEndPoint.Port == port))
	{
		return true;
    }
	return false;
}

在程序启动的时候如果 port=0 或者没有指定 port,则自己调用 GetRandAvailablePort 获取可 用端口。

如鹏网《Net微服务》-by杨中科

上次更新: 2019年 12月 28日 15:53:50