Manajemen Koneksi SignalR yang Efisien

Halo, Habrahabr. Saat ini saya sedang mengerjakan mesin obrolan berdasarkan perpustakaan SignalR . Selain proses imersi yang menarik di dunia aplikasi waktu nyata, saya juga harus menghadapi sejumlah tantangan teknis. Tentang salah satu dari mereka, saya ingin berbagi dengan Anda di artikel ini.

Pendahuluan


Apa itu SignalR - itu semacam fasad di atas WebSockets , Polling panjang , Server-send events technology. Berkat fasad ini, Anda dapat bekerja secara seragam dengan semua teknologi ini dan tidak khawatir tentang detailnya. Selain itu, berkat teknologi polling panjang, Anda dapat mendukung klien yang, karena alasan tertentu, tidak dapat bekerja di soket web, seperti IE-8. Fasad diwakili oleh API berbasis RPC tingkat tinggi. Selain itu, SignalR menawarkan untuk membangun komunikasi sesuai dengan prinsip "penerbit-pelanggan", yang dalam terminologi API disebut kelompok. Ini akan dibahas lebih lanjut.

Tantangan


Mungkin hal yang paling menarik dalam pemrograman adalah kemampuan untuk menyelesaikan masalah yang tidak standar. Dan hari ini kita akan menunjuk salah satu tugas ini dan mempertimbangkan solusinya.

Di era pengembangan ide penskalaan dan, pertama-tama, horisontal, tantangan utama adalah kebutuhan untuk memiliki lebih dari satu server. Dan pengembang perpustakaan yang ditunjukkan sudah mengatasi panggilan ini, deskripsi solusinya dapat ditemukan di MSDN . Singkatnya, diusulkan, menggunakan prinsip penerbit-pelanggan, untuk menyinkronkan panggilan antar server. Setiap server berlangganan bus bersama dan semua perintah yang dikirim dari server ini dikirim terlebih dahulu ke bus. Selanjutnya, perintah ini berlaku untuk semua server dan hanya untuk klien:

gambar

Penting untuk dicatat bahwa setiap klien yang terhubung ke server memiliki pengenal koneksi unik - ConnectionId - dan semua pesan pada akhirnya ditangani menggunakan pengenal ini. Oleh karena itu, setiap server menyimpan koneksi ini.

Namun, untuk alasan yang tidak diketahui, API pustaka SignalR tidak menyediakan akses ke data ini. Dan di sini kita dihadapkan dengan pertanyaan yang sangat akut tentang akses ke koneksi ini. Ini tantangan kita.

Mengapa kita perlu terhubung


Seperti disebutkan sebelumnya, SignalR menawarkan model penerbit-pelanggan. Di sini, unit perutean pesan bukan ConnectionId tetapi grup. Grup adalah kumpulan koneksi. Dengan mengirim pesan ke grup, kami mengirim pesan ke semua ConnectionId yang ada di grup ini. Lebih mudah untuk membangun grup - saat menghubungkan klien ke server, kami cukup memanggil metode API AddToGroupAsync :

public override async Task OnConnectedAsync() { foreach (var chat in _options.Chats) await Groups.AddToGroupAsync(ConnectionId, chat); await Groups.AddToGroupAsync(ConnectionId, Client); } 

Dan bagaimana cara meninggalkan grup? Pengembang menawarkan metode API RemoveFromGroupAsync :

 public override async Task OnDisconnectedAsync(Exception exception) { foreach (var chat in _options.Chats) await Groups.RemoveFromGroupAsync(ConnectionId, chat); await Groups.RemoveFromGroupAsync(ConnectionId, Client); } 

Perhatikan bahwa unit data adalah ConnectionId. Namun, dari sudut pandang model domain, ConnectionId tidak ada, tetapi ada klien. Dalam hal ini, organisasi pemetaan klien ke array ConnectionId dan sebaliknya ditugaskan untuk pengguna perpustakaan yang ditentukan.

Ini adalah array dari semua klien ConnectionId yang diperlukan ketika meninggalkan grup. Namun, array seperti itu tidak ada. Anda perlu mengaturnya sendiri. Tugas menjadi jauh lebih menarik dalam kasus sistem skala horizontal. Dalam hal ini, bagian dari koneksi bisa di satu server, sisanya di server lain.

Cara memetakan klien ke koneksi


Seluruh bagian tentang MSDN didedikasikan untuk masalah ini. Metode berikut diusulkan untuk dipertimbangkan:

  • Penyimpanan dalam memori
  • "Grup Pengguna"
  • Penyimpanan Eksternal Permanen

Bagaimana cara melacak koneksi?
Anda dapat melacak koneksi menggunakan metode hub OnConnectedAsync dan OnDisconnectedAsync .

Segera, saya perhatikan bahwa opsi yang tidak mendukung penskalaan tidak dipertimbangkan. Ini termasuk opsi untuk menyimpan koneksi dalam memori server. Tidak ada akses ke koneksi klien di server lain, jika ada. Opsi penyimpanan dalam penyimpanan persisten eksternal dikaitkan dengan kelemahannya, yang mencakup masalah pembersihan koneksi tidak aktif. Koneksi tersebut terjadi jika terjadi reboot server yang keras. Mendeteksi dan membersihkan koneksi ini bukanlah tugas yang sepele.

Di antara opsi di atas, opsi "grup pengguna" menarik. Kesederhanaan tentu berlaku untuk keuntungannya - tidak ada perpustakaan, repositori diperlukan. Yang tak kalah penting adalah konsekuensi dari kesederhanaan metode ini - keandalan.

Tapi bagaimana dengan Redis?
Omong-omong, menggunakan Redis untuk menyimpan koneksi juga merupakan pilihan yang buruk. Ada masalah akut dalam mengatur data dalam memori. Di satu sisi, kuncinya adalah klien, di sisi lain, grup.

"Grup Pengguna"


Apa itu "grup pengguna"? Ini adalah grup dalam terminologi SignalR di mana hanya satu klien yang dapat menjadi klien - dia sendiri. Ini menjamin 2 hal:

  1. Pesan akan dikirimkan hanya kepada satu orang
  2. Pesan akan dikirim ke semua perangkat manusia

Bagaimana ini akan membantu kami? Biarkan saya mengingatkan Anda bahwa tantangan kami adalah menyelesaikan masalah meninggalkan klien dari grup. Kami memerlukan itu, meninggalkan grup dari satu perangkat, sisanya juga akan berhenti berlangganan, tetapi kami tidak memiliki daftar koneksi untuk klien ini, kecuali yang kami gunakan untuk memulai keluar.

"Kelompok pengguna" adalah langkah pertama untuk menyelesaikan masalah ini. Langkah kedua adalah membangun "cermin" pada klien. Ya, ya, mirror.

Cermin


Sumber perintah yang dikirim dari klien ke server adalah tindakan pengguna. Kirim pesan - kirim perintah ke server:

 this.state.hubConnection .invoke('post', {message, group, nick}) .catch(err => console.error(err)); 

Dan kami memberi tahu semua klien grup tentang pos baru:

 public async Task PostMessage(PostMessage message) { await Clients.Group(message.Group).SendAsync("message", new { Message = message.Message, Group = message.Group, Nick = ClientNick }); } 

Namun, sejumlah perintah harus dijalankan secara serempak di semua perangkat. Bagaimana cara mencapai ini? Entah memiliki array koneksi dan menjalankan perintah untuk setiap koneksi pada klien tertentu, atau menggunakan metode yang dijelaskan di bawah ini. Pertimbangkan metode ini dengan keluar dari obrolan.

Tim yang tiba dari klien pertama-tama akan pergi ke "grup pengguna" untuk metode khusus, yang hanya akan mengarahkannya kembali ke server, mis. " Cermin ." Dengan demikian, bukan server yang akan berhenti berlangganan perangkat, tetapi perangkat itu sendiri akan diminta untuk berhenti berlangganan.

Berikut adalah contoh dari perintah obrolan server berhenti berlangganan:

 public async Task LeaveChat(LeaveChatMessage message) { await Clients.OthersInGroup(message.Group).SendAsync("lost", new ClientCommand { Group = message.Group, Nick = Client }); await Clients.Group(Client).SendAsync("mirror", new MirrorChatCommand { Method = "unsubscribe", Payload = new UnsubscribeChatMessage { Group = message.Group } }); } 

 public async Task Unsubscribe(UnsubscribeChatMessage message) { await Groups.RemoveFromGroupAsync(ConnectionId, message.Group); } 

Dan di sini adalah kode klien:

 connection.on('mirror', (message) => { connection .invoke(message.method, message.payload) .catch(err => console.error(err)); }); 

Mari kita teliti lebih detail apa yang terjadi di sini:

  1. Klien memulai berhenti berlangganan - mengirim perintah "meninggalkan" ke server
  2. Server mengirimkan perintah "berhenti berlangganan" ke "grup pengguna" di "mirror"
  3. Pesan dikirim ke semua perangkat klien.
  4. Pesan pada klien dikirim kembali ke server menggunakan metode yang ditentukan oleh server
  5. Di setiap server, klien berhenti berlangganan dari grup

Akibatnya, semua perangkat itu sendiri akan berhenti berlangganan dari server yang terhubung. Masing-masing akan berhenti berlangganan sendiri dan kita tidak perlu menyimpan apa pun. Tidak ada masalah juga akan muncul jika terjadi reboot dari server.

Jadi mengapa kita perlu terhubung?


Memiliki "grup pengguna" dan "mirror" pada klien menghilangkan kebutuhan untuk bekerja dengan koneksi. Bagaimana menurut Anda, para pembaca, tentang hal ini? Bagikan pendapat Anda di komentar.

Kode sumber untuk contoh:

github.com/aesamson/signalr-server
github.com/aesamson/signalr-client

Source: https://habr.com/ru/post/id470299/


All Articles