Dydaktyka:
FeedbackTo jest stara wersja strony!
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
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".
SFML oferuje własne wątki, zamki, …, ale wg. oficjalnej dokumentacji te klasy ustępują bibliotece standardowej C++:
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ąć aktywnego czekania, można dodawać co jakiś czas sleepy; w SFML jest
to "oficjalnie" opakowne w funkcję setFramerateLimit
Mimo posiadania modułu do obsługi połączeń sieciowych, funkcja pollEvent
nie wspiera zdarzeń z gniazd.
Obsługę gniazd w pętli zdarzeń należy zbudować ręcznie.
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.6 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.6 COMPONENTS graphics system network REQUIRED) add_executable(myProg main.cpp window.h window.cpp) target_link_libraries(myProg sfml-graphics sfml-network)
Klasy do obsługi sieci są dostępne po dodaniu do kodu dyrektywy:
#include <SFML/Network.hpp>
Uwaga: SFML obsługuje tylko IPv4
Adres IP jest reprezentowany przez klasę 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 sf::IpAddress::resolve
TcpListener - serwer TCP:
listen
- rozpoczyna nasłuchiwanie; przyjmuje przynajmniej numer portuaccept
- przyjmuje następne połączenieclose
- zamyka gniazdoTcpSocket - klient TCP:
connect
- nawiązuje połączenie; przyjmuje przynajmniej adres IP i numer portusend
- wysyła danereceive
- odbiera danedisconnect
- rozłącza i zamyka gniazdoUdpSocket - gniazdo UDP:
bind
- ustala lokalny adres; przyjmuje przynajmniej numer portusend
- wysyła danereceive
- odbiera dane
Wszystkie klasy udostępniają metodę setBlocking
pozwalającą przestawić gniazdo w tryb nieblokujący.
Operacje sieciowe zwracają wartość z wyliczenia sf::Socket::Status
:
Done
jeżeli wszystko się powiodło,Disconnected
jeśli gniazdo nie jest bądź przestało być połączone2),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ę.
Zadanie 1 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 2 Napisz wielowątkowy serwer czatu.
SFML oferuje klasę 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 (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; (…)
Zadanie 3 Wyjaśnij dlaczego operator<<
zapisuje typ wiadomości, ale
operator>>
go nie odczytuje.
Zadanie 4
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 5 Użyj tej struktury w kodzie klienta i serwera z zadania 1 i 2
Ż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 (kod).
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
(kod).
Nie wolno tego samego obiektu pakietu wysyłać nieblokująco do wielu gniazd!
SFML wspiera oczekiwanie na przyjście danych na podanej liście gniazd przez
obiektowe opakowanie funkcji select
przez klasę
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ć3).
SFML nie wspiera czekania na możliwość wysłania kolejnej porcji danych (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 <SFML/Network.hpp> #include <list> int main(int, char **argv) { sf::TcpListener srvSock; std::list<sf::TcpSocket> 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); } } }
SFML oferuje też klasy do wysłania żądań HTTP i obsługi FTP, ale nie obsługuje powszechnego dziś dla protokołu HTTP szyfrowania.
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ą.
NotReady
receive(Packet&)
wywołuje wewnętrznie receive(void*, std::size_t, std::size_t&)
do czasu odebrania całego pakietu