Blog'a Dön
SignalRReact NativeReal-TimeWebSocketPush Notifications

Kurumsal Mobil Uygulamalarda SignalR ile Gerçek Zamanlı Bildirimler

Umut Korkmaz2025-09-159 min

Push bildirimleri her mobil uygulamada beklenir, ancak bir kurumsal iş akışı uygulamasında gerçek zamanlı güncellemeler yalnızca sahip-olunsa-iyi-olur bir şey değildir — kritiktir. Bir doküman onaya ihtiyaç duyduğunda, onaylayan kişi bunu dakikalar içinde değil, saniyeler içinde bilmelidir. 2.000'den fazla çalışan için React Native uygulamamız DigiPort'u inşa ederken, gerçek zamanlı katman için Firebase Cloud Messaging yerine SignalR'ı seçtim ve bu karar beklemediğim şekillerde karşılığını verdi.

Neden FCM Yerine SignalR?

Firebase Cloud Messaging, mobil push bildirimleri için standart cevaptır ve uygulama çalışmadığında arka plan bildirimleri için onu kullanıyoruz. Ama uygulama içi gerçek zamanlı güncellemeler için SignalR'ın kurumsal bağlamda önemli avantajları var:

  1. Çift yönlü iletişim. FCM tek yönlüdür. SignalR, istemcinin sunucuya mesaj geri göndermesine olanak tanır ki bu, okundu bilgileri ve yazıyor göstergeleri için kullanışlıdır.
  2. Gerçek zamanlılık için üçüncü taraf bağımlılığı yok. Kamu müşterileri, hassas verilerin Google'ın sunucularından geçirilmesine temkinli yaklaşır. SignalR bizim altyapımızda çalışır.
  3. Web uygulamasıyla paylaşılan hub. DigiFlow (React web uygulamamız) zaten SignalR kullanıyor. Aynı hub, hem web hem de mobil istemcilere hizmet veriyor.
  4. Yapılandırılmış mesajlar. SignalR, ayrıştırılması gereken string yükleri değil, tipli nesneler gönderir.

Hub Tasarımı

SignalR hub'ı bilinçli olarak basittir. İş mantığını değil, bağlantı yönetimini ve mesaj yönlendirmeyi üstlenir:

csharp
[Authorize]
public class WorkflowHub : Hub
{
    private readonly IConnectionManager _connectionManager;
    private readonly ILogger<WorkflowHub> _logger;

    public override async Task OnConnectedAsync()
    {
        var userId = Context.User!.GetUserId();
        var departments = Context.User.GetDepartmentIds();

        await _connectionManager.AddConnectionAsync(
            userId, Context.ConnectionId);

        // Birime özel gruplara katıl
        foreach (var deptId in departments)
        {
            await Groups.AddToGroupAsync(
                Context.ConnectionId, $"dept:{deptId}");
        }

        _logger.LogInformation(
            "User {UserId} connected with {ConnectionId}",
            userId, Context.ConnectionId);

        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var userId = Context.User!.GetUserId();
        await _connectionManager.RemoveConnectionAsync(
            userId, Context.ConnectionId);

        await base.OnDisconnectedAsync(exception);
    }

    // İstemci, bir bildirimin alındığını teyit edebilir
    public async Task AcknowledgeNotification(string notificationId)
    {
        var userId = Context.User!.GetUserId();
        await _connectionManager.MarkDeliveredAsync(
            notificationId, userId);
    }
}

İş olayları, hub'ı temiz tutmak için MediatR olay handler'larından hub'a yayınlanır:

csharp
public class DocumentApprovedEventHandler
    : INotificationHandler<DocumentApprovedEvent>
{
    private readonly IHubContext<WorkflowHub> _hubContext;
    private readonly IConnectionManager _connectionManager;

    public async Task Handle(
        DocumentApprovedEvent notification, CancellationToken ct)
    {
        var recipientId = notification.RequesterId;
        var connections = await _connectionManager
            .GetConnectionsAsync(recipientId);

        if (connections.Any())
        {
            await _hubContext.Clients.Clients(connections)
                .SendAsync("DocumentApproved", new
                {
                    documentId = notification.DocumentId,
                    approvedBy = notification.ApproverName,
                    step = notification.CurrentStep,
                    timestamp = DateTimeOffset.UtcNow,
                }, ct);
        }
        else
        {
            // Kullanıcı çevrimdışı - FCM üzerinden push bildirimi için kuyruğa al
            await _connectionManager.QueueOfflineNotification(
                recipientId, notification);
        }
    }
}

Yedek mekanizmaya dikkat edin: kullanıcının aktif bir SignalR bağlantısı yoksa, bildirimi FCM teslimatı için kuyruğa alıyoruz. Bu hibrit yaklaşım, hiçbir bildirimin kaybolmamasını sağlar.

React Native İstemcisi

Mobil tarafta SignalR bağlantısının, mobil ağ gerçekliklerini ele alması gerekir: kesintili bağlantı, uygulamanın arka plana alınması ve token süresinin dolması.

typescript
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr';

class SignalRService {
  private connection: HubConnection | null = null;
  private isAppActive = true;

