16 lutego 2019

Tutorial: Wykonujemy Chat wykorzystując WebSocket cz.1

Wpadłem ostatnio na pomysł wykorzystania Websocketow, najlepszym pomysłem na ich wykorzystanie jest czat. Zacząć należy od wybrania odpowiedniej dla nas biblioteki.

Moja decyzja co do biblioteki padła na Ratchet i ją polecam. Zaczynamy!

Instalacja biblioteki

Aby zacząć z biblioteka Ratchet wykonujemy poniższe polecenie:

php composer require cboden/ratchet

Tworzenie serwera php

Tworzymy nowy plik, ja nazwałem go cmd.php (uruchamiany będzie z poziomu konsoli). Posłuży nam do obsługi czatu od strony serwera.

Poniżej wklejam gotowy plik /cmd.php

<?php
require 'vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use ChatApp\Chat;
use ChatApp\CheckMessage\Check;
use ChatApp\Preparation\PreparationMessage;

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Chat()
        )
    ),
    8080
);
$server->run();

Powyższy plik wywołuje metody pobrane z biblioteki Ratchet, lecz są również stworzone inne klasy wewnątrz ChatApp nimi zajmiemy się w dalszej części wpisu.

Serce programu, czyli klasa Chat

Zacznijmy od serca programu, a jest nim klasa „Chat

class/ChatApp/Chat.php > __construct

    public function __construct()
    {
        $this->clients = new \SplObjectStorage;
        $this->logs = [];
        $this->connectedUsers = [];
        $this->connectedUsersNames = [];
        $this->connectedUsersId = [];
        $this->checkMessage = new CheckMessage\Check;
        $this->preparationMessage = new PreparationMessage\Preparation;
    }

Klasa „Chat” jest dość długa, więc opiszę metody z osobna.

W powyższym konstruktorze:
$this->clients to obiekt klasy należącej do biblioteki Ratchet.
$this->logs wykorzystamy aby ładować do niego całą akcje użytkownika (czas wysłania wiadomości oraz treść wiadomości, id użytkownika itd.)
$this->connectedUsers tutaj znajdą się wszyscy użytkownicy czatu
$this->connectedUsersNames to samo co wyżej natomiast indeksem tablicy będzie id użytkownika, wartością jego nick
$this->connectedUsersId kolejny raz to samo co $this->connectedUsersNames, lecz tutaj indeksem będzie nick użytkownika, a wartością będzie jego id.

Zabieg tworzenia dwóch tablic connectedUsersId oraz connectedUsersNames ma na celu uniknięcie nadmiernego tworzenia pętli i przeszukiwania ich.

$this->checkMessage obiekt klasy która sprawdza czy wpisana wiadomość od użytkownika to na pewno wiadomość wysłana do wszystkich, może to polecenie jakieś do programu, a może wiadomość prywatna.
$this->preparationMessage obiekt klasy która przygotowuje wiadomość do wysłania i ostatecznie wyświetlenia, czyli np. usuwa część znaków

class/ChatApp/Chat.php > onMessage

public function onMessage(ConnectionInterface $from, $msg)
    {
        //użytkownik istnieje
        if (isset($this->connectedUsersNames[$from->resourceId])) {
            echo "\n" . $this->connectedUsersNames[$from->resourceId] . ' : ' . $msg;

            $this->logs = array(
                "user" => $this->connectedUsersNames[$from->resourceId],
                "user_id" => $from->resourceId,
                "msg" => $this->preparationMessage->start($msg),
                "all_users" => $this->connectedUsersNames,
                "timestamp" => time(),
                "nick" => $this->checkMessage->getNick($msg, $this->connectedUsersNames),
            );
           
            $this->sendMessage($this->logs, null);
        } else {
            //użytkownik nie istnieje, świeżo zalogowany
            echo "\nZalogował się: " . $msg;
            $this->connectedUsersId[$msg] = $from->resourceId;
            $this->connectedUsersNames[$from->resourceId] = $msg;
        }
    }

