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;
}
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).
2 Eternal vigilance
3 Obsługa wielu klientów
Dla poszerzenia wiedzy