Programowanie połączeń sieciowych
DO PRZYGOTOWANIA
Proszę przypomnieć sobie użycie
fork()
-
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
Obsługa adresów IPv4 oraz IPv6
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:
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
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)
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
- 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
gethostbyname
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;
}
Sprawdzić działanie programu dla www.yahoo.com
oraz innych wybranych adresów symbolicznych.
Dopisać instrukcje, które szczegółowo sprawdzają typ błędu funkcji gethostbyname
i w zależności od tego wyświetlają odpowiedni komunikat.
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).
25.2 Eternal vigilance
25.3 Obsługa wielu klientów
Dla poszerzenia wiedzy