Spis treści

Zmiana adresu domenowego na IP

Tradycyjna funkcja gethostbyname

Najstarsze funkcje do zmiany to gethostbyname i gethostbyaddr. Funkcje są przestarzałe i powszechnie używane. Problemy z ich działaniem obrazuje poniższy kod:

ghbn.cpp
#include <unistd.h>
#include <thread>
#include <arpa/inet.h>
#include <netdb.h>
#include <cstdio>
 
int main() {
 
    std::thread t1([]{
        sleep(1);
        gethostbyname("spam.org");
    });
 
    std::thread t2([]{
        hostent* ret = gethostbyname("cat.put.poznan.pl");
        sleep(2);
        printf("%s: %s\n", ret->h_name, inet_ntoa(**(in_addr**)ret->h_addr_list));
    });
 
    t1.join();
    t2.join();    
    return 0;
}

Zadanie 1 Przetestuj powyższy kod. Zastanów się jak uniknąć problemu, który pokazuje powyższy kod.

Rozszerzenia GNU

Kompilator GCC udostępnia wiele rozszerzeń "łatających" braki standardów https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html.
Towarzysząca mu biblioteka glibc wprowadza między innymi funkcję gethostbyname_r (funkcje *_r są reentrant, tj. można je łatwiej stosować w aplikacjach wielowątkowych https://en.wikipedia.org/wiki/Reentrancy_%28computing%29). gethostbyname_r może być używana współbieżnie.

hostent he, *resptr;
int status, retval;
char buffer[4096];
retval = gethostbyname_r("cat.put.poznan.pl", &he, buffer, sizeof(buffer), &resptr, &status);
if(retval)
    error(1,0,"gethostbyname_r error: %s", hstrerror(status));
if(!resptr)
    error(1,0,"empty result");
print(resptr);

getaddrinfo

Standard POSIX wprowadza funkcję getaddrinfo, która pozwala na bezpieczne tłumaczenie nazwy domenowej (lub adresu IP) z tekstu na odpowiedni format dla funkcji sieciowych (wskaźnik na strukturę sockaddr). Funkcja działa zarówno IPv4 i IPv6.
getaddrinfo przygotowuje wszystkie argumenty potrzebne funkcjom takim jak socket, connect czy bind.

Funkcja getaddrinfo sama alokuje pamięć dla wyników, stąd wyniki muszą być zwolnione ręcznie funkcją freeaddrinfo.

Standard POSIX wprowadza też m. inn. wygodną funkcję do tłumaczenia sockaddr* na tekst – funkcję getnameinfo.

Składnia getaddrinfo:

int getaddrinfo(                  // zwraca 0 (ok) lub niezerowy kod błędu (można go przerobić na tekst używając gai_strerror)
    const char * ip_or_hostname,  // nazwa domenowa lub adres IP 
    const char * port_or_service, // numer portu (np. "80") lub nazwa usługi (np. "http", ustawi port na 80)
    const struct addrinfo *hints, // pozwala wybrać typ adresu, wyłączyć używanie DNS, etc.
    struct addrinfo **res);       // pole na wynik (jeśli puste - nic nie znaleziono)

Definicja struktury addrinfo:

struct addrinfo {
    int ai_flags;  // pole flag, używane jeśli struktura ma być podpowiedziami dla getaddrinfo – patrz manual
 
    int ai_family;    // ai_family, ai_socktype i ai_protocol mają znaczenie identyczne jak
    int ai_socktype;  // w funkcji socket(). Mogą być ustawione w podpowiedziach na wybraną
    int ai_protocol;  // wartość lub na 0 (wtedy oznaczają wszystkie pasujące wartości)
 
    socklen_t ai_addrlen;      //  ai_addr to adres o długości ai_addrlen który może być
    struct sockaddr *ai_addr;  //          użyty w funkcji connect lub bind
 
    char *ai_canonname;  // wypełniane (tylko jeśli podano odpowiednią flagę) nazwą o którą zapytano 
 
    struct addrinfo *ai_next;  // struktura addrinfo tworzy jednokierunkową linked listę, ai_next wskazuje na jej następny element 
};

Składnia getnameinfo:

int getnameinfo(                 // zwraca 0 (ok) lub niezerowy kod błędu (można go przerobić na tekst używając gai_strerror)
    const struct sockaddr *addr, // adres (IPv4 lub IPv6) do przerobienia na tekst
    socklen_t addrlen,           // ilość bajtów  powyższego adresu 
                                 //
    char * host,                 // wskaźnik na bufor do którego zostanie wpisany adres IP (lub nazwa domenowa) jako tekst
    size_t hostLen,              // ilość miejsca w powyższym buforze
                                 //   (dla zmiany IP na tekst powinien wystarczyć bufor o długości NI_MAXHOST)
    char * port,                 // wskaźnik na bufor do którego zostanie wpisany numer (lub nazwa) portu jako tekst
    size_t portLen,              // ilość miejsca w powyższym buforze
                                 //   (dla zmiany portu na tekst powinien wystarczyć bufor o długości NI_MAXSERV)
    int flags);                  // pole flag, pozwalające wybrać m. inn. czy IP i port mają być tłumaczone na numery czy nazwy

Pełen opis funkcji i struktur znajdziesz na stronie manuala do getaddrinfo (man getaddrinfo) lub w standardzie POSIX ([1], [2], [3]).

Uwaga: getaddrinfo i getnameinfo w razie sukcesu zwraca 0, a w przypadku błędu zwraca jego kod. Czytelny dla człowieka komunikat powiązany z kodem błędu można uzyskać funckją gai_strerror. Zwróć uwagę, że to odbiega od typowej konwencji POSIX.

Przykłady użycia:

gai1.cpp
#include <netdb.h>
#include <cstdio>
#include <arpa/inet.h>
 
int main(){
    // Ustawienie "podpowiedzi" - sterowanie jakie wyniki chcemy otrzymać:
    addrinfo hints {};    // uwaga: puste 'list initialization' zeruje całą strukturę
    hints.ai_family   = AF_INET;         // tylko IPv4 (AF_INET)
    hints.ai_protocol = IPPROTO_UDP;     // protokół UDP
 
    // Zmienna w której będzie umieszczona lokalizacja wyniku w pamięci
    addrinfo * resolved;
 
    // Z sieci bezprzewodowej PP (lub innych używających serwerów nazw z podsieci 150.254.5.0/24)
    // proszę zmienić "pool.ntp.org" na "onet.pl" - Dział Obsługi i Eksploatacji z niezrozumiałych
    // przyczyn cenzuruje odpowiedzi z DNS 
    int res = getaddrinfo("pool.ntp.org", "ntp", &hints, &resolved);
 
    if(res) {fprintf(stderr, "Getaddrinfo failed: %s\n", gai_strerror(res)); return 1;}
 
    for(addrinfo * it = resolved; it; it=it->ai_next){
        sockaddr_in* addr = (sockaddr_in*) it->ai_addr; // <- rzutowanie bezpieczne,
        printf(" %s\n",inet_ntoa(addr->sin_addr));      //    bo w podpowiedziach 
    }                                                   //    zażądaliśmy adresów IPv4
 
    freeaddrinfo(resolved);
 
    return 0;
}
gai2.cpp
#include <netdb.h>
#include <cstdio>
 
int main(){
    addrinfo * resolved;
    // Podpowiedzi, port i nazwa hosta są opcjonalne.
    int res = getaddrinfo("ietf.org", nullptr, nullptr, &resolved);
 
    if(res) {fprintf(stderr, "Getaddrinfo failed: %s\n", gai_strerror(res)); return 1;}
 
    char ip[40]; // maks. długość IP(v6) jako tekst: 8 bloków po 4 znaki oddzielone ':'
                 // można użyć stałą NI_MAXHOST określającą maksymalną długość nazwy domenowej 
    for(addrinfo * it = resolved; it; it=it->ai_next){
        // funkcja getnameinfo tłumaczy dowolny sockaddr na tekst
        res = getnameinfo(it->ai_addr, it->ai_addrlen, ip, 40, nullptr, 0, NI_NUMERICHOST);
        if(res) {fprintf(stderr, "Getnameinfo failed: %s\n", gai_strerror(res)); return 1;}
        else printf("%40s (socktype: %d, proto: %d)\n", ip, it->ai_socktype, it->ai_protocol);
    }
 
    freeaddrinfo(resolved);
 
    return 0;
}

Zadanie 2 Stwórz klienta TCP, który:

W tym zadaniu wszystkie argumenty funkcji socket i wszystkie poza deskryptorem gniazda argumenty funkcji connect mają być brane z wyników funkcji getaddrinfo

Zadanie 3 Zmień kod ostatniego zadania z poprzedniego tematu (serwer UDP wysyłający każdą wiadomość do wszystkich od których wcześniej coś dostał) tak, by serwer w treść każdej wiadomości wstawiał informację skąd przyszła. Tłumacz adres IP na nazwę domenową, ale numer portu pozostaw jako liczbę.