courses:unix:lab_netprog

1. Network Byte Order

  • Zapoznać się z następującymi pojęciami:
    • Network Byte Order
    • Big-Endian
    • Little-Endian
  • Zapoznać się z funkcjami: htonl(3), htons(3), ntohl(3), ntohs(3)

2. getaddrinfo(3)

  • Funkcja używana do pobrania adresu IP dla zadanej nazwy symbolicznej (np. uj.edu.pl) i rodzaju usługi (ftp, http)
  • Jest ulepszoną wersją funkcji gethostbyname(3), działa zarówno z IPv4 jak i IPv6 i ogólnie jest bardziej nowoczesna niż poprzedniczka ;-)
  • Zwraca strukturę addrinfo, która zawiera szczegółowe informacje o adresie / adresach IP (jeżeli jest ich więcej niż jeden to pole ai_next zawiera wskaźnik na strukturę opisującą kolejny adres)
  • Funkcja zwraca 0 w przypadku powodzenia, albo kod błędu w przeciwnym wypadku - kod ten można przetłumaczyć na odpowiedni komunikat za pomocą funkcji gai_strerror(3) (działa analogicznie jak strerror, tylko tutaj dla kodów błędów funkcji getaddrinfo)

3. Obsługa adresów IPv4 oraz IPv6

  • Adresy IP są przechowywane w odpowiednich strukturach (wskazywanych m.in. przez funkcję getaddrinfo(3)):
    • IPv4: struct sockaddr_in, struct in_addr (zob. manual do ip(7))
    • IPv6: struct sockaddr_in6, struct in6_addr (zob. manual do ipv6(7))
    • struct sockaddr - struktura na którą często rzutowane są wskaźniki na powyższe struktury, do zapewnienia jednolitego interfejsu (zob. np. manual do bind(2))
  • Funkcje do konwersji:
    • inet_pton(3) – konwertuje zapis “192.168.1.1” na odpowiednią strukturę - czyli inaczej konwertuje string do reprezentacji binarnej.
    • inet_ntop(3) – konwertuje strukturę (reprezentację binarną) na string.

4. Gniazda

  • Gniazda (ang. sockets) są używane w czwartej warstwie sieciowego modelu OSI/ISO
  • Istnieje kilka rodzajów socketów, w tym:
    • Stream Socket – służą do komunikacji połączeniowej (użycie TCP)
    • Datagram Socket – służą do komunikacji bezpołączeniowej (użycie UDP)
  • Podstawowe funkcje systemowe służące do obsługi socketów:
    • socket(2) – do otwierania gniazd i uzyskania deskryptora do komunikacji sieciowej
    • bind(2) – do powiązania numeru portu z deskryptorem gniazda
    • listen(2) – rozpoczęcie nasłuchiwania po stronie serwera (nie jest wymagane użycie connect, ponieważ to klient będzie używał tej funkcji do podłączenia do serwera)
      • argumenty:
        • sockfd - deskryptor gniazda. Nasłuchiwanie będzie się odbywać zgodnie z parametrami opisywanymi przez deskryptor
        • backlog - maksymalna liczba połączeń oczekujących na akceptację
      • po wywołaniu tej funkcji serwer już przyjmuje połączenia (nie pojawia się błąd connection refused), ale klienci jeszcze nie są obsługiwani (lądują w kolejce)
    • accept(2) – akceptacja połączenia z pierwszym klientem z kolejki (ustawionej przez listen(2))
      • funkcja zwraca nowy deskryptor gniazda, który służy do komunikacji z zaakceptowanym połączeniem
    • connect(2) – do nawiązania połączenia z serwerem
      • argumenty:
        • sockfd - deskryptor gniazda
        • serv_addr - adres hosta docelowego, który możemy otrzymać przy pomocy funckji getaddrinfo
        • addrlen - długość adresu, najczęściej podaje się wartość addrinfo::ai_addrlen
      • uwaga: dopiero po pomyślnym nawiązaniu połączenia (za pomocą connect()) możemy używać sockfd do komunikowania się z serwerem!
    • close(2) – zamknięcie połączenia
      • dla zainteresowanych: porównać funkcję close(2) z funkcją shutdown(2)
  • Wysyłanie/odbieranie danych:
    • Wszystko w systemach GNU/Linux/Unix jest reprezentowane za pomocą plików - tak więc gniazda również
    • Wysyłanie/odbieranie danych przez/z gniazd jest bardzo podobne do zapisu/odczytu danych do/z pliku
    • Jest tak podobne, że do tego celu można użyć funkcji write(2)/read(2) :!:
    • Jednak system oferuje funkcje specjalizowane send(2)/recv(2) (oraz sendto(2)/recvfrom(2) dla komunikacji bezpołączeniowej) które oferują dodatkową konfigurację

