Xây dựng Microservices bằng ASP.NET Core – Giao tiếp máy chủ thời gian thực với SignalR và RabbitMQ

Xây dựng Microservices bằng ASP.NET Core – Giao tiếp máy chủ thời gian thực với SignalR và RabbitMQ

Danh sách các bài viết:

  • Phần 1: Lập kế hoạch.
  • Phần 2: Định hình kiến ​​trúc Microservices với CQRS và MediatR.
  • Phần 3: Khám phá dịch vụ với Eureka.
  • Phần 4: Xây dựng API Gateway với Ocelot.
  • Phần 5: Thao tác với database PostgreSQL bằng Marten.
  • Phần 6: Giao tiếp máy chủ thời gian thực với SignalR và RabbitMQ (bài viết này).
  • Phần 7: Transaction outbox với RabbitMQ.

Trong bài viết này, chúng tôi sẽ chỉ cho bạn cách bạn có thể kết hợp SignalR và RabbitMQ để xây dựng giao tiếp máy chủ-máy khách thời gian thực.

Chúng ta sẽ mở rộng cổng thông tin bán bảo hiểm của mình với dịch vụ chat. Dịch vụ chat này sẽ cho phép các đại lý bảo hiểm giao tiếp với nhau.

Chúng ta cũng sẽ sử dụng dịch vụ chat này để gửi cho người dùng thông tin về một số sự kiện kinh doanh nhất định như có sản phẩm mới, bán thành công hoặc sản phẩm bảo hiểm hoặc thay đổi thuế.

Mã nguồn cho giải pháp hoàn chỉnh có thể được tìm thấy trên GitHub của chúng tôi.

Chúng ta sẽ xây dựng cái gì?

Đối với hệ thống bán bảo hiểm dựa trên kiến trúc microservices hiện có của chúng ta, chúng ta sẽ thêm một dịch vụ chat.

Dịch vụ chat này sẽ có hai chức năng:

  • Cho phép các đại lý bảo hiểm nói chuyện với nhau bằng ứng dụng của chúng ta.
  • Cho phép hệ thống của chúng ta gửi thông báo khi sự kiện kinh doanh quan trọng xảy ra, ví dụ: giới thiệu sản phẩm mới hoặc thuế mới, bán thành công, tính toán hoa hồng hoặc các khoản thanh toán phải thu.
Kiến trúc hệ thống

Chúng ta bắt đầu với ứng dụng ASP.NET Core Web API thông thường. Như thường lệ, chúng ta sẽ thêm MediatR. Trước khi chúng ta có thể bắt đầu triển khai dịch vụ chat, chúng ta cần bảo mật dịch vụ của mình và thiết lập CORS.

Để đảm bảo quyền truy cập vào dịch vụ chat, chúng ta sẽ sử dụng phương thức bảo mật dựa trên token JWT.

Chúng ta cần thiết lập nó trong lớp Startup bằng cách thêm mã vào các phương thức ConfigureConfigureServices. Dưới đây là đoạn mã hiển thị các phần cấu hình chính:

var appSettings = appSettingsSection.Get();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);

services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;    
    })
    .AddJwtBearer(x =>
    {
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateActor = false
        };
        x.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];

                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/agentsChat")))
                {
                    context.Token = accessToken;
                }
                
                return Task.CompletedTask;    
            }
        };
    });

Đây là thiết lập JWT điển hình với secret được lưu trữ trong tệp cấu hình. Điều bất thường duy nhất là phương thức OnMessageReceived cần thiết để SignalR hoạt động bình thường (bạn có thể tìm thêm thông tin tại đây).

Điều tiếp theo là thiết lập CORS. Với .NET Core phiên bản mới, chúng ta có một sự thay đổi đột phá ở đây. Chúng ta không còn được phép có API public (API có AllowAnyOrigin) kết hợp với AllowCredentials.

Đó là lý do tại sao chúng ta cần chỉ định tất cả các máy khách được phép của chúng ta trong cấu hình. Với các máy khách được liệt kê trong cấu hình:

"AppSettings": {
    "AllowedChatOrigins" : ["http://localhost:8080"]
}

Bây giờ chúng ta có thể cấu hình CORS:

services.AddCors(opt => opt.AddPolicy("CorsPolicy",
    builder =>
    {
        Builder
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials()
            .WithOrigins(appSettingsSection.Get().AllowedChatOrigins);
    }
));

Xây dựng chat service with SignalR

SignalR là gì?

