Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

WebSockets - komunikacja między kohortami

Witaj, główny zwiadowco Imperium! Konsul Caesar.js potrzebuje natychmiastowej komunikacji między wszystkimi kohortami w naszym legionie. Tradycyjne HTTP to jak wysyłanie listów gołębiami pocztowymi - za wolne na rozległych szlakach Imperium! Czas nauczyć się WebSockets - potężnej technologii pozwalającej na błyskawiczną komunikację w czasie rzeczywistym.

Czym są WebSockets w świecie legionistów?

Wyobraź sobie, że każdy fort w legionie ma magiczne zwierciadło, przez które centurionowie mogą się ze sobą komunikować natychmiast, bez czekania. WebSockets to właśnie taka technologia - ustanawia trwałe połączenie między klientem a serwerem, pozwalając na dwukierunkową komunikację w czasie rzeczywistym.

Różnice między HTTP a WebSockets

1// HTTP - jak wysyłanie listów
2// Klient: jaki jest stan pogody?
3// Serwer: Słonecznie, 25°C
4// [Połączenie zamknięte]
5// Klient: Czy nadal słonecznie?
6// [Nowe połączenie...]
7
8// WebSockets - jak rozmowa przez radio
9// Klient ↔ Serwer: Trwałe połączenie
10// "Pogoda słoneczna" -> "Burza nadciąga" -> "Wszystko jasne"

Instalacja WebSockets w NestJS

1# Instalacja niezbędnych pakietów dla komunikacji
2npm install @nestjs/websockets @nestjs/platform-socket.io socket.io
3npm install --save-dev @types/socket.io

Pierwszy Gateway - centrum komunikacji

1// cohort-communication.gateway.ts
2import {
3 WebSocketGateway,
4 WebSocketServer,
5 SubscribeMessage,
6 MessageBody,
7 ConnectedSocket,
8 OnGatewayConnection,
9 OnGatewayDisconnect,
10} from '@nestjs/websockets';
11import { Server, Socket } from 'socket.io';
12import { Logger } from '@nestjs/common';
13
14@WebSocketGateway({
15 cors: {
16 origin: '*', // W produkcji ustaw konkretne domeny
17 },
18})
19export class LegionCommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
20 @WebSocketServer()
21 server: Server;
22 
23 private readonly logger = new Logger(LegionCommunicationGateway.name);
24 private connectedLegions = new Map<string, any>();
25
26 // Gdy fort łączy się z legionem
27 handleConnection(client: Socket) {
28 this.logger.log(` Nowy fort dołączył do legionu: ${client.id}`);
29 
30 // Wysyłamy powitanie
31 client.emit('welcome', {
32 message: 'Witaj w komunikacji legionu!',
33 cohortId: client.id,
34 timestamp: new Date().toISOString(),
35 });
36 }
37
38 // Gdy kohorta opuszcza legion
39 handleDisconnect(client: Socket) {
40 this.logger.log(`🦅 Kohorta opuściła legion: ${client.id}`);
41 this.connectedLegions.delete(client.id);
42 
43 // Powiadom pozostałe forty
44 this.server.emit('cohort-left', {
45 cohortId: client.id,
46 message: 'Kohorta opuściła legion',
47 timestamp: new Date().toISOString(),
48 });
49 }
50
51 // Obsługa wiadomości od kohort
52 @SubscribeMessage('cohort-message')
53 handleLegionMessage(
54 @MessageBody() data: { message: string; cohortName: string },
55 @ConnectedSocket() client: Socket,
56 ) {
57 this.logger.log(`Wiadomość od ${data.cohortName}: ${data.message}`);
58 
59 // Przekaż wiadomość do wszystkich kohort
60 this.server.emit('legion-message', {
61 from: data.cohortName,
62 message: data.message,
63 timestamp: new Date().toISOString(),
64 senderId: client.id,
65 });
66 
67 return {
68 status: 'delivered',
69 message: 'Wiadomość dostarczona do legionu',
70 };
71 }
72
73 // Sygnał SOS
74 @SubscribeMessage('emergency')
75 handleEmergency(
76 @MessageBody() data: { location: string; emergency: string; cohortName: string },
77 @ConnectedSocket() client: Socket,
78 ) {
79 this.logger.warn(` ALARM! ${data.cohortName} - ${data.emergency}`);
80 
81 // Natychmiastowy broadcast do wszystkich
82 this.server.emit('legion-emergency', {
83 alert: ' SYGNAŁ SOS! ',
84 from: data.cohortName,
85 emergency: data.emergency,
86 location: data.location,
87 timestamp: new Date().toISOString(),
88 priority: 'CRITICAL',
89 });
90 
91 return { status: 'emergency-broadcasted' };
92 }
93
94 // Status fortu
95 @SubscribeMessage('cohort-status')
96 handleLegionStatus(
97 @MessageBody() data: {
98 cohortName: string;
99 legion: number;
100 supplies: { food: number; water: number; ammunition: number };
101 location: { lat: number; lng: number };
102 },
103 @ConnectedSocket() client: Socket,
104 ) {
105 // Zapisz status fortu
106 this.connectedLegions.set(client.id, {
107 ...data,
108 lastUpdate: new Date(),
109 clientId: client.id,
110 });
111 
112 // Wyślij aktualizację do centrum dowodzenia
113 this.server.emit('legion-status-update', {
114 cohortId: client.id,
115 ...data,
116 timestamp: new Date().toISOString(),
117 });
118 
119 return { status: 'status-updated' };
120 }
121
122 // Metoda do wysyłania rozkazów od legata legionu
123 broadcastLegionOrder(order: string, priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL') {
124 this.server.emit('legion-order', {
125 order,
126 priority,
127 from: 'Legate Caesar.js',
128 timestamp: new Date().toISOString(),
129 });
130 }
131
132 // Pobierz status wszystkich kohort
133 getLegionStatus() {
134 return Array.from(this.connectedLegions.values());
135 }
136}

Integracja z modułem

1// cohort-communication.module.ts
2import { Module } from '@nestjs/common';
3import { LegionCommunicationGateway } from './cohort-communication.gateway';
4import { LegionService } from './legion.service';
5
6@Module({
7 providers: [LegionCommunicationGateway, LegionService],
8 exports: [LegionCommunicationGateway],
9})
10export class LegionCommunicationModule {}

Klient WebSocket (Frontend)

1// legion-client.ts (przykład klienta)
2import { io, Socket } from 'socket.io-client';
3
4class LegionClient {
5 private socket: Socket;
6 private cohortName: string;
7
8 constructor(cohortName: string) {
9 this.cohortName = cohortName;
10 this.socket = io('ws://localhost:3000', {
11 autoConnect: false,
12 });
13 
14 this.setupEventListeners();
15 }
16
17 connect() {
18 this.socket.connect();
19 }
20
21 disconnect() {
22 this.socket.disconnect();
23 }
24
25 private setupEventListeners() {
26 this.socket.on('connect', () => {
27 console.log(` ${this.cohortName} połączony z legionem`);
28 this.reportStatus();
29 });
30
31 this.socket.on('welcome', (data) => {
32 console.log('Powitanie:', data.message);
33 });
34
35 this.socket.on('legion-message', (data) => {
36 console.log(`Wiadomość od ${data.from}: ${data.message}`);
37 });
38
39 this.socket.on('legion-emergency', (data) => {
40 console.log(` ${data.alert} ${data.from}: ${data.emergency}`);
41 });
42
43 this.socket.on('legion-order', (data) => {
44 console.log(` Rozkaz [${data.priority}]: ${data.order}`);
45 });
46
47 this.socket.on('disconnect', () => {
48 console.log(`🦅 ${this.cohortName} odłączony od legionu`);
49 });
50 }
51
52 sendMessage(message: string) {
53 this.socket.emit('cohort-message', {
54 message,
55 cohortName: this.cohortName,
56 });
57 }
58
59 sendEmergency(emergency: string, location: string) {
60 this.socket.emit('emergency', {
61 emergency,
62 location,
63 cohortName: this.cohortName,
64 });
65 }
66
67 reportStatus() {
68 this.socket.emit('cohort-status', {
69 cohortName: this.cohortName,
70 legion: Math.floor(Math.random() * 50) + 10,
71 supplies: {
72 food: Math.floor(Math.random() * 100),
73 water: Math.floor(Math.random() * 100),
74 ammunition: Math.floor(Math.random() * 50),
75 },
76 location: {
77 lat: Math.random() * 180 - 90,
78 lng: Math.random() * 360 - 180,
79 },
80 });
81 }
82}
83
84// Użycie
85const blackEquestris = new LegionClient('Equus Niger');
86blackEquestris.connect();
87
88setTimeout(() => {
89 blackEquestris.sendMessage('Wszystko w porządku, kontynuujemy kampania!');
90}, 2000);
91
92setTimeout(() => {
93 blackEquestris.sendEmergency('Atak barbarzyńców!', 'Mare Nostrum');
94}, 5000);

Testowanie WebSockets

1// cohort-communication.gateway.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { INestApplication } from '@nestjs/common';
4import { Socket, io } from 'socket.io-client';
5import { LegionCommunicationGateway } from './cohort-communication.gateway';
6
7describe('LegionCommunicationGateway', () => {
8 let app: INestApplication;
9 let gateway: LegionCommunicationGateway;
10 let clientSocket: Socket;
11
12 beforeAll(async () => {
13 const moduleFixture: TestingModule = await Test.createTestingModule({
14 providers: [LegionCommunicationGateway],
15 }).compile();
16
17 app = moduleFixture.createNestApplication();
18 gateway = moduleFixture.get<LegionCommunicationGateway>(LegionCommunicationGateway);
19 await app.listen(3001);
20
21 clientSocket = io('ws://localhost:3001', {
22 autoConnect: false,
23 });
24 });
25
26 afterAll(async () => {
27 await app.close();
28 });
29
30 afterEach(() => {
31 clientSocket.disconnect();
32 });
33
34 it('should connect and receive welcome message', (done) => {
35 clientSocket.connect();
36 
37 clientSocket.on('welcome', (data) => {
38 expect(data.message).toBe('Witaj w komunikacji legionu!');
39 expect(data.cohortId).toBeDefined();
40 done();
41 });
42 });
43
44 it('should broadcast cohort message to all clients', (done) => {
45 const testMessage = 'Test message from Equus Niger';
46 
47 clientSocket.connect();
48 
49 clientSocket.on('legion-message', (data) => {
50 expect(data.message).toBe(testMessage);
51 expect(data.from).toBe('Equus Niger');
52 done();
53 });
54 
55 clientSocket.emit('cohort-message', {
56 message: testMessage,
57 cohortName: 'Equus Niger',
58 });
59 });
60
61 it('should handle emergency broadcasts', (done) => {
62 clientSocket.connect();
63 
64 clientSocket.on('legion-emergency', (data) => {
65 expect(data.emergency).toBe('Hydra attack!');
66 expect(data.priority).toBe('CRITICAL');
67 done();
68 });
69 
70 clientSocket.emit('emergency', {
71 emergency: 'Hydra attack!',
72 location: 'Hispania Sea',
73 cohortName: 'Equus Niger',
74 });
75 });
76});

Najlepsze praktyki WebSockets

1. Obsługa błędów połączenia

1@WebSocketGateway()
2export class RobustCommunicationGateway {
3 @WebSocketServer()
4 server: Server;
5 
6 private readonly logger = new Logger(RobustCommunicationGateway.name);
7 private heartbeatInterval: NodeJS.Timeout;
8
9 afterInit() {
10 this.logger.log('Gateway zainicjalizowany');
11 
12 // Heartbeat co 30 sekund
13 this.heartbeatInterval = setInterval(() => {
14 this.server.emit('heartbeat', { timestamp: new Date().toISOString() });
15 }, 30000);
16 }
17
18 handleConnection(client: Socket) {
19 try {
20 this.logger.log(`Nowe połączenie: ${client.id}`);
21 
22 // Timeout dla nieaktywnych połączeń
23 const timeout = setTimeout(() => {
24 this.logger.warn(`Timeout dla klienta: ${client.id}`);
25 client.disconnect();
26 }, 300000); // 5 minut
27 
28 client.data.timeout = timeout;
29 
30 } catch (error) {
31 this.logger.error('Błąd podczas połączenia:', error);
32 client.disconnect();
33 }
34 }
35
36 handleDisconnect(client: Socket) {
37 if (client.data.timeout) {
38 clearTimeout(client.data.timeout);
39 }
40 this.logger.log(`Klient rozłączony: ${client.id}`);
41 }
42
43 @SubscribeMessage('ping')
44 handlePing(@ConnectedSocket() client: Socket) {
45 // Reset timeout
46 if (client.data.timeout) {
47 clearTimeout(client.data.timeout);
48 client.data.timeout = setTimeout(() => {
49 client.disconnect();
50 }, 300000);
51 }
52 
53 return { pong: new Date().toISOString() };
54 }
55}

2. Rate Limiting

1import { RateLimiterMemory } from 'rate-limiter-flexible';
2
3@WebSocketGateway()
4export class RateLimitedGateway {
5 private rateLimiter = new RateLimiterMemory({
6 points: 10, // Liczba żądań
7 duration: 60, // Na 60 sekund
8 });
9
10 @SubscribeMessage('cohort-message')
11 async handleRateLimitedMessage(
12 @MessageBody() data: any,
13 @ConnectedSocket() client: Socket,
14 ) {
15 try {
16 await this.rateLimiter.consume(client.id);
17 
18 // Przetwórz wiadomość
19 return this.processMessage(data);
20 
21 } catch (rateLimiterRes) {
22 client.emit('rate-limit-exceeded', {
23 error: 'Za dużo wiadomości! Zwolnij tempo!',
24 retryAfter: rateLimiterRes.msBeforeNext,
25 });
26 }
27 }
28}

WebSockets w NestJS to potężne narzędzie do budowania aplikacji real-time. Pozwalają na natychmiastową komunikację między kohortami legionu, co jest kluczowe dla koordynacji operacji rzymskich!

W następnym module poznamy zaawansowane techniki obsługi eventów i broadcasting!

Przejdź do CodeWorlds