I. Sockety w Bashu

  • Jest możliwe otworzenie Socketa w Bashu za pomocą następującej składni:
    exec {deskryptor-pliku}<>/dev/tcp/{host}/{port}
  • Np. aby otworzyć dwukierunkowego socketa dla strony Google z portem HTTP i deskryptorem nr 3 (dlaczego akurat taki?) należy napisać:
    exec 3<>/dev/tcp/www.google.com/80
  • Uruchom i przeanalizuj poniższe przykłady:
    • webpage.sh
      #!/bin/bash
       
      ###
      # Połącz się ze stroną internetową i pobierz zawartość strony głównej
      ### 
       
      exec 3<>/dev/tcp/www.google.com/80
      echo -e "GET / HTTP/1.1\nHost: www.google.com\nConnection: close\n\n" >&3
      cat <&3
    • timeserver.sh
      #!/bin/bash
       
      ###
      # Pobierz aktualny czas z serwera NTP
      # Źródło: https://tldp.org/LDP/abs/html/devref1.html
      # UWAGA: może NIE działać na serwerze SPK (ze względu na zablokowane porty)
      ### 
       
      cat </dev/tcp/time.nist.gov/13
    • port-scanner.sh
      #!/bin/bash
       
      ###
      # Skaner portów (sprawdza które porty są otwarte).
      # Jako argument wywołania podaj adres serwera, który chcesz przeskanować,
      # np. ./port-scanner.sh localhost
      ### 
       
      host=$1
      port_first=1
      port_last=65535
      for ((port=$port_first; port<=$port_last; port++))
      do
        # echo "Skanowanie portu $port..."
        timeout 1 bash -c "(echo >/dev/tcp/$host/$port) >/dev/null 2>&1" && echo "$port otwarty!"
      done

II. getaddrinfo

  1. Proszę przeanalizować, skompilować i uruchomić program (źródło):
    showip.c
    /*
    ** showip.c -- show IP addresses for a host given on the command line
    */
     
    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
     
    int main(int argc, char *argv[])
    {
    	struct addrinfo hints, *res, *p;
    	int status;
    	char ipstr[INET6_ADDRSTRLEN];
     
    	if (argc != 2) {
    	    fprintf(stderr,"usage: showip hostname\n");
    	    return 1;
    	}
     
    	memset(&hints, 0, sizeof hints);
    	hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
    	hints.ai_socktype = SOCK_STREAM;
     
    	if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
    		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
    		return 2;
    	}
     
    	printf("IP addresses for %s:\n\n", argv[1]);
     
    	for(p = res;p != NULL; p = p->ai_next) {
    		void *addr;
    		char *ipver;
     
    		// get the pointer to the address itself,
    		// different fields in IPv4 and IPv6:
    		if (p->ai_family == AF_INET) { // IPv4
    			struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
    			addr = &(ipv4->sin_addr);
    			ipver = "IPv4";
    		} else { // IPv6
    			struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
    			addr = &(ipv6->sin6_addr);
    			ipver = "IPv6";
    		}
     
    		// convert the IP to a string and print it:
    		inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
    		printf("  %s: %s\n", ipver, ipstr);
    	}
     
    	freeaddrinfo(res); // free the linked list
     
    	return 0;
    }
  2. Sprawdzić działanie programu dla www.yahoo.com, uj.edu.pl oraz innych wybranych adresów symbolicznych.
  3. Jakie parametry przyjmuje funkcja getaddrinfo(3) i jakie zwraca?
  4. W jaki sposób można wyświetlić komunikaty o błędach funkcji getaddrinfo(3)?
  5. Zmodyfikuj program w taki sposób, aby przyjmował drugi argument oznaczający usługę (np. http, ftp, telnet, smtp). Przekaż ten parametr w odpowiedni sposób do funkcji getaddrinfo(3)

