A Simple publish-subscribe messaging app built on top of TCP sockets with a linux server and an Electron client
Konrad Szychowiak
Julia Auguścik
Wiadomości przesyłane przez tcp mają następującą postać: (wiadomości są przesyłane jako tekst)
-
Każda wiadomość zaczyna się od symbolu typu komunikatu i kończy znakiem
;
<wiadomość> := <symbol-typu> <lista-argumentów> ;
-
Po symbolu znajdują się argumenty, zależne of typu:
<lista-argumentów> := \t string | \t string <lista-argumentów>
argumenty są ciągami znaków oddzielonymi od siebie znakiem tabluacji
\t
symbol | rozwinięcie | argumenty | wysyła |
---|---|---|---|
C |
Create conversation | name: string – tytuł konwersacji, uuid: string – identyfikator nadany przez autora |
klient |
D |
Delete conversation | id: int – identyfikator konwersacji |
klient |
S |
Subscribe | id: int – identyfikator konwersacji |
klient |
U |
Unsubscribe | id: int – identyfikator konwersacji |
klient |
P |
Post message | id: int – identyfikator konwersacji, content: string – treść wiadomości |
klient |
N |
New message (created) | id: int – identyfikator konwersacji, content: string – treść wiadomości |
serwer |
L |
List of conversations | ciąg następujących po sobie trójek: id: int – identyfikator konwersacji, name: string – tytuł konwersacji, uuid: string (j/w) |
serwer |
id
oznacza identyfikator taki, jaki trzyma i używa serwerint
oznacza, że przesłany ciąg znaków interpretowany będzie jako liczba
- po stronie klienta interpretacja przychodzących wiadomości ma miejsce w
TcpMiddleware.onData()
w plikuclient/src/tcp/TcpMiddleware.ts
a wiadomości tworzone są przezPack(.ts)
- po stronie klienta wiadomosci odbiera
ThreadBehavior
wserver/src/main.cpp
a wysyłają klasy oparte na klasieListener
na podstawie wiadomości stworzonych przezVisitor
-ów.
Serwer [C++20]
- Wątek główny (funkcja
main
) uruchamia serwer i rozpoczyna nasłuchiwanie.- serwer tworzony jest przez klasę
Server
opartą na klasieSocket
- serwer odbiera połącznie używając funkcji
accept()
i przekazuje sterowanie do funkcjiconnectionHandlerFactory
- serwer tworzony jest przez klasę
connectionHandlerFactory
zapisuje do strukturythread_data_t
deskryptor gniazda połączenia a następnie uruchamia funkcjęThreadBehavior
w nowym wątkuThreadBehavior
odpowiada za obsługę operacji wykonywanych przez pojedynczego klienta:- rejestruje
ConversationsListener
, który zostanie powiadomiony przy każdej zmainie w liście konwersacji (dodanie/usunięcie) - nasłuchuje w pętli na komunikaty przychodzące na socket używając funckji
read()
a następnie wykonuje odpowiednie akcje:- Subscribe: wyszukuje żądaną konwersację i tworzy dla niej
MessagesListener
– klasę która zostaje powiadomiona przy publikacji wiadomości - Unsubscribe: usuwa
MessagesListener
dla podanej konwersacji - Post message: dodaje wiadomość do wskazanej konwersacji i powiadamia o tym wszystkich
MessagesListener
-ów. - Create conversation: jeżli klient utworzy konwersację,
ThreadBehavior
dodaje ją do globalnego stanu przechowującego konwersacje i powiadamia wszystkich podłączonych klientów - Delete conversation: usuwa wskazaną konwersację i powiadamia o tym
- Subscribe: wyszukuje żądaną konwersację i tworzy dla niej
- rejestruje
- Po zakończeniu połączenia
ThreadBehavior
usuwa wszystkie stworzone przez siebie konwersacje iListener
-y oraz zamyka gniazdo połączenia- stworzone konwersacje są pamiętane w
createdConversations: vector<Conversation *>
- zarejestrowane
Listener
-y są pamiętane wcreatedListeners: map<int, MessagesListener *>
oraz wconversationsListener: ConversationsListener*
- stworzone konwersacje są pamiętane w
Klient [ElectronJS]
src/main.ts
uruchomiony zostaje w głównym procesie Electrona.src/renderer/renderer.js
zostaje uruchomiony w osobnym procesami odpowiedzialnym za wyrenderowanie strony w html stanowiącej interfejs graficzny- Komunikacja między tymi dwoma procesami odbywa się za pomocą
Event
-ów (jest to mechanizm wbudowany w Electrona). - Gdy użytkownik poda dane połączenia (w procesie renderera) generowany jest odpowiedni event i proces główny nawiązuje połączenie poprzez
net.Socket
. - Za każdym razem gdy użytkownik chce wykonać jakąś akcję generowany jest w rendererze event, na podstawie którego wysyłany jest komunikat poprzez
net.Socket.write()
- Komunikaty tworzone są przez klasę
Pack
- Komunikaty tworzone są przez klasę
- Za każdym razem gdy serwer wysyła jakieś dane
generowany jest event
'data'
który następnie zostaje obsłużony przez klasęTcpMiddleware
TcpMiddleware
używa dekoratora@reEmmit('event')
który powoduje przesłanie do renderera eventu o nazwie'event'
i danymi takimi jakie zwraca metoda – ułatwia to przekazanie rendererowi odpowiednio przetworzone dane- Stan konwersacji jest przechowywany w klasie
Store
(src/main/Store.ts), tam zapisywana jest lista konwersacji przekazana przez serwer i stamtąd jest odczytywana
Serwer wymaga
cmake
oraz kompilatora C++20, np. gnug++
# server/
cmake .
cmake --build .
# wygeneruje plik wykonywalny o nazwie `server`
./server
Serwer można także skompilować i uruchomić jako kontener dokerowy, używając pliku
Dockerfile
dostępnego w kataloguserver/
.
# server/
docker build -t tcp-server .
Klient bazuje na frameworku Electron, który jest wymagany do uruchomienia projektu. Dodatkowo wymagany jest nodeJS oraz npm.
# client/
npm install # instaluje wymagane zależności
npm start # uruchomi projekt na podstawie skryptu zawartego w package.json
# oraz stworzy katalog client/build/ zawierający skompilowany javascript
Projekt może zostać skompilowany do plików wykonywalnych używając np. electron forge lub electron builder. Skompilowane pliki wykonywalne znajdują się w repozytorium GitHub.