Większość przykładów w materiałach i programów pisanych na laboratoriach jest w C++.
Stąd przypominam jak obsługiwać kompilator GCC / clang w Linuksie.
Przykładowe polecenie do kompilacji przykładów ze strony:
g++ --std=c++20 -Wall -O0 -g -pthread -o example example.cpp
c++ | domyślny kompilator C++. Zwykle link do g++ lub clang++ |
c++ zrodlo.cpp -o prog | kompiluje plik zrodlo.cpp do programu prog |
c++ -Wall z.cpp -o p | włącza wszystkie1) ostrzeżenia kompilatora (-W = warn, all = wszystkie) |
c++ -O0 -g z.cpp -o p | wyłącza optymalizacje i dodaje do programu dane umożliwiające debugowanie |
c++ --std=c++20 z.cpp -o p | włącza używanie standardu ISO C++ z 2020 roku |
c++ -pthread z.cpp -o p | włącza obsługę wątków standardu POSIX wymagane do wersji glibc ≤ 2.33 [1] [2] |
Przykładowy plik cmake:
cmake_minimum_required(VERSION 3.12) project(projname) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() set(CMAKE_CXX_STANDARD 20) find_package(Threads) link_libraries(Threads::Threads) add_executable(example1 example1.cpp) add_executable(example2 example2.cpp) # target_link_libraries(example2 Threads::Threads)
Interface obsługi gniazd BSD jest bardzo zbliżony do API specyfikowanego przez POSIX do obsługi plików (który znacie z PSiW).
BSD socket API został przyjęty jako standard przez większość systemów operacyjnych, m. inn. został bez większych zmian wpisany w standard POSIX.
Zadanie 1 Napisz program który otworzy plik o nazwie 'date' i wypisze jego zawartość. Poniżej "pusty" plik źródłowy. Pomiń obsługę błędów. (Jeśli nie wiesz skąd wziąć plik date, wpisz w konsolę: date>date .)
Do obsługi pliku i standardowego wejścia / wyjścia użyj funkcji open, read, write i close.
Opis funkcji znajdziesz na stronach podręcznika systemowego (np. man open).
#include <fcntl.h> #include <unistd.h> using namespace std; int main(int argc, char **argv) { return 0; }
Zadanie 2 Co trzeba dodać do obsłużenia błędów?
Zadanie 3 Jakie adresy (numery) są potrzebne by móc przesłać wiadomość do konkretnego programu na wybranym komputerze?
Przypomnienie programów netcat / socat oraz programów netstat / ss (link do materiałów z SK1)
Zadanie 4 Prześlij między sobą dowolny tekst używając programu netcat lub socat
Zadanie 5 Nawiąż połączenie na port 13 (daytime) twojego komputera.
Jeżeli używasz własnego komputera i port 13 nie odpowiada, uruchom i pozostaw działającą w osobnym terminalu komendę
socat tcp-listen:1313,fork,reuseaddr exec:date i do tego oraz dalszych ćwiczeń używaj port 1313.
Więcej informacji o "useful debugging and measurement tools": echo, discard, chargen, qotd, daytime
Gniazdo (socket) – interfejs między systemem operacyjnym a programem użytkownika używany do dwukierunkowej komunikacji poza program.
Porównaj z: FIFO (named pipe, nazwany potok; było na SOP i PSiW)
API stworzone dla systemu BSD zostało przyjęte przez praktycznie wszystkie systemy operacyjne (POSIX socket API, WinSock).
Po więcej informacji o BSD Socket API zajrzyj na https://en.wikipedia.org/wiki/Berkeley_sockets.
man 7 socket tcp udp
Ekstra: man 7 pipe – przypomnienie potoków; man 7 unix – unix sockets – wykorzystanie gniazd do komunikacji międzyprocesowej
W jakiej kolejności są wysyłane bajty danych i dlaczego to jest ważne.
Przykład wysłania liczby 0x010F, czyli 271, czyli 0b0000000100001111
| byte order\bit order | MSB first | LSB first | ||
| little endian | 0000111100000001 | 1111000010000000 | x86/x86_64 | ARM, IA-64 (Itanium), SPARC≥v9, RISC-V |
| big endian | 0000000100001111 | 1000000011110000 | Motorola 68k, SPARC<v9, z/Arch | |
| I²C, SDH | Ethernet, RS232, USB |
MSB/LSB first – to głównie problem producentów sprzętu.
Little/big endian - to problem programistów.
https://en.wikipedia.org/wiki/Bit_numbering#Most-_vs_least-significant_bit_first
https://pl.wikipedia.org/wiki/Kolejność_bajtów
Sieciowa kolejność bajtów (networking byte order) to BE – odwrotnie niż na x86 https://en.wikipedia.org/wiki/Endianness#Networking
uint16_t hostEndianessPort = 13; uint16_t networkEndianessPort = htons(hostEndianessPort);
Opis funkcji pomocniczych – patrz man byteorder
Protokół trzeba podać na etapie tworzenia gniazda. Adres IP oraz numer portu (lub ścieżkę dla gniazd unix) trzeba podać w odpowiedniej strukturze.
Do przekazywania adresu gniazda funkcje wymagają wskaźnika na strukturę sockaddr (w C – struct sockaddr).
Ta struktura nie pozwala na bezpośrednie wykorzystanie. Zamiast tego dla IPv4 należy używać sockaddr_in (w C – struct sockaddr_in)
Opis struktury – patrz man sockaddr, man 7 ip i man netinet_in.h
C nie pozwala na dziedziczenie, więc zamiast tego (https://en.wikipedia.org/wiki/Type_punning#Sockets_example) struktura sockaddr ma kilka "specjalizacji" dla konkretnej rodziny adresów:
sockaddr_in (INET, czyli IPv4)sockaddr_in6 (INET6, czyli IPv6)sockaddr_un (dla unix socket).
Zadanie 6 Zmień program do odczytu pliku tak, by zamiast funkcji open(…):
socket(…) gniazdo:PF_INET (zamiennie AF_INET) SOCK_STREAM (czyli TCP) IPPROTO_TCP (można też podać 0, oznaczające domyślny protokół)sockaddr_in (zdefiniowaną w pliku nagłówkowym netinet/in.h):AF_INEThtons)inet_addr lub inet_aton); htonl(INADDR_LOOPBACK). .sin_addr to struktura typu in_addr z jedną składową .s_addr typu uint32_t inet_aton przyjmuje wskaźnik na strukturę, natomiast inet_addr zwraca liczbę którą trzeba przypisać składowej .s_addr: sockaddr_in nazwa_zmiennej; wersja 1: nazwa_zmiennej.sin_addr.s_addr = inet_addr("8.8.8.8"); wersja 2: inet_aton("8.8.8.8", &nazwa_zmiennej.sin_addr);
connect(…) connect oczekuje sockaddr* zamiast sockaddr_in* – wymagane rzutownaiesizeof() zmiennej lub typuclose() zamykał połączenie funkcją shutdown(); shutdown() można osobno zakończyć nadawanie i odbiór, drugi argument określa co zakończyć – SHUT_RD / SHUT_WR / SHUT_RDWR
Opis funkcji znajdziesz w podręczniku systemowym (man 3 … / man 3p …)
Potrzebne pliki nagłówkowe to:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
Zadanie 7 Przekształć poprzedni program tak, by czytał z adresu IP i portu podanego w argumentach programu.
Zadanie 8 Dodaj do programu obsługę błędów zwracanych przez funkcje connect i read.
Zadanie 9 Zmień IP na losowe (tak, by nie odpowiadało na próbę połączenia). Programem netstat -tnp / ss -tnp wyświetl utworzone połączenie.
Poza funkcją read(…) do odbierania danych można używać funkcji recv,
recvfrom i recvmsg, przy czym read(fd, buf, len) jest
równoważne recv(fd, buf, len, 0) i recvfrom(sockfd, buf, len, 0, NULL, NULL).
Podobnie poza funkcją write do wysyłania można używać też funkcji send,
sendto i sendmsg, analogicznych do powyższych.
Dodatkowy argument recv/send (pole flag) pozwala na zmianę zachowania
tych funkcji i będzie omawiany później.
recvfrom i sendto mają dodatkowe pole na adres nadawcy/odbiorcy i są
przeznaczone dla protokołów warstwy transportowej pozwalających na komunikację
po jednym gnieździe z wieloma partnerami. Będą omawiane przy obsłudze UDP.
Zadanie 10 Zmień program tak, by zamiast read(…) używał funkcji recv(…)
Zadanie 11 Przed wywołaniem connect ustal lokalny adres funkcją bind. Powtórz zadanie 8 i 9.