  async connect() {
    this.connection = new HubConnectionBuilder()
      .withUrl(`${Config.API_URL}/hubs/workflow`, {
        accessTokenFactory: async () => {
          const token = await tokenManager.getAccessToken();
          return token;
        },
      })
      .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: (ctx) => {
          if (!this.isAppActive) return null; // Arka planda yeniden deneme
          return Math.min(Math.pow(2, ctx.previousRetryCount) * 1000, 30_000);
        },
      })
      .configureLogging(LogLevel.Warning)
      .build();

    this.registerHandlers();
    await this.connection.start();
  }

  private registerHandlers() {
    if (!this.connection) return;

    this.connection.on('DocumentApproved', (data) => {
      useNotificationStore.getState().addNotification({
        id: crypto.randomUUID(),
        type: 'approval',
        title: 'Document Approved',
        body: `${data.approvedBy} approved your document`,
        documentId: data.documentId,
        timestamp: data.timestamp,
        read: false,
      });
    });

    this.connection.on('NewTask', (data) => {
      useNotificationStore.getState().addNotification({
        id: crypto.randomUUID(),
        type: 'task',
        title: 'New Task Assigned',
        body: data.description,
        documentId: data.documentId,
        timestamp: data.timestamp,
        read: false,
      });
    });

    this.connection.onreconnecting(() => {
      useNotificationStore.getState().setConnectionStatus(false);
    });

    this.connection.onreconnected(() => {
      useNotificationStore.getState().setConnectionStatus(true);
    });
  }

  setAppState(active: boolean) {
    this.isAppActive = active;
    if (active && this.connection?.state === 'Disconnected') {
      this.connect(); // Uygulama ön plana geldiğinde yeniden bağlan
    }
  }
}

Kilit ayrıntı setAppState. Uygulama arka plana geçtiğinde, pili korumak için yeniden bağlanma girişimlerini durdururuz. Tekrar ön plana geldiğinde anında yeniden bağlanırız. Bu, React Native'in AppState API'sine bağlanır:

typescript
useEffect(() => {
  const subscription = AppState.addEventListener('change', (state) => {
    signalRService.setAppState(state === 'active');
  });
  return () => subscription.remove();
}, []);

Redis ile Connection Manager

Kullanıcı başına birden fazla cihazı desteklemek, Redis destekli bir connection manager gerektirir:

csharp
public class RedisConnectionManager : IConnectionManager
{
    private readonly IConnectionMultiplexer _redis;
    private const string PREFIX = "signalr:connections:";

    public async Task AddConnectionAsync(string userId, string connectionId)
    {
        var db = _redis.GetDatabase();
        await db.SetAddAsync($"{PREFIX}{userId}", connectionId);
        await db.KeyExpireAsync($"{PREFIX}{userId}", TimeSpan.FromHours(12));
    }

    public async Task<IReadOnlyList<string>> GetConnectionsAsync(string userId)
    {
        var db = _redis.GetDatabase();
        var connections = await db.SetMembersAsync($"{PREFIX}{userId}");
        return connections.Select(c => c.ToString()).ToList();
    }
}

Bir kullanıcının masaüstünde web uygulaması, telefonunda da mobil uygulaması açık olabilir. Her iki bağlantı kimliği de Redis set'inde saklandığı için ikisi de bildirimi aynı anda alır.

Teslimat Garantileri

Teyit (acknowledgment) deseni, bir bildirimin gerçekten alındığını bilmemizi sağlar:

typescript
connection.on('DocumentApproved', async (data) => {
  // Yerel duruma ekle
  useNotificationStore.getState().addNotification(/* ... */);

  // Sunucuya alındığını teyit et
  await connection.invoke('AcknowledgeNotification', data.notificationId);
});

Sunucu tarafında, 60 saniye sonra teyit edilmemiş bildirimler yeniden gönderilir. 3 başarısız denemenin ardından FCM push bildirimlerine düşerler. Bu üç katmanlı teslimat sistemi — SignalR, yeniden deneme, FCM yedeği — bize %99,8'lik bir teslimat oranı sağlıyor.

Üretim Metrikleri

500'den fazla günlük aktif kullanıcıyla bu sistemi 6 ay çalıştırdıktan sonra:

  • Ortalama bildirim teslim süresi: 180ms (SignalR), 2-4 saniyeye (FCM) karşı
  • SignalR bağlantı uptime'ı: çalışma saatlerinde %98,5
  • Bildirim teslimat oranı: %99,8
  • Pil etkisi: ihmal edilebilir — SignalR, işletim sisteminin verimli şekilde yönettiği WebSocket'i kullanır

Temel Çıkarımlar

  1. Uygulama içi gerçek zamanlılık için SignalR, arka plan push için FCM kullanın. Birbirlerini tamamlarlar.
  2. Arka planda yeniden bağlanmayı durdurun. Mobil piller önemlidir.
  3. Kullanıcıların birden fazla cihazı olduğunda bağlantıları Redis'te takip edin.
  4. Teslimatı garantilemek için bildirimleri teyit edin.
  5. Zarif bir şekilde yedeğe düşün. SignalR -> yeniden deneme -> FCM, sağlam bir teslimat zinciridir.