III. Programowanie gniazd - protokół TCP

  1. Proszę przeanalizować, skompilować i uruchomić program:
    simple-server.c
    /*
    ** simple-server.c -- a stream socket server demo
    ** Modified version of server.c from https://beej.us/guide/bgnet/html/
    */
     
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    #include <sys/wait.h>
    #include <signal.h>
     
    #define PORT "3490"  // the port users will be connecting to
     
    #define BACKLOG 10	 // how many pending connections queue will hold
     
    void sigchld_handler(int s)
    {
    	(void)s; // quiet unused variable warning
     
    	// waitpid() might overwrite errno, so we save and restore it:
    	int saved_errno = errno;
     
    	while(waitpid(-1, NULL, WNOHANG) > 0);
     
    	errno = saved_errno;
    }
     
     
    // get sockaddr, IPv4 or IPv6:
    void *get_in_addr(struct sockaddr *sa)
    {
    	if (sa->sa_family == AF_INET) {
    		return &(((struct sockaddr_in*)sa)->sin_addr);
    	}
     
    	return &(((struct sockaddr_in6*)sa)->sin6_addr);
    }
     
    int main(void)
    {
    	int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
    	struct addrinfo hints, *servinfo, *p;
    	struct sockaddr_storage their_addr; // connector's address information
    	socklen_t sin_size;
    	struct sigaction sa;
    	int yes=1;
    	char s[INET6_ADDRSTRLEN];
    	int rv;
     
    	memset(&hints, 0, sizeof hints);
    	hints.ai_family = AF_UNSPEC;
    	hints.ai_socktype = SOCK_STREAM;
    	hints.ai_flags = AI_PASSIVE; // use my IP
     
    	if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
    		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
    		return 1;
    	}
     
    	// loop through all the results and bind to the first we can
    	for(p = servinfo; p != NULL; p = p->ai_next) {
    		if ((sockfd = socket(p->ai_family, p->ai_socktype,
    				p->ai_protocol)) == -1) {
    			perror("server: socket");
    			continue;
    		}
     
    		if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
    				sizeof(int)) == -1) {
    			perror("setsockopt");
    			exit(1);
    		}
     
    		if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
    			close(sockfd);
    			perror("server: bind");
    			continue;
    		}
     
    		break;
    	}
     
    	freeaddrinfo(servinfo); // all done with this structure
     
    	if (p == NULL)  {
    		fprintf(stderr, "server: failed to bind\n");
    		exit(1);
    	}
     
    	if (listen(sockfd, BACKLOG) == -1) {
    		perror("listen");
    		exit(1);
    	}
     
    	sa.sa_handler = sigchld_handler; // reap all dead processes
    	sigemptyset(&sa.sa_mask);
    	sa.sa_flags = SA_RESTART;
    	if (sigaction(SIGCHLD, &sa, NULL) == -1) {
    		perror("sigaction");
    		exit(1);
    	}
     
    	printf("server: waiting for connections...\n");
     
    	sin_size = sizeof their_addr;
    	new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
    	if (new_fd == -1) {
    		perror("accept");
    	}
     
    	inet_ntop(their_addr.ss_family,
    		get_in_addr((struct sockaddr *)&their_addr),
    		s, sizeof s);
    	printf("server: got connection from %s\n", s);
     
    	if (send(new_fd, "Hello, world!", 13, 0) == -1)
    		perror("send");
    	sleep(5);  // just for observing easily that the server cannot serve a few clients concurrently
    	close(new_fd);
     
    	return 0;
    }
    • Uwaga: do danego portu może być przypisany tylko jeden program (za pomocą bind) – dlatego jeżeli pracujesz na współdzielonym serwerze (np. SPK), zmień wartość PORT na jakąś własną (w #define PORT “3490”)
    • Jest to prosty serwer, który czeka na połączenie od klienta, wysyła mu wiadomość Hello, world!, utrzymuje połączenie przez 5 sekund (sleep), a następnie zamyka połączenie i kończy swoje działanie
    • Podłączenie do serwera jako klient można zrealizować na dwa sposoby:
      • Korzystając z programu telnet wpisując:
        $ telnet host port

        podając odpowiednią nazwę hosta i port (np. telnet localhost 3490, jeżeli łączymy się z tej samej maszyny i korzystamy z domyślnego portu)

      • Korzystając z przykładowego programu-klienta: client.c (źródło) – uwaga: zmodyfikuj zmienną PORT, aby była zgodna z wartością w programie serwera!
  2. Kilka pytań na rozgrzewkę:
    • Adres IP identyfikuje hosta w danej sieci (podsieci), co identyfikuje numer portu?
    • Czym różni się deskryptor gniazda od deskryptora pliku?
  3. Proszę zmodyfikować serwer tak, aby po obsłużeniu klienta nie kończył działania, ale powracał do oczekiwania na kolejne połączenie
  4. Proszę zmodyfikować serwer tak, aby mógł obsługiwać jednocześnie więcej niż jednego klienta
    • Podpowiedź: Można użyć funkcji fork() do tworzenia procesów potomnych - każdy proces potomny będzie obsługiwał jednego klienta.
  5. Proszę zmodyfikować serwer tak, aby umożliwiał prowadzenie dialogu a'la komunikator internetowy:
    • Jedną stroną jest podłączony klient, a drugą stroną może być serwer (w wersji minimum), albo inny klient (wtedy serwer powinien parować klientów w odpowiedni sposób, np. pierwszy klient rozmawia z drugim, trzeci z czwartym, itd)
    • Powinna być umożliwiona komunikacja asynchroniczna (tzn. jedna strona może napisać kilka wiadomości pod rząd). Podpowiedź: po akceptacji połączenia program powinien utworzyć dwa procesy potomne, jeden do czytania portu, drugi do pisania

IV. Programowanie gniazd - protokół UDP

  1. Jak zmienia się komunikacja w protokole UDP? Proszę przeanalizować, skompilować i uruchomić programy (źródło):
    • listener.c - program oczekujący na przychodzącą wiadomość
    • talker.c - program umożliwiający wysyłanie wiadomości (wcześniej należy uruchomić program listener, aby wiadomość została odebrana)
  2. W jakich zastosowaniach przydaje się protokół UDP, a w jakich TCP? Jakie są ich zalety i wady?
  • courses/unix/lab_netprog.txt
  • Last modified: 4 years ago
  • by 127.0.0.1