SignalR là một thư viện cho phép các nhà phát triển .NET thêm giao tiếp thời gian thực vào các ứng dụng web. Nó có khả năng nhận tin nhắn từ các máy khách được kết nối và gửi thông báo từ máy chủ cho máy khách.

Ngoài việc triển khai phía máy chủ, bạn có thể tìm thấy triển khai máy khách cho hầu hết các nền tảng phổ biến: JavaScript, Java, .NET và .NET Core.

Thêm SignalR vào project

Bây giờ chúng ta có thể thêm SignalR vào project của mình. Bước đầu tiên là thêm nó vào phương thức ConfigureServices của lớp Startup:

services.AddSignalR();

Tiếp theo chúng ta cần thêm một Hub, đó là một điểm giao tiếp trung tâm giữa máy chủ và máy khách. Điều này khá đơn giản. Bạn chỉ cần thêm một lớp kế thừa từ lớp Hub như sau:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class AgentChatHub : Hub
{
}

Chúng ta cũng thêm attribute Authorize và chỉ định lược đồ xác thực SignalR sẽ sử dụng.

Chúng ta cũng cần cung cấp một dịch vụ sẽ cung cấp cho SignalR “username” dựa trên người dùng hiện tại. Với mục đích này, chúng ta cần triển khai interface IUserIdProvider và đăng ký triển khai này như sau.

public class NameUserIdProvider : IUserIdProvider
{
    public string GetUserId(HubConnectionContext connection)
    {
        return connection.User?.Identity?.Name;
    }
}

//Startup.cs ConfigureServices
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();

Bước cuối cùng là thêm ánh xạ URL cho Hub của chúng ta. Chúng ta cần thêm nó vào phương thức Configure của lớp Startup:

app.UseSignalR(routes =>
{
    routes.MapHub("/agentsChat");
});

Bây giờ Hub của chúng ta đã có thể được truy cập từ thế giới bên ngoài bởi các máy khách cung cấp mã thông báo JWT hợp lệ và được liệt kê trong cài đặt cấu hình AllowedChatOrigins.

SignalR Hub

SignalR Hub cho phép máy khách gửi tin nhắn đến máy chủ và ngược lại. Hub duy trì danh sách các máy khách đã kết nối và cho phép mã phía máy chủ “thực thi” các phương thức trên máy khách.

Chúng ta sẽ thêm phương thức SendMessage vào lớp AgentChatHub. Phương thức này sẽ được gọi bằng ứng dụng SPA của máy khách khi một trong những nhân viên muốn gửi tin nhắn cho tất cả người dùng hiện đang được kết nối.

public async Task SendMessage(string message)
{
    var avatar = Context.User?.Claims?.FirstOrDefault(c=>c.Type=="avatar")?.Value;
            
    await Clients.All.SendAsync("ReceiveMessage", Context.User?.Identity?.Name, avatar, message);
}

Phương thức này trích xuất một số thông tin từ dữ liệu người dùng được xác thực và gọi phương thức ReceiveMessage tất cả các máy khách, chuyển tên người dùng, hình đại diện và tin nhắn.

Hub cũng phản ứng với các sự kiện như người dùng đã kết nối hoặc ngắt kết nối. Dưới đây bạn có thể tìm thấy các phương thức đơn giản để gửi thông tin về các sự kiện như vậy cho tất cả người dùng.

public override async Task OnConnectedAsync()
{
    await Clients.Others.SendAsync("ReceiveNotification", $"{Context.User?.Identity?.Name} join chat");
}

public override async Task OnDisconnectedAsync(Exception exception)
{
    await Clients.Others.SendAsync("ReceiveNotification", $"{Context.User?.Identity?.Name} left chat");
}

Triển khai phía máy khách với VueJS

Để sử dụng SignalR từ VueJS chúng ta cần cài đặt package thích hợp bằng npm.

"dependencies": {
    "@aspnet/signalr": "^1.1.2",
    //…
}

Bây giờ chúng ta có thể thêm thao tác gửi tin nhắn và xử lý tin nhắn mới từ máy chủ. Hầu hết các mã thú vị nằm trong component Chat.vue.

Chúng ta cần kết nối với Hub. Điều này xảy ra trong trình xử lý created:

this.hubConnection
    .start()
    .then(()=>console.info("connected to hub"))
    .catch(err => console.error(err));

Chúng ta cũng cần đăng ký listener cho các sự kiện được gửi bởi máy chủ:

this.hubConnection.on("ReceiveMessage",(usr, avatar,msg) => {
    this.appendMsgToChat(usr,avatar,msg);
});

this.hubConnection.on("ReceiveNotification", msg => {
    this.appendAlertToChat(msg);
});

Phần bị thiếu duy nhất là một phương thức để gửi tin nhắn đến Hub của chúng ta:

send() {       
    this.hubConnection.invoke("SendMessage", this.message);
    this.message = '';
}

Dưới đây là ảnh chụp màn hình từ một ứng dụng đang chạy.

Màn hình Chat trên ứng dụng

Triển khai Pub-sub với RabbitMQ

RabbitMQ là một message broker mã nguồn mở, nhẹ, dễ sử dụng và vận hành. Chúng tôi sử dụng nó trong nhiều dự án trong ít nhất 6 năm. Nó có thể được sử dụng để tích hợp các thành phần hệ thống không đồng bộ với mẫu pub-sub hoặc cũng có thể được sử dụng như một cơ chế RPC.

Để sử dụng RabbitMQ với .NET Core, chúng tôi khuyên bạn nên thư viện RawRabbit.

Trước khi chúng ta bắt đầu triển khai giải pháp pub-sub của mình, chúng ta phải cài đặt RabbitMQ. Hướng dẫn cài đặt cho hầu hết các hệ điều hành phổ biến có thể được tìm thấy trên trang tài liệu chính thức.

Sau khi bạn đã cài đặt RabbitMQ và chạy nó lên, chúng ta có thể bắt đầu thực hiện giải pháp của chúng ta.

Xuất bản sự kiện

Chúng ta sẽ thêm chức năng xuất bản sự kiện miền vào PolicyService. Khi ai đó mua hoặc chấm dứt chính sách, chúng ta sẽ gửi tin nhắn đến RabbitMQ để các microservices khác quan tâm đến các sự kiện như vậy có thể phản ứng. Ví dụ: PaymentService có thể tạo tài khoản cho chính sách vừa được bán và đóng tài khoản, khi chính sách bị chấm dứt.

Trong bài viết này, chúng ta sẽ thấy cách ChatService có thể đăng ký sự kiện chính sách mới được bán và cách nó sử dụng SignalR để thông báo cho người dùng hiện đang đăng nhập về trường hợp đó.

Để thực hiện xuất bản sự kiện với RawRabbit, chúng ta cần thêm các gói NuGet sau.

<PackageReference Include="RawRabbit.DependencyInjection.ServiceCollection" Version="2.0.0-rc5" />
<PackageReference Include="RawRabbit.Operations.Tools" Version="2.0.0-rc5" />

Bây giờ chúng ta có thể cấu hình thư viện. Chúng ta đóng gói cấu hình trong lớp RawRabbitInstaller.

services.AddRawRabbit(new RawRabbitOptions
{
    ClientConfiguration = new RawRabbit.Configuration.RawRabbitConfiguration
    {
        Username = "guest",
        Password = "guest",
        VirtualHost = "/",
        Port = 5672,
        Hostnames = new List {"localhost"},
        RequestTimeout = TimeSpan.FromSeconds(10),
        PublishConfirmTimeout = TimeSpan.FromSeconds(1),
        RecoveryInterval = TimeSpan.FromSeconds(1),
        PersistentDeliveryMode = true,
        AutoCloseConnection = true,
        AutomaticRecovery = true,
        TopologyRecovery = true,
        Exchange = new RawRabbit.Configuration.GeneralExchangeConfiguration
        {
            Durable = true,
            AutoDelete = false,
            Type = RawRabbit.Configuration.Exchange.ExchangeType.Topic
        },
        Queue = new RawRabbit.Configuration.GeneralQueueConfiguration
        {
            Durable = true,
            AutoDelete = false,
            Exclusive = false
        }
    }
});

Ở đây chúng ta cấu hình địa chỉ broker, thông tin đăng nhập để kết nối, exchange và queue cài đặt mặc định. Đối với giải pháp production, chúng ta nên di chuyển hầu hết các params này sang file cấu hình hệ thống appsettings.json.

Sau đó, chúng ta có thể đăng ký triển khai publisher, sẽ được sử dụng trong code của chúng ta.

services.AddSingleton<IEventPublisher, RabbitEventPublisher>();

Tất nhiên chúng ta phải thêm dịch vụ vào phương thức ConfigureServices của lớp Startup:

services.AddRabbit();

Và đây là mã publisher:

public class RabbitEventPublisher : IEventPublisher
{
    private readonly IBusClient busClient;

    public RabbitEventPublisher(IBusClient busClient)
    {
        this.busClient = busClient;
    }

    public Task PublishMessage(T msg)
    {
        return busClient.BasicPublishAsync(msg, cfg => {
            cfg.OnExchange("lab-dotnet-micro").WithRoutingKey(typeof(T).Name.ToLower());
        });
    }
}

Nó chấp nhận busClient là một phần của thư viện RawRabbit và sử dụng nó để gửi tin nhắn đến exchange có tên lab-dotnet-micro, với khóa định tuyến (routing key) là tên của lớp tin nhắn và nội dung là instance của kiểu tin nhắn.

Ví dụ: chúng ta có lớp PolicyCreated đại diện cho sự kiện bán chính sách mới. Nếu chúng ta gọi phương thức PublishMessage(new PolicyCreated { … }), một tin nhắn với khóa định tuyến là PolicyCreated và nội dung là JSON của PolicyCreated sẽ được gửi đến exchange lab-dotnet-micro.

Bây giờ chúng ta có thể sửa đối CreatePolicyHandlerđể gửi sự kiện khi chính sách mới được bán.

Chúng ta cần thêm sự phụ thuộc vào IEventPublisher và sử dụng nó để gửi sự kiện.

public async Task Handle(CreatePolicyCommand request, CancellationToken cancellationToken)
{
    using (var uow = uowProvider.Create())
    {
        var offer = await uow.Offers.WithNumber(request.OfferNumber);
        var customer =  …;
        var policy = offer.Buy(customer);

        uow.Policies.Add(policy);
        await uow.CommitChanges();

        await eventPublisher.PublishMessage(PolicyCreated(policy));

        return new CreatePolicyResult
        {
            PolicyNumber = policy.Number
        };    
    }
}

private PolicyCreated PolicyCreated(Policy policy)
{
    return new PolicyCreated
    {
        PolicyNumber = policy.Number,
        //…
    };
}

Lắng nghe các sự kiện và gửi tin nhắn SignalR cho người dùng

Bây giờ, khi phần xuất bản đã sẵn sàng, chúng ta có thể triển khai listener cho sự kiện PolicyCreated trong lớp ChatService.

Để bắt đầu, chúng ta cần thêm các gói phụ thuộc NuGet sau vào project ChatService.csproj.

<PackageReference Include="RawRabbit.DependencyInjection.ServiceCollection" Version="2.0.0-rc5" />
<PackageReference Include="RawRabbit.Operations.Subscribe" Version="2.0.0-rc5" />

Bây giờ chúng ta có thể cấu hình kết nối với RabbitMQ và cấu hình listener. Cấu hình kết nối giống như publisher. Bạn có thể tìm thấy nó trong lớp RawRabbitInstaller của ChatService.

Trước khi chúng ta đi vào chi tiết về cách chúng ta sẽ triển khai đăng ký tin nhắn, chúng ta cần nói một chút về các nguyên tắc kiến trúc trong giải pháp của mình. Cho đến nay, chúng ta đã sử dụng các trình xử lý command và query như một cái gì đó gói gọn logic miền và đưa nó ra thế giới bên ngoài. Các command và query này được đưa ra bởi controller của Web API.

Kiến trúc của chúng ta dựa trên MediatR. Vì sự nhất quán, chúng ta cũng sẽ sử dụng MediatR để xử lý tin nhắn. MediatR có định nghĩa interface INotification INotificationHandler.INotification được sử dụng để đánh dấu các lớp sự kiện hoặc tin nhắn và INotificationHandlerđược sử dụng để triển khai trình xử lý cho loại thông báo đã cho (loại sự kiện / tin nhắn).

Như bạn có thể đã thấy lớp PolicyCreated được đánh dấu bằng interface INotification.

Dưới đây, bạn có thể thấy trình xử lý cho thông báo PolicyCreated.

public class PolicyCreatedHandler : INotificationHandler
{
    private readonly IHubContext chatHubContext;

    public PolicyCreatedHandler(IHubContext chatHubContext)
    {
        this.chatHubContext = chatHubContext;
    }

    public async Task Handle(PolicyCreated notification, CancellationToken cancellationToken)
    {
        await chatHubContext.Clients.All.SendAsync("ReceiveNotification",$"{notification.AgentLogin} just sold policy for {notification.ProductCode}!!!");
    }
}

