To jest stara wersja strony!
Programowanie połączeń sieciowych
DO PRZYGOTOWANIA
Proszę przypomnieć sobie użycie
fork()
-
Instrukcja poniżej jest w trakcie restrukturyzacji. Natomiast materiały do przygotowania, zamieszczone powyżej, nie zmienią się. Można więc już powoli się przygotowywać
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
- netscanner.sh
#!/bin/bash
###
# Skaner portów (sprawdza które porty są otwarte). Jako argument wywołania podaj adres serwera, który chcesz przeskanować
###
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
Przeanalizować, skompilować i uruchomić program:
#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.google.pl
oraz innych wybranych adresów symbolicznych.
Do powyższego kodu 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.
Komunikator
Poniżej przedstawiony jest kod programu 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);
}
while(1) { // main accept() loop
sin_size = sizeof their_addr;
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) {
perror("accept");
continue;
}
printf("server: got connection from %s\n",inet_ntoa(their_addr.sin_addr));
if (!fork()) { // this is the child process
close(sockfd); // child doesn't need the listener
if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); // parent doesn't need this
}
return 0;
}
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).
Podpowiedź po akceptacji połączenia program powinien utworzyć dwa procesy potomne, jeden do czytania portu, drugi do pisania.
Transfer plików
Powyższy komunikator może być z łatwością zmodyfikowany aby zamiast tekstu wysyłał plik.
Zmodyfikować komunikator tak aby po nawiązaniu połączenia przez klienta rozpoczął wysyłanie pliku.
Zmodyfikować program client.c tak aby był zdolny odebrać i zapisać przesyłany plik.
Użycie Select
Dla poszerzenia wiedzy