Aggiunta di Socket.io a Node.js multi-thread

Foto di Vidar Nordli-Mathisen su Unsplash

Uno degli svantaggi di Node è che è a thread singolo. Ovviamente, c'è un modo per aggirarlo - vale a dire un modulo chiamato cluster. Il cluster ci consente di diffondere la nostra applicazione su più thread.

Ora, tuttavia, si presenta un nuovo problema. Vedi, il nostro codice che viene eseguito su più istanze ha in realtà alcuni aspetti negativi significativi. Uno di questi non sta avendo stati globali.

Normalmente, in un'istanza a thread singolo, questo non sarebbe molto preoccupante. Per noi ora cambia tutto.

Vediamo perché.

Quindi qual'è il problema?

La nostra applicazione è una semplice chat online in esecuzione su quattro thread. Ciò consente a un utente di accedere contemporaneamente al proprio telefono e computer.

Immagina di avere i socket impostati esattamente come li avremmo impostati per un thread. In altre parole, ora abbiamo un grande stato globale con socket.

Quando l'utente accede al proprio computer, il sito Web apre la connessione con un'istanza Socket.io sul nostro server. Il socket è memorizzato nello stato del thread n. 3.

Ora, immagina che l'utente vada in cucina a fare uno spuntino e porti con sé il telefono, naturalmente desiderando continuare a mandare SMS con i propri amici online.

Il loro telefono si collega al thread n. 4 e il socket viene salvato nello stato del thread.

L'invio di un messaggio dal proprio telefono non gioverà all'utente. Solo le persone del thread n. 3 potranno vedere il messaggio. Questo perché i socket salvati sul thread n. 3 non sono in qualche modo magicamente memorizzati sui thread n. 1, n. 2 e n. 4.

Abbastanza divertente, anche l'utente stesso non vedrà i propri messaggi sul proprio computer una volta tornati dalla cucina.

Naturalmente, quando aggiornano il sito Web, potremmo inviare una richiesta GET e recuperare gli ultimi 50 messaggi, ma non possiamo davvero dire che è il modo "dinamico", vero?

Perché sta succedendo?

Diffondere il nostro server su più thread equivale in qualche modo ad avere diversi server separati. Non si conoscono dell'esistenza reciproca e certamente non condividono alcun ricordo. Ciò significa che un oggetto su un'istanza non esiste sull'altra.

I socket salvati nel thread n. 3 non sono necessariamente tutti i socket che l'utente sta utilizzando al momento. Se gli amici dell'utente si trovano su thread diversi, non vedranno i messaggi dell'utente a meno che non aggiornino il sito Web.

Idealmente, vorremmo informare altre istanze di un evento per l'utente. In questo modo possiamo essere certi che ogni dispositivo connesso sta ricevendo aggiornamenti in tempo reale.

Una soluzione

Possiamo notificare altri thread utilizzando il paradigma di messaggistica di pubblicazione / sottoscrizione di Redis (pubsub).

Redis è un archivio di strutture di dati in memoria open source (con licenza BSD). Può essere utilizzato come database, cache e broker di messaggi.

Ciò significa che possiamo usare Redis per distribuire eventi tra le nostre istanze.

Si noti che normalmente vorremmo archiviare l'intera struttura all'interno di Redis. Tuttavia, poiché la struttura non è serializzabile e deve essere mantenuta "viva" all'interno della memoria, ne memorizzeremo parte su ogni istanza.

Il flusso

Pensiamo ora ai passaggi in cui gestiremo un evento in arrivo.

  1. L'evento chiamato messaggio arriva a uno dei nostri socket - in questo modo, non dobbiamo ascoltare per ogni possibile evento.
  2. All'interno dell'oggetto passato al gestore di questo evento come argomento, possiamo trovare il nome dell'evento. Ad esempio, sendMessage - .on ('message', ({event}) => {}).
  3. Se esiste un gestore per questo nome, lo eseguiremo.
  4. Il gestore può eseguire l'invio con una risposta.
  5. L'invio invia l'evento di risposta al nostro pubs Redis. Da lì viene emesso in ciascuna delle nostre istanze.
  6. Ogni istanza lo emette nei loro socketState, assicurando che ogni client connesso riceverà l'evento.

