Tuesday, June 25, 2024

SignalR ve WebSockets Transport Kullanımı

SignalR'ın client ve server durumuna göre transport metodlarından en uygun hangisi ise onu seçtiğini biliyoruz. Ancak geliştirme ortamında projemin tam olarak benim istediğim şekilde anlık sonuç vermemesi istediğim bir şey değil. Bu noktadan itibaren önceki yazıyı baz alarak devam edeceğim.

TrackHub sınıfında override ettiğim OnDisconnectedAsync metoduna breakpoint koyduğumda, client kapatıldığında bu metoda düşmesi. Ancak OnDisconnectedAsync metotu tetiklenmiyor. Bu sebeple anlık olarak bir değişme var mı, göremiyorum.

Geliştirme ortamında projemi geliştirirken farkettiğim şey SignalR'ın transport metotu olarak Long Polling kullanmasıydı.

public override async Task OnConnectedAsync()
{
    var transportType = Context.Features.Get<IHttpTransportFeature>().TransportType;
    ...
}
Bu durum beklenen bir şey, ancak beklentim tarayıcım modern bir tarayıcı olduğu için ve muhtemelen asp.net core api projemin çalışması için sunulan IIS Express'de WebSockets transport'u destekliyor diye, WebSockets'i ilk olarak tercih eder demiştim, öyle olmuyormuş.

Transport metodu olarak Long Polling kullanıldığı için yapılan işlemleri anlık olarak göremiyorum. WebSockets ile Long Polling iki transport metodu olarak aralarında ciddi çalışma farkı var. İlk olarak WebSockets server ile client arasında kesintisiz uzun süreli çalışacak olan bir TCP bağlantısı oluşturmamızı sağlıyor ve bu oluşturulan iletişim kanalı real-time full-duplex bir kanal. Full-duplex demek anlık olarak çift yönlü veri transferinin sağlanabilmesidir. 

Long Polling'de ise aslında WebSockets davranışı taklit edilmekle beraber aynı performansı vermiyor. WebSockets'in aksine request-response HTTP mantığında çalışmakta. Yani ilk olarak client tarafından server'a request gönderiliyor, server, kendisi ve client arasında oluşturulan TCP connection'u mesaj gönderene kadar kapatmıyor. Mesaj oluştuktan sonra server bu mesajı response olarak gönderiyor ve mesaj client'a ulaştıysa oluşan bağlantıyı kapatıyor. Daha sonrasında tekrar client server'a request yolluyor ve döngü bu şekilde devam ediyor. Yani sonuç olarak istemci üzerinden sürekli bir tetikleme durumu var.

Ayrıntılı bilgiyi şu bağlantılara bakarak öğrenebilirsiniz:
Benim yaptığım uygulamada ise online olan kullanıcıları sistemde anlık olarak görmem gerek, örneğin kullanıcı client browser'ı kapattığında sistem tarafında bu bilgi hemen güncellenmeli, ancak istemci tarafında ben bu tetiklemeyi yaptırmadığım için connection açıkmış gibi görünüyor. Normal şartlarda Blazor WASM uygulamasında dispose metotunda hubconnection'ı durdurup, dispose ediyorum ancak bir değişiklik olmuyor. Sunucu tarafında çalışan hub'daki OnDisconnectedAsync metotu tetiklenmiyor. Bunun sebebini tam olarak çözemedim, bir süre bu konu hakkında araştırma yaptım ancak net bir sonuç bulamadım. Bu bir bug da olabilir ya da kullanım hatası da olabilir, Long Polling'in çalışma şekli ile alakalı da olabilir.

Bu sebeple TrackHub ile iletişim kurarken WebSockets transport kullanımını zorunlu hale getirdim. İlk olarak API kısmında yapılan konfigürasyonlara bakalım:
builder.Services.AddWebSockets(o => { 
    o.AllowedOrigins.Add("https://localhost:7058");
    o.AllowedOrigins.Add("https://localhost:7212");
});
Sadece belli origin'ler için websockets bağlantısı yapılmasını istiyorum o yüzden AddWebSockets ile bu sınırlandırmayı yukarıdaki gibi yapabiliriz. Bu güvenlik açısından önemli bir ayarlama. Daha sonrasında cors policy'i aşağıdaki gibi ayarladım:
builder.Services.AddCors(options =>
{
    options.AddPolicy("all", builder => builder
        .WithOrigins("https://localhost:7058", "https://localhost:7212")
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials()  // Eğer credential (kimlik doğrulama) kullanılıyorsa ekleyin.
        .SetIsOriginAllowed((host) => true) // CORS doğrulamasını özelleştirmek için gerekebilir.
    );
});
Ayrıca SignalR Hub'ın sadece WebSockets bağlantılarını kabul etmesini istiyorum. Diğer transport'lar devredışı kalmış oluyor:
app.MapHub<TrackHub>("track", o => { 
    o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
});
Bir önemli nokta ise geliştirme ortamında kullandığım sunucu olan IIS Express'in WebSockets'i desteklemesi gerekli. Bunun için Turn Windows features on or off Windows işletim sistemi arama kısmına yazıp:
  • Internet Information Services
    • World Wide Web Services
      • Application Development Features
        • WebSocket Protocol'u seçip aktif hale getirin
Daha sonrasında client tarafında çalışacak Blazor WASM'de HubConnection sağlarken aşağıdaki değişiklikleri uyguluyorum:
    protected async override Task OnInitializedAsync()
    {
        var authState = await authenticationStateTask;
        var user = authState.User;
        string username = string.Empty;
        if (user.Identity.IsAuthenticated)
        {
            username = user.Identity.Name;
        }

        string group = "admin";
        ...

        string token = await localStorage.GetItem("token");


        hubConnection = new HubConnectionBuilder()
            .WithUrl(
                "https://localhost:44342/track",
                o => {
                    o.AccessTokenProvider = () => Task.FromResult<string?>(token);
                    o.Url = new Uri($"https://localhost:44342/track?username={username}<group={group}");
                    o.SkipNegotiation = true;
                    o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
                }
            )
            .Build();

		...

        isLoaded = true;
    }
SkipNegotiation true yaparak direkt hangi transport'u seçtiysek doğrudan o transport'u kullanmasını müzakere yapmadan sağlamış oluyoruz. Projeyi daha sonrasında çalıştırırsak muhtemelen alacağımız hata client tarafında şu olacaktır:
WebSocket connection to 'wss://localhost:44342/.. Authentication failed; no valid credentials available
Bu hatanın çözümünü bulamamıştım ta ki dünyanın en iyi arama motoru ChatGPT gerekli çözümü verene kadar:
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.SaveToken = true;

    o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero,
        ValidIssuer = configuration["JwtSettings:Issuer"],
        ValidAudience = configuration["JwtSettings:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))
    };

    o.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/track")))
            {
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});
OnMessageReceived event'i her HTTP mesajı için tetiklenir, amaç JWT token gönderen durumları dinlemektir. accessToken boş değilse ve url track ile başlıyorsa context.Token'a atama token ataması gerçekleştirilir. Yani kısacası OnMessageReceived token'i alır, token doğrulanır ve 'Authorize' attribute'u eğer token doğrulandıysa erişimi sağlar.

No comments:

Post a Comment