Hai
Nama saya Sasha dan saya bekerja sebagai arsitek di Tinkoff Business.
Pada artikel ini saya ingin berbicara tentang cara mengatasi batas browser pada jumlah koneksi HTTP yang berumur panjang dalam domain yang sama menggunakan pekerja layanan.
Jika Anda mau, silakan lewati latar belakang, uraian masalah, cari solusi, dan segera lanjutkan ke hasilnya.

Latar belakang
Sekali waktu di Tinkoff Business ada obrolan yang berhasil di
Websocket .
Setelah beberapa waktu, ia tidak lagi cocok dengan desain akun pribadinya, dan secara umum ia sudah lama meminta penulisan ulang dari sudut 1,6 ke sudut 2+. Saya memutuskan bahwa sudah waktunya untuk mulai memperbaruinya. Seorang kolega-pendukung menemukan bahwa frontend obrolan akan berubah, dan menyarankan pada saat yang sama mengulang API, khususnya - mengubah transportasi dari websocket ke
SSE (peristiwa yang dikirim server) . Dia menyarankan ini karena ketika memperbarui konfigurasi NGINX semua koneksi terputus, yang kemudian menyakitkan untuk dipulihkan.
Kami membahas arsitektur solusi baru dan sampai pada kesimpulan bahwa kami akan menerima dan mengirim data menggunakan permintaan HTTP biasa. Misalnya, kirim pesan
POST: / api / kirim-pesan , dapatkan daftar dialog
GET: / api / daftar percakapan, dan sebagainya. Dan peristiwa tak sinkron seperti "pesan baru dari teman bicara" akan dikirim melalui SSE. Jadi kami akan meningkatkan toleransi kesalahan aplikasi: jika koneksi SSE terputus, obrolan akan tetap berfungsi, hanya saja ia tidak akan menerima notifikasi waktu nyata.
Selain obrolan di websocket, kami mengejar acara untuk komponen "pemberitahuan tipis". Komponen ini memungkinkan Anda untuk mengirim berbagai pemberitahuan ke akun pribadi pengguna, misalnya, bahwa impor akun, yang mungkin memakan waktu beberapa menit, telah berhasil diselesaikan. Untuk sepenuhnya meninggalkan websocket, kami memindahkan komponen ini ke koneksi SSE yang terpisah.
Masalah
Ketika Anda membuka satu tab browser, dua koneksi SSE dibuat: satu untuk obrolan dan satu untuk pemberitahuan halus. Baiklah, biarkan mereka diciptakan. Maaf atau apa? Kami tidak merasa menyesal, tetapi browser merasa menyesal! Mereka memiliki
batasan jumlah koneksi persisten bersamaan untuk suatu domain . Coba tebak berapa banyak di Chrome? Benar enam! Saya membuka tiga tab - Saya mencetak seluruh kumpulan koneksi dan Anda tidak bisa lagi membuat permintaan HTTP. Ini berlaku untuk protokol HTTP / 1.x. Dalam HTTP / 2 tidak ada masalah seperti itu karena multiplexing.
Ada beberapa cara untuk mengatasi masalah ini di tingkat infrastruktur:
- Pecahan domain.
- HTTP / 2.
Kedua metode ini tampaknya mahal, karena banyak infrastruktur yang harus terkena dampak.
Karenanya, sebagai permulaan, kami mencoba menyelesaikan masalah di sisi browser. Gagasan pertama adalah membuat semacam transportasi antar tab, misalnya, melalui
LocalStorage atau
Broadcast Channel API .
Artinya adalah ini: kita membuka koneksi SSE hanya dalam satu tab dan mengirim data ke yang lain. Solusi ini juga tidak terlihat optimal, karena akan membutuhkan pelepasan seluruh 50 SPA, yang merupakan akun pribadi Tinkoff Business. Melepaskan 50 aplikasi juga mahal, jadi saya terus mencari cara lain.
Solusi
Saya baru-baru ini bekerja dengan pekerja layanan dan berpikir: apakah mungkin menerapkannya dalam situasi ini?
Untuk menjawab pertanyaan ini, Anda harus terlebih dahulu memahami apa yang biasanya dilakukan oleh pekerja layanan? Mereka dapat mem-proxy permintaan, tampilannya seperti ini:
self.addEventListener('fetch', event => { const response = self.caches.open('example') .then(caches => caches.match(event.request)) .then(response => response || fetch(event.request)); event.respondWith(response); });
Kami mendengarkan acara untuk permintaan HTTP dan merespons sesuka kami. Dalam hal ini, kami mencoba merespons dari cache, dan jika tidak berhasil, maka kami membuat permintaan ke server.
Oke, mari kita coba untuk mencegat koneksi SSE dan menjawabnya:
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } event.respondWith(new Response('Hello!')); });
Dalam permintaan jaringan, kita melihat gambar berikut:

Dan di konsol, ini:

Sudah tidak buruk. Permintaan dicegat, tetapi SSE tidak menginginkan respons dalam bentuk
teks / polos , tetapi menginginkan
teks / aliran acara . Bagaimana cara membuat streaming sekarang? Tetapi bisakah saya bahkan merespons dengan aliran dari pekerja layanan? Baiklah mari kita lihat:

Hebat! Kelas
Respons diambil sebagai body
ReadableStream . Setelah membaca
dokumentasi , Anda dapat mengetahui bahwa
ReadableStream memiliki pengontrol yang memiliki metode
enqueue () - dengan bantuannya Anda dapat mengalirkan data. Cocok, bawa!
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const responseText = 'Hello!'; const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream); event.respondWith(response); });

Tidak ada kesalahan, koneksi hang dalam status pending dan tidak ada data tiba di sisi klien. Membandingkan permintaan saya dengan permintaan server nyata, saya menyadari bahwa jawabannya ada di header. Untuk permintaan SSE, tajuk berikut harus ditentukan:
const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', };
Ketika Anda menambahkan header ini, koneksi akan terbuka dengan sukses, tetapi data tidak akan diterima di sisi klien. Ini jelas, karena Anda tidak bisa hanya mengirim teks acak - pasti ada beberapa format.
Di javascript.info,
format data di mana Anda ingin mengirim data dari server
dijelaskan dengan baik . Dapat dengan mudah dijelaskan dengan satu fungsi:
const sseChunkData = (data: string, event?: string, retry?: number, id?: number): string => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n';
Untuk mematuhi format SSE, server harus mengirim pesan yang dipisahkan oleh jeda baris ganda
\ n \ n .
Pesan terdiri dari bidang-bidang berikut:
- data - badan pesan, beberapa data dalam satu baris ditafsirkan sebagai satu pesan, dipisahkan oleh jeda baris \ n;
- id - memperbarui properti lastEventId yang dikirim dalam header Last-Event-ID saat menghubungkan kembali;
- coba lagi - penundaan yang disarankan sebelum menghubungkan kembali dalam milidetik, tidak dapat diatur menggunakan JavaScript;
- acara - nama acara pengguna, ditunjukkan sebelum data.
Tambahkan tajuk yang diperlukan, ubah jawaban ke format yang diinginkan dan lihat apa yang terjadi:
self.addEventListener('fetch', event => { const {headers} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream'; if (!isSSERequest) { return; } const sseChunkData = (data, event, retry, id) => Object.entries({event, id, data, retry}) .filter(([, value]) => ![undefined, null].includes(value)) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n'; const sseHeaders = { 'content-type': 'text/event-stream', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', }; const responseText = sseChunkData('Hello!'); const responseData = Uint8Array.from(responseText, x => x.charCodeAt(0)); const stream = new ReadableStream({start: controller => controller.enqueue(responseData)}); const response = new Response(stream, {headers: sseHeaders}); event.respondWith(response); });

Astaga! Ya, saya membuat koneksi SSE tanpa server!
Hasil
Sekarang kita berhasil mencegat permintaan SSE dan meresponsnya tanpa melampaui browser.
Awalnya, idenya adalah untuk membuat koneksi dengan server, tetapi hanya satu hal - dan darinya mengirim data ke tab. Ayo lakukan!
self.addEventListener('fetch', event => { const {headers, url} = event.request; const isSSERequest = headers.get('Accept') === 'text/event-stream';
Kode yang sama di github.Saya mendapat solusi yang cukup sederhana untuk tugas yang tidak terlalu sepele. Tapi, tentu saja, masih banyak nuansa. Misalnya, Anda harus menutup koneksi ke server saat menutup semua tab, mendukung protokol SSE, dan sebagainya.
Kami telah berhasil memutuskan semua ini - saya yakin tidak akan sulit bagi Anda!