Programowanie połączeń sieciowych

DO PRZYGOTOWANIA

  • Proszę przypomnieć sobie użycie fork()
  • Proszę poczytać opis podstaw gniazd sieciowych (socketów) z The Linux Programmer's Guide. Konkretnie sekcję Gniazda sieciowe - podstawy oraz jej podsekcje: „Podstawowe funkcje”, „TCP: SOCK_STREAM”, „SOCK_DGRAM (UDP)”, „PF_UNIX”, „SOCK_RAW i PF_PACKET”

WPROWADZENIE

Network Byte Order

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

getaddrinfo

  • Przeczytać manual do funkcji getaddrinfo
    • Jakie parametry przyjmuje funkcja i jakie wartości zwraca.
    • Który parametr funkcji może jako wartość przyjąć http lub ftp lub telnet lub smtp?
  • Zwrócić uwagę na opis struktury: struct addrinfo
    • Co oznaczają poszczególne pola struktury i jakie wartości mogą przyjmować.
    • Jaki jest sens wprowadzenia pola ai_next w strukturze?

Obsługa adresów IPv4 oraz IPv6

  • Zapoznać się z następującymi strukturami (man socket, /usr/include):
    • struct sockaddr
    • struct sockaddr_in
    • struct in_addr
    • struct sockaddr_in6
    • struct in6_addr
    • Które z nich dotyczą protokołu IPv4 a które IPv6?
    • Jakie są zależności pomiędzy tymi strukturami?
  • Funkcje konwersji:
    • inet_pton - konwertuje zapis „192.168.1.1” na odpowiednią strukturę - czyli inaczej konwertuje string do reprezentacji binarnej.
    • inet_ntop - konwertuje strukturę (reprezentację binarną) na string.

Gniazda

  • Gniazdo z ang. socket.
  • Są używane w czwartej warstwie sieciowego modelu OSI/ISO.
  • Otwieranie gniazd (i uzyskanie deskryptora do komunikacji sieciowej) dokonuje się za pomocą funkcji
    socket(int domain, int type, int protocol)
    • Adres IP identyfikuje hosta w danej sieci (podsieci), co identyfikuje numer portu?
    • Czym różni się deskryptor gniazda od deskryptora pliku?
  • 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)
  • Powiązanie numeru portu z deskryptorem gniazda dokonuje się za pomocą funkcji
    bind(int sockfd, struct sockaddr *my_addr, int addrlen)
  • Proszę przeczytać manuale dla funkcji socket(2) i bind(2), zwrócić uwagę na parametry jakie przyjmują i wartości jakie zwracają.

Podstawowe funkcje systemowe:

  • socket(2)
  • bind(2)
  • listen(2)
  • accept(2)
  • connect(2)

Połączenie

  • Do nawiązywania połączeń wykorzystujemy funkcję
    connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
    • 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
  • Dopiero po pomyślnym nawiązaniu połączenia możemy używać sockfd do komunikowania się z serwerem.

Nasłuchiwanie

  • Rozpoczęcie nasłuchiwania nie wymaga użycia funkcji connect ponieważ to zdalny klient będzie jej używał do połączenia się z naszym serwerem.
  • Nasłuchiwanie można rozpocząć przy pomocy funkcji
    listen(int sockfd, int backlog)
    • sockfd - deskryptor gniazda. Nasłuchiwanie będzie się odbywać zgodnie z parametrami opisywanymi przez deskryptor.
    • backlog - maksymalna liczba połączeń oczekujących na akceptaję.
  • Ostatnim krokiem rozpoczęcia komunikacji z klientem jest akceptacja jego próby połączenia. Dokonuje się tego za pomocą funkcji:
    accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
  • Funkcja accept(2) jako wartość zwraca nowy deskryptor gniazda który służy do komunikacji z akceptowanym połączeniem.

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) które oferują dodatkową konfigurację.
  • Proszę przeczytać manual dla powyższych funkcji zwracając uwagę na:
    • przyjmowane parametry
    • zwracane wartości

Zamknięcie połączenia

  • Po zakończeniu wysyłania/odbierania danych należy zamknąć połączenie.
  • Zamknięcie połączenia reprezentowanego przez dany deskryptor można dokonać przy pomocy funkcji close(2)
  • Dla zainteresowanych: porównać funkcję close(3) z funkcją shutdown(3).

ĆWICZENIA

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.pl/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.pl/80
echo -e "GET / HTTP/1.1\nHost: www.google.pl\nConnection: close\n\n" >&3
cat <&3
timeserver.sh
#!/bin/bash
 
###
# Pobierz aktualny czas z serwera NTP
### 
 
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

