-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHTTP-0.9.c
149 lines (116 loc) · 11.7 KB
/
HTTP-0.9.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <stdio.h>
#include <sys/socket.h> // socket
#include <errno.h> // errno
#include <arpa/inet.h> // htons
#include <unistd.h> // write
#include <string.h> // strlen
int main(){
// definizione di variabili locali
struct sockaddr_in server_addr; // struct per definire l'indrizzo del server
int s; // socket
int t; // variabile temporanea
unsigned char * p; // puntatore per indirizzo IP
int i;
/*
Creiamo un socket:
→ AF_INET : servizio della famiglia IP4
→ SOCK_STREAM : rappresenta lo stream
→ 0 : protocollo 0
Il valore di ritorno della funzione socket() è un file descriptor. Stampando a video l'intero, notiamo che stampa a schermo 3. Se però si aprono più socket, si osserva che il valore dei socket crescono, partendo da 3. Questo ci suggerisce che è un indice, in particolare rappresenta la tebella di sistema contentente tutti gli stream. I tre indici prima di 3 sono rispettivamente:
• Std input
• Std output
• Std error, flusso anch'esso, di default è a schermo ma è diviso nel caso in cui si vogliano direzionare i flussi di errori da un'altra parte
Se si apre uno stream su file prima di aprire la socket:
FILE * f = fopen("file.txt", "w");
Si osserva che il valore di s, da 3 passa a 4 parchè abbiamo aperto un altro flusso prima di s
*/
s = socket( AF_INET, SOCK_STREAM, 0 );
// printf("Socket: %d\n", s);
/*
Se la funzione non ha successo ritorna '-1' e si ha un 'errno', che è una variabile globale che identifica il numero di errore. La chiamata di libreria, detta 'perror' che prende una stringa e ci aggiunge il messaggio di errore a sistema.
Per far scatenare un errore è sufficient inserire un numero casuale al posto di uno dei campi della funzione socket in quanto tali varibili rappresentano dei numeri interi.
*/
if( s == -1){
printf("ERRNO = %d (%d)\n", errno, EAFNOSUPPORT);
perror("Socket fallita\n");
return 1;
}
/*
Ora che abbiamo il socket, dobbiamo aprire lo stream. Per farlo, dobbiamo prendere l'iniziativa (perchè così è strutturato il modello Client-Server) attraverso una request al server. Tale richiesta viene mandata in formato stream. È necessario aprire uno stream verso il server. Come facciamo a raggiungere il server?
Avendo scelto il socket adibito per una connessione TCP, ci vengono messe a disposizione delle chiamata a sistema che verranno utilizzate per creare applicazione client, server e proxy. Quella di nostro interesse è la funzione connect().
La funzione connect() apre uno stream, una connessione TCP nei confronti di un server remoto. In particolare, questa chiamata a sistema connette il socket ad un indirizzo. Questi sono specificati come segue
→ int sockfd, ovvero il file descriptor del socket
→ const struct sockaddr *addr
→ socklen_t addrlen
Cerchiamo ora di capire come gestire la struct sockaddr, descritta anche in 'man 7 ip' come segue:
struct sockaddr_in{
sa_family_t sin_family; // famiglia
in_port_t sin_port; //
struct in_addr sin_addr; // indirizzo internet
}
Prima di tutto va osservato che il tipo 'struct sockaddr_in' è diverso da 'struct sockaddr'. Questo perchè l'interfaccia Linux deve valere per tutto, tanto che questa funzione non fa alcun riferimento al TCP. Abbiamo quindi degli input spcifici su una funzione generica: problema tipico di polimorfismo. Questo problema è stato risolto mediante i puntatori in quanto questi, qualnque sia il contenuto dello cella, contiene sempre e solo l'indirizzo. Però il puntatore è di tipo 'struct sockaddr' che è un tipo generale che non si usa mai. Questo tipo contiene solo un campo, ovvero un numero intero che specifica il tipo di struttura sockaddr specializzata, ovvero la famiglia. Nel momento in cui sin_family = AF_INET, si determina il modo in cui gli altri due campi della struct verranno letti.
In secondo luogo la funzione necessita di un indirizzo. L'indirizzo IP è composto da 4 campi che in decimale vanno da 0 a 255 e si traducon in 8 bit. Questi 4 valori in memoria sono salvati nel modo in cui si leggono: 147.162.2.100 all'indirizzo x sarà contenuto 147, all'indrizzo x + 1 sarà contenuto 162 e così via. Si identifca quindi un'interfaccia di rete connessa ad internet. L'instradamento dell'informazione, quindi tutte le procedure necessarie per far passare un dato da un server ad un'altro con determinati indirizzi IP è risolto dal livello 3.
Infine è necessaria la porta. Questo perchè nel modello Client-Server, il server non è altro che un programma eseguito. L'indirizzo di rete non basta in quanto ci porta alla macchina server, a noi serve l'effettivo programma. È come avere l'indirizzo di un condominio ma non conoscere l'effettiva porta dell'appartamento giusto. Non a caso il terzo campo è la porta: questa ci porta ad un end-point - ad un socket - che è un file descriptor a sua volta.
→ Per indirizzare un programma (server) che gira sulla macchina server, oltre all'indirizzo IP è anche necessario il port, ovvero un numero a 16bit, unico all'interno della macchina server, specifico per un tipo di servizio. Inoltre, i programmi server utilizzano dei numero di port, dette "well-known", che identificano il tipo di protocollo utilizzato. Nel nostro caso, per il servizio HTTP è necessaria la porta 80. [/etc/services]
*/
// imposto i campi della struct sockaddr_in del server
server_addr.sin_family = AF_INET; // la famiglia qualificherà il tipo specifico
/*
Alcune architetture sono BigEndian, altre LittleEndin; si è scelto che il network order è BigEndian. In una macchina Linux, è necessario utilizzare la funzione htons() function: Hosto-TO-Network-Short.
*/
server_addr.sin_port = htons(80); // la porta a cui ci vogliamo collegare
/*
Per capire quale indirizzo inserire per fare una richiesta a Google, è necessario digitare sul terminale:
nslookup www.google.com → mostra l'indirizzo del server di Google, in questo caso è 142.250.187.196
Come rappresentare un indirizzo IP? La soluzione è utilizzare un puntatore di tipo char (quindi che occupa 8 bit) e poi accedere alle 4 celle, ciascuna delle quali definisce una parte dell'indirizzo IPv4. Si osserva che in questo caso non è necessario fare la conversione Host-TO-Network perchè già sono stati salvati nell'ordine giusto
Se inserisco un indirizzo IP erratto, il programma si mette in attesa della CONFIRM in quanto è una chiamata bloccante ad funzione asincrona. Dopo un po' di tempo, se l'indirizzo è inesistente, il programma termina dopo che il time-out scade. In altri casi la richiesta può essere direttamente rifiutata, si può provare inserendo come indirizzo 127.0.0.1 con una port non aperta. Ciò nonostante questa casistica è sempre più rara perchè sempre più spesso sono implementati dei firewall che bloccano una richiesta indesiderata ancora prima che raggiunga il server
*/
p = (unsigned char *) &server_addr.sin_addr.s_addr;
p[0] = 142; p[1] = 250; p[2] = 187; p[3] = 196;
// per invocare la connect è necessario passare un puntatore a server_addr castato come sockaddr in modo da gestire il problema del polimorfismo con i puntatori
t = connect(s, (struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));
// Per capire se la connect ha successo si controlla che il suo valore di ritorno sia diverso da -1
if(t == -1){
perror("Connessione fallita\n");
return 1;
}
/*
Per utilizzare un socket, quindi per poter mandare a Google una stream si usano le stesse chiamate a sistema che si usano per scrivere sui file.
La funzione write() richiede un file un file_descriptor - che vale sia se è un socket, oppure un file -, un buffer e la dimensione.
Si osserva che la requesta ha due caratteri per indiricare il fine riga:
• \r che indica il carriage return
• \n che indica la nuova linea
*/
char * request = "GET /\r\n";
write(s, request, strlen(request));
/*
Dopo aver mandato la stringa, si deve leggera la richiesta. La funzione read(), come la connect(), è bloccante. Ciò significa che finchè il server non mi risponde rimango in attesa e blocco l'esecuzione.
Dato che stiamo usando un servizio di STREAM è necessario trattare la richiesta come tale e non come un messaggio. Uno stream è un flusso di dati che inizia e fino a che il server non finisce, si rimane in ascolto. Utilizzando la notazione:
t = read(s, response, RESPONSE_SIZE - 1);
Ci si aspetta un comportamento da messaggio. Non importa quanto RESPONSE_SIZE sia grande, ma la pagina di Google non arriverà mai tutta in un solo colpo.
La read() infatti tenta di leggere fino a RESPONSE_SIZE caratteri dal file descriptor. Questo non è altro che un limite per evitare di andare a scrivere in area di memoria non allocata e generare un segmentatio fault. Posto che questo limite sia sufficientemente grande, non è detto che tutto lo stream venga letto in una volta. Nel manuale [man 2 read] è sottolineato che quando la read ha successo viene ritornato il numero di bytes letti (zero nel caso in cui il file finisca) e la posizione del file è incrementata del suddetto numero. Viene inoltre specificato che non è un errore se il numero è minore dei bytes richisti, avviene perché il numero di byte inferiori sono gli effettivi disponibili nel momento in cui la read viene effettuata.
Dobbiamo gestire il momento in cui il file non è stato mandato completamente ma che il server è occupato a fare altro quindi siamo in attesa. È quindi necessario modificare la notazione precedente
t = read(s, response, RESPONSE_SIZE - 1);
*/
const int RESPONSE_SIZE = 2000000; // dimensione del buffer
char response[RESPONSE_SIZE]; // buffer per la risposta
// sleep(2);
for ( i = 0;
/*
→ se t == 0 significa che il file è stato letto del tutto e si esce dal ciclo
→ si sottrae i a RESPONSE_SIZE per limitare la read, altrimenti si legge il file letto scrivendo sul buffer response[] ma la massima dimensione scrivibile non si ridurrebbe: sbagliato perche se ho 100 byte disponibili, dopo aver scritto 40 byte no ho 100 - 40 = 60, non 100 nuovamente.
*/
t = read(s, response + i, RESPONSE_SIZE - 1 - i);
i += t // incremento del pezzo che ho letto quindi ad ogni iterazione i indica la posizione dove inziare a leggere riespetto a response[0]
) {
/*
Siccome lo stream arriva quando vuole stampo 't' per capire quando arriva.
Si osserva che sono presenti molti multipli di 1400. Esiste quindi una unità di 1.4k che spesso ritorna intera. Se prima del ciclo facciamo una sleep(2), terminati i due secondi 't' viene stampato una volta e value 52kB, indice del fatto che aspettando due secondi la pagina è arrivata tutta ed è stata letta in una sola volta.
Stando sopra il lvl stiamo notando delle tracce di ciò che succede nei livelli sottostanti.
*/
// printf("t = %d\n", t);
}
response[i] = 0; // null-terminare la stringa, 'i' indica esattamente la fine del file letto
// printf("%s", response);
return 0;
} // main