C ා httpclient uses Consul discovery service

Keywords: JSON github

Tried Overt.Core.Grpc The use of GRPC is transformed to WCF, and the performance test is also very good. It is highly recommended for you to use
But most of the existing projects are http requests. If they are transformed into GRPC, the workload will be heavy, so we found Steeltoe.Discovery Add DelegatingHandler to HttpClient in Startup, dynamically change the host and port in the request url, and point the http request to the service instance discovered by consumer, thus realizing the dynamic discovery of the service
After performance testing, Steeltoe.Discovery Only Overt.Core.Grpc 20%, which is very difficult to accept, so I implemented a set of service discovery tools based on consumer. Well, the name is so hard to choose, tentatively ConsulDiscovery.HttpClient bar
The function is simple:


  1. The webapi reads the configuration information from json, ConsulDiscoveryOptions;
  2. If you are a service, register yourself in the consumer and set the health check Url;
  3. ConsulDiscovery.HttpClient There is a consumer client to refresh the url access addresses of all services regularly

 

Compare the two core classes

using Consul;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace ConsulDiscovery.HttpClient
{
    public class DiscoveryClient : IDisposable
    {
        private readonly ConsulDiscoveryOptions consulDiscoveryOptions;
        private readonly Timer timer;
        private readonly ConsulClient consulClient;
        private readonly string serviceIdInConsul;

        public Dictionary<string, List<string>> AllServices { get; private set; } = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);


        public DiscoveryClient(IOptions<ConsulDiscoveryOptions> options)
        {
            consulDiscoveryOptions = options.Value;
            consulClient = new ConsulClient(x => x.Address = new Uri($"http://{consulDiscoveryOptions.ConsulServerSetting.IP}:{consulDiscoveryOptions.ConsulServerSetting.Port}"));
            timer = new Timer(Refresh);

            if (consulDiscoveryOptions.ServiceRegisterSetting != null)
            {
                serviceIdInConsul = Guid.NewGuid().ToString();
            }
        }

        public void Start()
        {
            var checkErrorMsg = CheckParams();
            if (checkErrorMsg != null)
            {
                throw new ArgumentException(checkErrorMsg);
            }
            RegisterToConsul();
            timer.Change(0, consulDiscoveryOptions.ConsulServerSetting.RefreshIntervalInMilliseconds);
        }

        public void Stop()
        {
            Dispose();
        }

        private string CheckParams()
        {
            if (string.IsNullOrWhiteSpace(consulDiscoveryOptions.ConsulServerSetting.IP))
            {
                return "Consul server address ConsulDiscoveryOptions.ConsulServerSetting.IP Cannot be empty";
            }

            if (consulDiscoveryOptions.ServiceRegisterSetting != null)
            {
                var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting;
                if (string.IsNullOrWhiteSpace(registerSetting.ServiceName))
                {
                    return "Service name ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceName Cannot be empty";
                }
                if (string.IsNullOrWhiteSpace(registerSetting.ServiceIP))
                {
                    return "Service address ConsulDiscoveryOptions.ServiceRegisterSetting.ServiceIP Cannot be empty";
                }
            }
            return null;
        }

        private void RegisterToConsul()
        {
            if (string.IsNullOrEmpty(serviceIdInConsul))
            {
                return;
            }

            var registerSetting = consulDiscoveryOptions.ServiceRegisterSetting;
            var httpCheck = new AgentServiceCheck()
            {
                HTTP = $"{registerSetting.ServiceScheme}{Uri.SchemeDelimiter}{registerSetting.ServiceIP}:{registerSetting.ServicePort}/{registerSetting.HealthCheckRelativeUrl.TrimStart('/')}",
                Interval = TimeSpan.FromMilliseconds(registerSetting.HealthCheckIntervalInMilliseconds),
                Timeout = TimeSpan.FromMilliseconds(registerSetting.HealthCheckTimeOutInMilliseconds),
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10),
            };
            var registration = new AgentServiceRegistration()
            {
                ID = serviceIdInConsul,
                Name = registerSetting.ServiceName,
                Address = registerSetting.ServiceIP,
                Port = registerSetting.ServicePort,
                Check = httpCheck,
                Meta = new Dictionary<string, string>() { ["scheme"] = registerSetting.ServiceScheme },
            };
            consulClient.Agent.ServiceRegister(registration).Wait();
        }

        private void DeregisterFromConsul()
        {
            if (string.IsNullOrEmpty(serviceIdInConsul))
            {
                return;
            }
            try
            {
                consulClient.Agent.ServiceDeregister(serviceIdInConsul).Wait();
            }
            catch
            { }
        }

        private void Refresh(object state)
        {
            Dictionary<string, AgentService>.ValueCollection serversInConsul;
            try
            {
                serversInConsul = consulClient.Agent.Services().Result.Response.Values;
            }
            catch // (Exception ex)
            {
                // If connected consul error, Do not update service list. Continue to use the list of previously acquired services
                // But if you can't connect for a long time consul, Some instances in the service list are no longer available, It doesn't make sense to keep providing such an old list, So do you want to have a health check here? In that case, You have to change the check address to something that can't be set
                return;
            }

            // 1. Update service list
            // 2. If this program provides services, And test the service Id Is it in the service list
            var tempServices = new Dictionary<string, HashSet<string>>();
            bool needReregisterToConsul = true;
            foreach (var service in serversInConsul)
            {
                var serviceName = service.Service;
                if (!service.Meta.TryGetValue("scheme", out var serviceScheme))
                {
                    serviceScheme = Uri.UriSchemeHttp;
                }
                var serviceHost = $"{serviceScheme}{Uri.SchemeDelimiter}{service.Address}:{service.Port}";
                if (!tempServices.TryGetValue(serviceName, out var serviceHosts))
                {
                    serviceHosts = new HashSet<string>();
                    tempServices[serviceName] = serviceHosts;
                }
                serviceHosts.Add(serviceHost);

                if (needReregisterToConsul && !string.IsNullOrEmpty(serviceIdInConsul) && serviceIdInConsul == service.ID)
                {
                    needReregisterToConsul = false;
                }
            }

            if (needReregisterToConsul)
            {
                RegisterToConsul();
            }

            var tempAllServices = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
            foreach (var item in tempServices)
            {
                tempAllServices[item.Key] = item.Value.ToList();
            }
            AllServices = tempAllServices;
        }


        public void Dispose()
        {
            DeregisterFromConsul();
            consulClient.Dispose();
            timer.Dispose();
        }
    }
}
View Code

 

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace ConsulDiscovery.HttpClient
{
    public class DiscoveryHttpMessageHandler : DelegatingHandler
    {
        private static readonly Random random = new Random((int)DateTime.Now.Ticks);

        private readonly DiscoveryClient discoveryClient;

        public DiscoveryHttpMessageHandler(DiscoveryClient discoveryClient)
        {
            this.discoveryClient = discoveryClient;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (discoveryClient.AllServices.TryGetValue(request.RequestUri.Host, out var serviceHosts))
            {
                if (serviceHosts.Count > 0)
                {
                    var index = random.Next(serviceHosts.Count);
                    request.RequestUri = new Uri(new Uri(serviceHosts[index]), request.RequestUri.PathAndQuery);
                }
            }
            return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
    }
}
View Code

 

How to use

For simplicity, I added a HelloController for the new WebApi, provided the SayHelloService service, and registered myself with Consul

When we access the WebApi's / WeatherForecast, its Get() method accesses the http://SayHelloService/Hello/NetCore , which is equivalent to a remote call, just calling the WebApi's / hello / NETCORE

  1.  appsettings.json increase

"ConsulDiscoveryOptions": {
    "ConsulServerSetting": {
      "IP": "127.0.0.1", // Required
      "Port": 8500, // Required
      "RefreshIntervalInMilliseconds": 1000
    },
    "ServiceRegisterSetting": {
      "ServiceName": "SayHelloService", // Required
      "ServiceIP": "127.0.0.1", // Required
      "ServicePort": 5000, // Required
      "ServiceScheme": "http", // It can only be http perhaps https, default http, 
      "HealthCheckRelativeUrl": "/HealthCheck",
      "HealthCheckIntervalInMilliseconds": 500,
      "HealthCheckTimeOutInMilliseconds": 2000
    }
  }

 

2. Modification Startup.cs

using ConsulDiscovery.HttpClient;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

namespace WebApplication1
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            // register ConsulDiscovery Related configuration
            services.AddConsulDiscovery(Configuration);
            // to configure SayHelloService Of HttpClient
            services.AddHttpClient("SayHelloService", c =>
                {
                    c.BaseAddress = new Uri("http://SayHelloService");
                })
                .AddHttpMessageHandler<DiscoveryHttpMessageHandler>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            // start-up ConsulDiscovery
            app.StartConsulDiscovery(lifetime);
        }
    }
}

 

3. Add HelloController

using Microsoft.AspNetCore.Mvc;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HelloController : ControllerBase
    {
        [HttpGet]
        [Route("{name}")]
        public string Get(string name)
        {
            return $"Hello {name}";
        }
    }
}

 

4. modify the WeatherForecast

using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.Threading.Tasks;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly IHttpClientFactory httpClientFactory;

        public WeatherForecastController(IHttpClientFactory httpClientFactory)
        {
            this.httpClientFactory = httpClientFactory;
        }

        [HttpGet]
        public async Task<string> Get()
        {
            var httpClient = httpClientFactory.CreateClient("SayHelloService");
            var result = await httpClient.GetStringAsync("Hello/NetCore");
            return $"WeatherForecast return:           {result}";
        }
    }
}

 

5. Start consumer

consul agent -dev

 

6. Start WebApplication1 and access http://localhost:5000/weatherforecast

 

 

 

 

 

The above example can go to https://github.com/zhouandke/ConsulDiscovery.HttpClient Download, remember to start the consumer: consumer agent - Dev

 

  End

Posted by Lessur on Tue, 26 May 2020 07:42:24 -0700