Programowanie gniazd

Przeglądnąć artykuł: Beej's Guide to Network Programming

Skompilować i przetestować omówione w nim programy, w tym:

client.c + server.c (uwaga na nr portu!)

listener.c + talker.c (uwaga na nr portu!)

Kompilacja:

gcc -Wall -o server server.c

gethostbyname

  1. Proszę przeanalizować, skompilować i uruchomić program:
    gethostbyname-demo.c
    #include <stdio.h>
    #include <errno.h>
    #include <netdb.h>      /* 4 gethostbyname, hostent structure */
    #include <unistd.h>     /* 4 exit */
    #include <netinet/in.h> /* 4 ntohn */
     
    int main(int argc, char *argv[])
    {
         int i,j;
         struct hostent *he;
     
         if (argc != 2) 
         {
              fprintf(stderr,"usage: %s hostname\n", argv[0]);
              return 1;
         }
     
         if ((he = gethostbyname(argv[1])) == NULL) 
         {  
              fprintf(stderr, "gethostbyname error\n");
              return 1;
         }
     
         /* host info: */
         printf("\nHost name: %s", he->h_name);
         printf("\nAliases:");
         for(i=0;he->h_aliases[i] != NULL;++i)
              printf("\n%d. %s", i+1, he->h_aliases[i]);
         if(he->h_addrtype == AF_INET)
              printf("\nAddres type: IPv4");
         if(he->h_addrtype == AF_INET6)
              printf("\nAddres type: IPv6");
         printf("\nAddress length: %d bytes", he->h_length);
         printf("\nAddresses:");
         for(j=0;j<he->h_length;++j)
         {
              printf("%d", (uint8_t)he->h_addr[j]);
              if(j < (he->h_length-1))
                   printf(".");
         }
         printf("\n");
     
         return 0;
    }
  2. Sprawdzić działanie programu dla www.yahoo.com oraz innych wybranych adresów symbolicznych.
  3. Dopisać instrukcje, które szczegółowo sprawdzają typ błędu funkcji gethostbyname i w zależności od tego wyświetlają odpowiedni komunikat.
  4. Zmodyfikować tak program aby wyświetlał wszystkie adresy IP odnoszące się do podanego adresu.

Serwer

Poniżej przedstawiony jest kod programu server.c

server.c
/*
** server.c -- a stream socket server demo
*/
 
#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 <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
 
#define MYPORT 3490	// the port users will be connecting to
 
#define BACKLOG 10	 // how many pending connections queue will hold
 
void sigchld_handler(int s)
{
	while(waitpid(-1, NULL, WNOHANG) > 0);
}
 
int main(void)
{
	int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
	struct sockaddr_in my_addr;	// my address information
	struct sockaddr_in their_addr; // connector's address information
	socklen_t sin_size;
	struct sigaction sa;
	int yes=1;
 
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		perror("socket");
		exit(1);
	}
 
	if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
		perror("setsockopt");
		exit(1);
	}
 
	my_addr.sin_family = AF_INET;		 // host byte order
	my_addr.sin_port = htons(MYPORT);	 // short, network byte order
	my_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP
	memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
 
	if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr) == -1) {
		perror("bind");
		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);
	}
 
	sin_size = sizeof their_addr;
	if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
		perror("accept");
	}
	printf("server: got connection from %s\n",inet_ntoa(their_addr.sin_addr));
	if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
		perror("send");
	sleep(5); // just for observing easily that the server cannot serve a few clients concurrently
	close(new_fd);
 
	return 0;
}

25.1 Komunikator

  • Należy przerobić powyższy program tak aby działał jako server. Łącząc się za pomocą np. programu telnet program powinien umożliwiać prowadzenie dialogu jak popularne komunikatory internetowe (np. gg, tlen, itp).
    • Korzystając z programu telnet można połączyć się z serwerem wpisując:
      $ telnet remotehostname XXX

      gdzie remotehostname jest nazwą komputera, na którym uruchomiono serwer (localhost, jeżeli to ta sama maszyna), a XXX numerem przypisanego do niego portu.

    • Podpowiedź: po akceptacji połączenia program powinien utworzyć dwa procesy potomne, jeden do czytania portu, drugi do pisania.

25.2 Eternal vigilance

  • Proszę zmodyfikować serwer tak, aby po obsłużeniu klienta nie kończył działania, ale powracał do oczekiwania na kolejne połączenie.

25.3 Obsługa wielu klientów

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

Dla poszerzenia wiedzy

pl/dydaktyka/unix/lab_prog_siec.txt · ostatnio zmienione: 2019/06/27 15:50 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0