===== Instalacja bibliotek deweloperskich w laboratoriach ===== Aby zainstalować potrzebne pakiety na __Linuksie SK prog__, należy wykonać:
su apt update apt install libsfml-dev Aby zainstalować potrzebne pakiety na __Linuksie lokalnym__, należy wykonać:
sudo zypper install -y sfml2-devel ====== Dokumentacja SFML ====== [[https://www.sfml-dev.org/tutorials/2.6/#network-module|Zbiór samouczków SFML]] [[https://www.sfml-dev.org/documentation/2.6.1/group__network.php|API klas sieciowych]] ====== Ogólne informacje ====== ===== SFML ===== [[https://sfml-dev.org|SFML]] jest prostym zestawem bibliotek do rysowania okienek, opakowywania okna dla OpenGL, obsługi dźwięku i obsługi sieci. \\ "Prostym" należy rozumieć zarówno jako "łatwym do nauki" jak i "mającym funkcjonalność ograniczoną do wybranych, podstawowych rzeczy". ===== Wątki ===== SFML oferuje własne wątki, zamki, …, ale wg. oficjalnej dokumentacji te klasy ustępują bibliotece standardowej C+%%%%+:
If you work with compilers that support the new standard and its header, forget about the SFML thread classes and use it instead -- it will be much better. But if you work with a pre-2011 compiler, or plan to distribute your code and want it to be fully portable, the SFML threading classes are a good solution.
(źródło: [[https://www.sfml-dev.org/tutorials/2.6/system-thread.php|SFML tutorial on threads]])
===== Pętla zdarzeń ===== W SFML do obsługi GUI programista musi ręcznie zbudować pętlę zdarzeń. Zarówno oficjalne materiały jak i literatura polecana ze strony biblioteki promują użycie aktywnego czekania, tzn. aplikacja będzie zużywać procesor nawet jeśli nic się nie dzieje. Aby uniknąć nadmiernego użycia procesora, można dodawać co jakiś czas sleepy; w SFML jest to "oficjalnie" opakowne w funkcję ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1Window.php#af4322d315baf93405bf0d5087ad5e784|setFramerateLimit]]''. Mimo posiadania modułu do obsługi połączeń sieciowych, funkcja ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1WindowBase.php#a6a143de089c8716bd42c38c781268f7f|pollEvent]]'' nie wspiera zdarzeń z gniazd. \\ **Obsługę gniazd w pętli zdarzeń należy zbudować ręcznie.** ===== Budowanie kodu i CMake ===== Aby zbudować kod używający SFML, należy dodać do opcji kompilatora łączenie z odpowiednimi bibliotekami: \\ ''sfml-system'', ''sfml-graphics'', ''sfml-network'', ''sfml-audio'', ''sfml-window''. Ręcznie, z linii poleceń można podać (np. dla programu potrzebującego modułów graphics i network):
c++ prog.cpp -o prog -lsfml-graphics -lsfml-network Dla systemu automatyzacji budowania CMake należy do pliku ''CMakeLists.txt'' dodać:
find_package(SFML 2.5 COMPONENTS system network REQUIRED) target_link_libraries(prog … sfml-system sfml-network … )
SFML __nie__ ustawia zmiennych zawierających listę modułów do linkowania, trzeba je podać ręcznie. Przykład kompletnego pliku ''CMakeLists.txt'':
cmake_minimum_required(VERSION 3.12) project(myProg) set(CMAKE_CXX_STANDARD 20) find_package(SFML 2.5 COMPONENTS graphics system network REQUIRED) add_executable(myProg main.cpp window.h window.cpp) target_link_libraries(myProg sfml-graphics sfml-network) ====== Moduł obsługi sieci ====== Klasy do obsługi sieci są dostępne po dodaniu do kodu dyrektywy:
#include ===== Adresy IP ===== **Uwaga: SFML obsługuje tylko IPv4** Adres IP jest reprezentowany przez klasę ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1IpAddress.php#details|sf::IpAddress]]'' \\ Ta klasa ma konstruktor przyjmujący ''std::string'' / ''char*'' akceptujący zapisany jako tekst adres IP **lub nazwę domenową**. \\ Wersja rozwojowa SFML zamiast konstruktora używa metody statycznej ''[[https://github.com/SFML/SFML/blob/master/include/SFML/Network/IpAddress.hpp#L270|sf::IpAddress::resolve]]'' ===== Klasy gniazd ===== [[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1TcpListener.php|TcpListener]] - serwer TCP: * ''listen'' - rozpoczyna nasłuchiwanie; przyjmuje przynajmniej numer portu * ''accept'' - przyjmuje następne połączenie * ''close'' - zamyka gniazdo [[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1TcpSocket.php|TcpSocket]] - klient TCP: * ''connect'' - nawiązuje połączenie; przyjmuje przynajmniej adres IP i numer portu * ''send'' - wysyła dane * ''receive'' - odbiera dane * ''disconnect'' - rozłącza i zamyka gniazdo [[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1UdpSocket.php|UdpSocket]] - gniazdo UDP: * ''bind'' - ustala lokalny adres; przyjmuje przynajmniej numer portu * ''send'' - wysyła dane * ''receive'' - odbiera dane **Uwaga:** klasy oferowane przez SFML nie mają konstruktorów kopiujących (//copy constuctor//) __ani przesuwających (//move constructor//)__. ==== Tryb nieblokujący ==== Wszystkie klasy udostępniają metodę ''setBlocking'' pozwalającą przestawić gniazdo w tryb nieblokujący. ==== Konwencje ==== Operacje sieciowe zwracają wartość z wyliczenia ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1Socket.php#a51bf0fd51057b98a10fbb866246176dc|sf::Socket::Status]]'': * ''Done'' jeżeli wszystko się powiodło, * ''NotReady'' lub ''Partial'' dla odpowiednich wyników działania gniazd w trybie nieblokującym((Choć czasami mimo odebrania części danych funkcje odbierające zwracają ''NotReady'')), * ''Disconnected'' jeśli gniazdo nie jest bądź przestało być połączone((Szczegóły w [[https://github.com/SFML/SFML/blob/2.6.x/src/SFML/Network/Unix/SocketImpl.cpp#L89|implementacji]] biblioteki)), * ''Error'' jeśli wystąpił błąd (inny niż raportowany jako ''Disconnected''). **Uwaga:** operacje takie jak ''accept'', ''send'', ''receive'' też zwracają ''sf::Socket::Status''. \\ Odpowiednio nowe połączenie, ilość wysłanych / odebranych bajtów jest wpisywana do dodatkowego argumentu przekazywanego przez referencję, np:
char buffer[1024]; size_t receivedBytes; if (sock.receive(buffer, 1024, receivedBytes) == sf::Socket::Done) { sf::String receivedBufferAsString(std::string(buffer, receivedBytes)); (…)
std::unordered_map clients; if (servSock.accept(clients[clientIdSequencer]) == sf::Socket::Done) { int clientId = clientIdSequencer++; clients[clientId].send(helloPacket); (…)
==== Zadania ==== ~~Zadanie.#~~ Napisz (wielowątkowo) prostego konsolowego klienta TCP, który jako argumenty przyjmuje docelową nazwę hosta i numer portu. Klient ma jednocześnie odbierać dane z sieci i czytać dane ze standardowego wejścia. ~~Zadanie.#~~ Napisz wielowątkowy serwer czatu. ===== sf::Packet - serializacja wiadomości ===== SFML oferuje klasę ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1Packet.php|sf::Packet]]'' która reprezentuje logiczną wiadomość, pozwala strumieniowo dodawać dane do wiadomości i współpracuje z metodami ''send'' i ''receive'' gniazd. \\ Dla gniazd TCP wysłanie pakietu używając metody ''send'' wysyła przed danymi ich długość w postaci 4-ro bajtowej liczby w notacji big endian ([[https://github.com/SFML/SFML/blob/2.6.x/src/SFML/Network/TcpSocket.cpp#L323|kod]]). \\ Uwaga: to jest szczegół implementacji bieżącej wersji SFML; API bibliotek nie określa w jaki sposób wiadomości są przesyłane. Przykładowo, do wysłania i odbioru danych można:
enum MessageType : sf::Uint8 { Foo = 1, Bar, Baz }; struct Message { Message(MessageType type) : type(type){}; virtual ~Message() {} const MessageType type; }; struct FooMessage : public Message { FooMessage() : Message(MessageType::Foo){}; sf::String text; sf::Uint32 number; }; (…) // wysłanie wiadomości FooMessage m; sf::Packet packet; packet << m.type << m.text << m.number; socket.send(packet); (…) // odebranie wiadomości sf::Packet packet; socket.receive(packet); // (pominięto obsługę błędów) sf::Uint8 type; packet >> type; switch (type) { case MessageType::Foo: { FooMessage m; packet >> m.text >> m.number; (…)
Dla poprawienia modularności i czytelności kodu można dodać własne operatory:
sf::Packet &operator<<(sf::Packet &packet, const FooMessage &msg) { packet << msg.type << msg.text << msg.number; return packet; } sf::Packet &operator>>(sf::Packet &packet, FooMessage &msg) { packet >> msg.text >> msg.number; return packet; } (…) // wysłanie wiadomości FooMessage m; sf::Packet packet; packet << m; socket.send(packet); (…) // odebranie wiadomości sf::Packet packet; socket.receive(packet); // (pominięto obsługę błędów) sf::Uint8 type; packet >> type; switch (type) { case MessageType::Foo: { FooMessage m; packet >> m; (…)
==== Zadania ==== ~~Zadanie.#~~ Wyjaśnij dlaczego ''operator<<'' zapisuje typ wiadomości, ale ''operator>>'' go nie odczytuje. ~~Zadanie.#~~ Stwórz strukturę którą mógłby używać serwer czatu żeby przesyłać do klientów wiadomość z nazwą użytkownika i treścią wiadomości. Napisz odpowiednie operatory pozwalające wpisać tą strukturę w ''sf::Packet''. ~~Zadanie.#~~ Użyj tej struktury w kodzie klienta i serwera z zadania 1 i 2 ==== sf::Packet i tryb nieblokujący ==== Żeby odebrać ''sf::Packet'' w trybie nieblokującym, należy powtarzać wykonanie funkcji ''receive'' do czasu otrzymania w wyniku statusu ''Done''. \\ Klasa ''sf::TcpSocket'' w prywatnym polu przechowuje częściowe dane przychodzącego pakietu ([[https://github.com/SFML/SFML/blob/2.6.x/include/SFML/Network/TcpSocket.hpp#L219|kod1]], [[https://github.com/SFML/SFML/blob/2.6.x/src/SFML/Network/TcpSocket.cpp#L379|kod2]]). \\ Pakiet podany jako argument jest wypełniany danymi tylko w ostatnim wywołaniu funkcji ''receive''. Podobnie żeby wysłać ''sf::Packet'' w trybie nieblokującym należy podawać jako argument metody ''send'' ten sam pakiet do czasu otrzymania statusu ''Done''. \\ **Uwaga:** to, ile bajtów udało się wysłać zapisywane jest w przekazanym obiekcie z klasy ''sf::Packet'' ([[https://github.com/SFML/SFML/blob/2.6.x/src/SFML/Network/TcpSocket.cpp#L349|kod]]). \\ **Nie wolno tego samego obiektu pakietu wysyłać nieblokująco do wielu gniazd!** ===== Oczekiwanie na zdarzenie na gniazdach ===== SFML wspiera oczekiwanie na przyjście danych na podanej liście gniazd przez obiektowe opakowanie funkcji ''select'' przez klasę ''[[https://www.sfml-dev.org/documentation/2.6.1/classsf_1_1SocketSelector.php|sf::SocketSelector]]''. Uwaga: jeżeli ''SocketSelector'' zwrócił gotowość odczytu na gnieździe, to na tym gnieździe wywołana w trybie blokującym metoda ''receive(void*, std::size_t, std::size_t&)'' się nie zablokuje, ale **metoda ''receive(Packet&)'' dalej może się zablokować**(( ''receive(Packet&)'' wywołuje wewnętrznie ''receive(void*, std::size_t, std::size_t&)'' do czasu odebrania całego pakietu)). SFML nie wspiera czekania na możliwość wysłania kolejnej porcji danych ([[https://github.com/SFML/SFML/blob/2.6.x/src/SFML/Network/SocketSelector.cpp#L168|kod]]). API klasy ''sf::SocketSelector'' definiuje metody: * ''add'' / ''remove'' – dodaje/usuwa gniazdo do/ze zbioru monitorowanych, * ''wait'' – wywołuje systemową funkcję ''select'' dla wcześniej dodanych gniazd; opcjonalnie przyjmuje limit czasu oczekiwania, * ''isReady'' – sprawdza czy podane jako argument gniazdo było gotowe w momencie ostatniego wykonania ''wait''. **Uwaga:** ani zamknięcie ani usunięcie gniazda nie usuwa go z ''SocketSelector''a, trzeba to zrobić ręcznie - inaczej implementacja ''SocketSelector''a nie zauważy że występuje błąd i po prostu nie będzie działać. Przykład użycia klasy ''sf::SocketSelector'' do stworzenia serwera czatu:
#include #include int main(int, char **argv) { sf::TcpListener srvSock; std::list clients; srvSock.listen(atoi(argv[1])); sf::SocketSelector selector; selector.add(srvSock); while (true) { selector.wait(); if (selector.isReady(srvSock)) { clients.emplace_back(); srvSock.accept(clients.back()); selector.add(clients.back()); } decltype(clients)::iterator curr, next = clients.begin(); while ((curr = next++) != clients.end()) { if (!selector.isReady(*curr)) continue; char data[1024]; size_t recvCnt; if (curr->receive(data, 1024, recvCnt) != sf::Socket::Done) { selector.remove(*curr); clients.erase(curr); continue; } for (auto &c : clients) if (&c != &(*curr)) c.send(data, recvCnt); } } }
===== HTTP, FTP ===== SFML oferuje też klasy do wysłania żądań HTTP i obsługi FTP, ale nie obsługuje powszechnego dziś dla protokołu HTTP szyfrowania (tzn. nie zadziała żadna strona z adresem zaczynającym się od ''https:/%%%%/''). ===== Przykład z GUI ===== Kod uproszczonego graficznego klienta TCP znajduje się tutaj: {{sfml-tcp-client.tar.xz}}. \\ Na zajęciach nie ma zadań z graficznymi programami w SFML ze względu na dużą objętość kodu który trzeba napisać żeby cokolwiek osiągnąć w tej bibliotece. Powyższy przykład ma 277 linii kodu. Dla porównania daleko bardziej funkcjonalny klient TCP w Qt (z poprzednich zajęć) ma 115 linii kodu i to już okazało się dla części osób zaporową liczbą.