Narzędzia użytkownika

Narzędzia witryny


Pasek boczny

sk2:sfml

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

Ogólne informacje

SFML

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.

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ę 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.

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 <SFML/Network.hpp>

Adresy IP

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

Klasy gniazd

TcpListener - serwer TCP:

  • listen - rozpoczyna nasłuchiwanie; przyjmuje przynajmniej numer portu
  • accept - przyjmuje następne połączenie
  • close - zamyka gniazdo

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

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 sf::Socket::Status:

  • Done jeżeli wszystko się powiodło,
  • NotReady lub Partial dla odpowiednich wyników działania gniazd w trybie nieblokującym1),
  • 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ę, 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<int, sf::TcpSocket> clients;
if (servSock.accept(clients[clientIdSequencer]) == sf::Socket::Done) {
  int clientId = clientIdSequencer++;
  clients[clientId].send(helloPacket);
  ()

Zadania

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.

sf::Packet - serializacja wiadomości

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;
()

Zadania

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

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 (kod1, 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 (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ę 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 SocketSelectora, trzeba to zrobić ręcznie - inaczej implementacja SocketSelectora 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);
    }
  }
}

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ą.

1) Choć czasami mimo odebrania części danych funkcje odbierające zwracają NotReady
2) Szczegóły w implementacji biblioteki
3) receive(Packet&) wywołuje wewnętrznie receive(void*, std::size_t, std::size_t&) do czasu odebrania całego pakietu
sk2/sfml.txt · ostatnio zmienione: 2023/11/30 13:15 przez jkonczak