Sembra complicato, lo so, ma abbi pazienza.

Implementazione

Ecco il repository con l'ambiente pronto, in modo da non dover installare e configurare tutto da soli.

Innanzitutto, configureremo un server con Express.

Creiamo un'app Express, un server HTTP e dei socket init.

Ora possiamo concentrarci sull'aggiunta di socket.

Passiamo l'istanza del server di Socket.io alla nostra funzione in cui impostiamo i middleware.

onAuth

La funzione onAuth imita semplicemente un'autorizzazione simulata. Nel nostro caso è basato su token.

Personalmente, probabilmente lo sostituirò con JWT in futuro, ma non viene applicato in alcun modo.

Passiamo ora al middleware onConnection.

OnConnection

Qui vediamo che recuperiamo l'ID dell'utente, che è stato impostato nel middleware precedente, e lo salviamo nel nostro socketState, con la chiave come id e il valore come una matrice di socket.

Quindi, ascoltiamo l'evento del messaggio. Tutta la nostra logica si basa su questo: ogni evento che il frontend ci invia verrà chiamato: messaggio.

Il nome dell'evento verrà inviato all'interno dell'oggetto argomenti - come indicato sopra.

handlers

Come puoi vedere in onConnection, in particolare nel listener per l'evento del messaggio, stiamo cercando un gestore basato sul nome dell'evento.

I nostri gestori sono semplicemente un oggetto in cui la chiave è il nome dell'evento e il valore è la funzione. Lo useremo per ascoltare gli eventi e rispondere di conseguenza.

Inoltre, in seguito, aggiungeremo la funzione di invio e la useremo per inviare l'evento attraverso le istanze.

SocketsState

Conosciamo l'interfaccia del nostro stato, ma non abbiamo ancora implementato.

Aggiungiamo metodi per aggiungere e rimuovere un socket, nonché per emettere un evento.

La funzione di aggiunta verifica se lo stato ha una proprietà uguale all'ID dell'utente. In tal caso, semplicemente lo aggiungiamo al nostro array già esistente. Altrimenti, prima creiamo un nuovo array.

La funzione di rimozione controlla anche se lo stato ha l'ID dell'utente nelle sue proprietà. Altrimenti, non fa nulla. Altrimenti, filtra l'array per rimuovere il socket dall'array. Quindi, se l'array è vuoto, lo rimuove dallo stato, impostando la proprietà su indefinita.

Pubsub di Redis

Per creare il nostro pubsub useremo il pacchetto chiamato node-redis-pubsub.

Aggiunta della spedizione

Ok, ora non resta che aggiungere la funzione di invio ...

... e aggiungi un ascoltatore per outgoing_socket_message. In questo modo, ogni istanza riceve l'evento e lo invia ai socket dell'utente.

Rendendo tutto multi-thread

Infine, aggiungiamo il codice necessario affinché il nostro server sia multi-thread.

Nota: dobbiamo uccidere la porta, perché dopo aver chiuso il nostro processo Nodemon con Ctrl + c si blocca lì.

Con un po 'di modifica, ora abbiamo socket funzionanti in tutte le istanze. Di conseguenza: un server molto più efficiente.

Grazie mille per aver letto!

Apprezzo che tutto possa sembrare travolgente all'inizio e faticoso prenderlo tutto in una volta. Con questo in mente, ti incoraggio vivamente a leggere di nuovo il codice nella sua interezza e ponderarlo nel suo insieme.

Se hai domande o commenti, sentiti libero di inserirli nella sezione commenti qui sotto o di inviarmi un messaggio.

Dai un'occhiata ai miei social media!

Iscriviti alla mia newsletter!

Originariamente pubblicato su www.mcieslar.com il 10 settembre 2018.