Ở đây bạn có thể thấy rằng chúng ta sử dụng instance củaAgentChatHub được inject để gửi tin nhắn cho tất cả người dùng được kết nối mà đại lý có đăng nhập đã bán một chính sách thuộc loại nhất định.

Bằng cách này, chúng ta kết hợp SignalR với thông điệp đến từ các microservices khác. Nhưng chúng ta vẫn không thấy cách trình xử lý thông báo MediatR xử lý tin nhắn RabbitMQ.

Hãy phân tích lớp RabbitEventListenervà RabbitListenersInstaller. Hãy xem phương thức ListenTo của RabbitEventListener làm gì:

public void ListenTo(List eventsToSubscribe)
{
    foreach (var eventType in eventsToSubscribe)
    {
        this.GetType()
            .GetMethod("Subscribe", 
                System.Reflection.BindingFlags.NonPublic |
                System.Reflection.BindingFlags.Instance)
            .MakeGenericMethod(eventType)
            .Invoke(this, new object[] { });
    }
}

Phương thức này lấy một danh sách các kiểu đại diện cho các tin nhắn mà chúng ta muốn nghe. Chúng ta duyệt qua danh sách này và đối với từng kiểu dữ liệu, chúng ta gọi phương thức Subscribe<T>. Đây là nơi công việc thực tế được thực hiện.

private void Subscribe<T>() where T : INotification
{
    this.busClient.SubscribeAsync(
    async (msg) =>
    {
        using (var scope = serviceProvider.CreateScope())
        {
            var internalBus = scope.ServiceProvider.GetRequiredService();
            await internalBus.Publish(msg);
        }
    },
    cfg => cfg.UseSubscribeConfiguration(
        c => c.OnDeclaredExchange(e => e
            .WithName("lab-dotnet-micro")
            .WithType(RawRabbit.Configuration.Exchange.ExchangeType.Topic)
            .WithArgument("key", typeof(T).Name.ToLower()))
            .FromDeclaredQueue(q => q.WithName("lab-chat-service-" + typeof(T).Name)))
    );
}

Ở đây chúng ta kết nối với exchange lab-dotnet-micro và khai báo một queue sẽ thu thập tin nhắn với khóa định tuyến bằng tên kiểu sự kiện. Tên queue là lab-chat-service cộng với tên kiểu sự kiện.

Khi tin nhắn có khóa như vậy đến exchange, nó được đặt trong queue của chúng ta. Khi ChatService của chúng ta hoạt động, nó sẽ nhận được tin nhắn này và thực thi.

Mã này được IMediator giải quyết và sử dụng nó để định tuyến đến INotificationHandler thích hợp đã đăng ký.

Hãy nhớ rằng chúng ta sử dụng tích hợp MediatR với dependency injection của ASP.NET Core để chúng ta không cần thực hiện bất kỳ bước bổ sung nào (ngoài gọi services.AddMediatR() trong lớp Startup) để đăng ký PolicyCreatedHandler của chúng tôi.

Vì vậy, chúng ta chỉ cần đăng ký các kiểu tin nhắn mà chúng ta muốn xử lý để RabbitEventListener sẽ khởi tạo một queue và bắt đầu đăng ký.

Chúng ta làm điều đó bằng cách thêm code sau vào phương thức Configure của lớp Startup:

app.UseRabbitListeners(new List<Type> {typeof(PolicyCreated)});

Bằng cách này nếu bạn muốn xử lý một số kiểu tin nhắn mới, ví dụ: PolicyTerminated, ProductActivated, tất cả những gì chúng ta phải làm sẽ là:

  • Triển khai INotificationHandler cho kiểu tin nhắn đó.
  • Thêm nó vào danh sách các kiểu được truyền vào phương thức UseRabbitListeners.

Tóm tắt

Như bạn có thể thấy với một chút trợ giúp của SignalR và RabbitMQ, chúng ta có thể thực hiện chức năng khá tinh vi. Kết hợp MadiatR với các thành phần được đề cập trước đó giúp mã business logic của chúng ta tránh xa các mối quan tâm về cơ sở hạ tầng và giúp chúng ta giữ kiến trúc sạch sẽ cho toàn bộ giải pháp.

Chúng ta cũng đã thêm một dịch vụ tuyệt vời vào giải pháp của chúng ta. Bây giờ đại lý bảo hiểm của chúng ta có thể nói chuyện với nhau và họ cũng nhận được thông báo theo thời gian thực về các sự kiện kinh doanh khác nhau.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *