Skip to content

Budowa Uniwersalnego Serwera WebSocket dla 7u.pl

Serwer WebSocket

W miarę jak dodajemy do ekosystemu 7u.pl coraz więcej interaktywnych mikro-narzędzi, pojawiła się potrzeba synchronizacji stanu w czasie rzeczywistym między kompletnie odseparowanymi urządzeniami.

Przykład: nasz projekt virtual-keyboard musi przesyłać zdarzenia ("naciśnięto przycisk Q") ze smartfona, przeglądarki na Macu, wprost do fizycznego Raspberry Pi, z ominięciem ograniczeń sieci lokalnej obu stron.

Zamiast budować logikę WebSocketów osobno dla każdej aplikacji (co szybko doprowadziłoby do powielania infrastruktury), postanowiliśmy stworzyć jeden, niezależny, bardzo "głupi" serwer komunikacyjny, który robi tylko jedną rzecz, ale robi ją bezbłędnie: nasłuchuje na pokoje (rooms) i rozsyła wiadomości (broadcast) pomiędzy klientami.

Architektura "Głupiego" Serwera

Kluczem do uniwersalności jest brak logiki biznesowej po stronie serwera. Serwer nie wie, czym jest "klawiatura", co to jest "myszka" czy jakiej struktury ma wysyłany po sieci JSON.

Wybór technologii

Zdecydowaliśmy się na Node.js w połączeniu z Socket.IO. Chociaż natywne WebSockety (ws) są lżejsze, Socket.IO ma wbudowany świetny mechanizm organizowania połączeń w tzw. Rooms, co idealnie odpowiada naszym potrzebom izolowania różnych aplikacji. Oprócz tego zdejmuje nam z głowy problem automatycznych re-connectów na urządzeniach mobilnych z niestabilnym łączem.

Kod źródłowy w pigułce

Rdzeń całego serwera zamyka się w zaledwie kilku nasłuchiwaczach na instancji io:

javascript
io.on('connection', (socket) => {
  // Dołącz klienta do wirtualnego pokoju
  socket.on('join-room', (roomName) => {
    socket.join(roomName);
  });

  // Rozgłoś wiadomość do innych w pokoju (pomija nadawcę)
  socket.on('broadcast', ({ roomName, eventName, payload }) => {
    socket.to(roomName).emit(eventName, payload);
  });
  
  // Rozgłoś wiadomość do wszystkich w pokoju (w tym nadawcy)
  socket.on('broadcast-all', ({ roomName, eventName, payload }) => {
    io.to(roomName).emit(eventName, payload);
  });
});

To dosłownie wszystko. Serwer przyjmuje dowolny obiekt (payload), znajduje pokój (roomName), opakowuje go we wskazane przez dewelopera zdarzenie (eventName) i podrzuca reszcie klientów.

Bezpieczeństwo poprzez CORS

Mimo że serwer nic nie wie o samych przesyłanych danych, chcieliśmy, aby nasz publiczny endpoint nie zamienił się w darmowy serwer proxy dla połowy internetu.

Implementacja wymagała ścisłej reguły walidacji nagłówka Origin, aby weryfikować połącznie na najwcześniejszym etapie tzw. handshake'u:

javascript
const io = new Server(httpServer, {
  cors: {
    origin: (origin, callback) => {
      // Przepuść brak Origin (urządzenia IoT/skrypty)
      if (!origin) return callback(null, true);

      // Dozwolone: *.7u.pl oraz środowiska lokalne
      const allowedOrigins = [
        /^https?:\/\/((.*)\.)?7u\.pl$/,
        /^http:\/\/localhost(:\d+)?$/,
        /^http:\/\/127\.0\.0\.1(:\d+)?$/
      ];

      if (allowedOrigins.some((regex) => regex.test(origin))) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    }
  }
});

Dzięki użyciu Regex-ów zamiast sztywnych stringów, domeny takie jak sklep.7u.pl, jak również porty developerskie (np. localhost:5173 używane przez ekosystem Vite) są autoryzowane dynamicznie, bez potrzeby restartowania i rekonfiguracji serwera przy każdym nowym projekcie.

Wdrażanie i Utrzymanie (Docker)

Serwer waży zaledwie parę megabajtów. Obraz Dockera został zbudowany w oparciu o środowisko node:20-alpine.

dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
EXPOSE 80
CMD ["npm", "start"]

Na maszynie produkcyjnej obraz jest schowany za load balancerem (NGINX/Cloudflare), który obsługuje ostateczną terminację certyfikatów SSL (wss://sock.7u.pl). Taki setup powoduje, że nowa infrastruktura stała się kompletnie niewidoczna ("transparentna") dla przyszłych modyfikacji.

Podsumowanie i Korzyści

Przeniesienie punktu zwrotnego ("relay") WebSocketów do oddzielnego repozytorium uwolniło nas od:

  1. Posiadania backendów zmuszonych nasłuchiwać portów, tylko po to, by klienty "frontend-only" mogły się zobaczyć,
  2. Tworzenia od nowa systemów autentykacji,
  3. Utrzymywania w projektach niepotrzebnego długu technicznego.

Od teraz każdy nadchodzący interaktywny projekt pod domeną *.7u.pl otrzyma funkcjonalność Real-Time po dodaniu pięciu linijek importu. A o to przecież w dobrej architekturze chodzi.