Jeśli użytkownik napisał wiadomość, wywołujemy metodę onMessage, przyjmuje ona parametr $from (obiek z danymi użytkownika) oraz $msg (string z treścią wiadomości).

Za pomocą warunku

if (isset($this->connectedUsersNames[$from->resourceId]))

Sprawdzamy czy użytkownik istnieje u nas w tablicy connectedUsersNames jeśli tak, to tworzymy tablicę $this->log i przekazujemy ją do wysłania $sendMessag.

W przeciwnym razie tworzymy nowe indeksy i w nich wartości w tablicach które opisałem w konstruktorze. A są to $this->connectedUsersId oraz $this->connectedUsersNames. Oznacza to, że użytkownik wysłał wiadomość, lecz dla niego był to pierwszy ekran programu w którym się loguje (podaje swój nick).

W tym miejscu powinienem wspomnieć o metodzie onOpen

class/ChatApp/Chat.php > onOpen

public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);
        $conn->send(json_encode($this->logs));
        $this->connectedUsers [$conn->resourceId] = $conn;
    }

Użycie metody onOpen wymusza na nas sama biblioteka Ratchet. W powyższej metodzie tak na prawdę odbywa się logowanie użytkownika, lecz na tym etapie nie znamy jego pierwszej wiadomości którą jest podanie nicku, dla tego nie tworzę tutaj tablic connectedUsersId oraz connectedUsersNames. Jego nick poznaję w pierwszej wiadomości, czyli w pierwszym wywołaniu metody onMessage.

class/ChatApp/Chat.php > onClose

public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);
        unset($this->connectedUsersId[$this->connectedUsersNames[$conn->resourceId]]);
        unset($this->connectedUsersNames[$conn->resourceId]);
        unset($this->connectedUsers[$conn->resourceId]);
    }

Metoda zostanie wywołana w sytuacji gdy użytkownik wyłączy okno przeglądarki. Więc wywołamy w niej metodę $this->clients->detach aby usunąć cały $conn który stworzony został przy zalogowaniu się, w metodzie onClose oraz usuwamy nasze indeksy w tablicach.

class/ChatApp/Chat.php > sendMessage

private function sendMessage(array $message)
    {
        $jsonMessage = json_encode(array($message));
        if (!$message['nick']) {
            foreach ($this->connectedUsers as $i => $user) {
                $user->send($jsonMessage);
            }
        } else {
            foreach ($message['nick'] as $nick) {
                if (isset($this->connectedUsersId[$nick])) {
                    $this->connectedUsers[$this->connectedUsersId[$nick]]->send($jsonMessage);
                }
            }
            $this->connectedUsers[$message['user_id']]->send($jsonMessage);
        }
    }

W powyższej funkcji wysyłamy po prostu wiadomość na czacie. Lecz wiadomość która ma zostać wysłana, musi zostać sprowadzona do takiej postaci: $jsonMessage = json_encode(array($message));

Jeśli nie mamy w tablicy $message[‚nick’] wpisanych nazw użytkowników, oznacza to że wiadomość wysłać chcemy do wszystkich, więc pozostaje nam pętla foreach na użytkownikach wszystkich zalogowanych do czatu oraz dla każdego z tych użytkowników wywołanie metody send.

Jeśli zaś w tablicy $message[‚nick’] isteniają pseudonimy użytkowników, robimy znowu foreach, lecz nie na wszystkich użytkownikach zalogowanych a tylko na tych z tablicy $message[‚nick’]. Wewnątrz pętli musimy jeszcze sprawdzić czy nick który jest w tablicy $message[‚nick’] na pewno istnieje w connectedUsers i ostatecznie wywołać metodę send.

Podsumowanie

Jest to pierwsza część tutoriala, ponieważ temat jest zbyt długi aby opisać go w jednym wpisie.

Drugą częścią będzie przedstawienie dwóch klas odpowiedzialnych za przygotowanie wiadomości do wysłania, odseparowanie nicków do których chcemy ową wiadomość wysłać.

Trzecią częścią jest klient.

Projekt możecie zobaczyć tutaj a działający czat wypróbować tutaj.