cours/content/reseau/4-API_C/index.md
2018-10-12 23:06:04 +02:00

318 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Programmation réseau avec l'API C"
date: 2018-09-26
modify: 2018-10-03
tags: ["programmation", "C"]
categories: ["Réseau", "Cours"]
---
# API C pour un paquet UDP (exemple du DNS)
1. `socket()` : Appel système qui retourne un identifiant unique (entier non
signé) représentant une socket ouverte par le noyau, dans le style d'un
descripteur de fichier.
2. `bind(53)` : Le processus annonce qu'il se positionne sur le port 53 en
écoute (serveur DNS)
3. `rcvfrom()` : récupère les données (datagrammes) depuis le noyau, c'est un
appel bloquant.
4. `sento()` : le processus fournit au noyau les données pour le transfert (
l'écriture). C'est l'équivalent de `write()` utilisé poour les fichiers.
5. `close()` : ferme la connexion.
Ces étapes décrites ci-dessus sont valables pour la partie serveur, la partie
client est plus simple : `socket()` -> `sendto()` -> `rcvfrom()` -> `close()`
# API pour un transfert TCP (avec connexion)
# côté serveur
1. `socket()`
2. `bind(80)`
3. `listen()` : annonce l'ouverture du port et l'écoute
4. `accept()` : attend des connexion avec un client (état disponible)
5. `read()` : appel système lit les données provenant du réseau, en HTTP ce
serait la demande de la page `index.html` par exemple.
6. `write()` : renvoi les données vers le réseau qui seront ensuite envoyées
par le noyau vers le client.
## côté client
1. `connect()` : appel cloquant pour initier la connexion.
2. `write()` : envoyer les donnés vers le client (la requete HTML par exemple)
3. `read()` : lit les données (bloquant). Dès que les données sont prête, elles
sont envoyées à l'application. Si rien n'est lut, l'appel se terminera alors
seulement au moment du `close()`
# Socket
Pour démarrer une socket, plusieurs information son importante et changerons la
manière de procéder :
- la famille d'adresse : IPv4, IPv6
- le type de socket : échange simple de datagrammes (UDP) ou par une
connexion (TCP)
- le protocole : TCP, UDP, ICMP ...
- l'adresse locale (IP et port)
- l'adresse distante (IP et port)
# API C
Il faut bien avoir à l'esprit qu'en réseau, les données sont toujours au forman
big-endian. Pour avoir plus d'explications voir la page Wikipedia sur
[l'endianess](https://fr.wikipedia.org/wiki/Endianness).
Il faut donc penser à utiliser des fonctions de conversion :
```c
/* Conversion vers/depuis ordre réseau */
uint16_t htons(uint16_t hostshort); #host to network short
uint16_t ntohs(uint16_t netshort); #network to host short
uint32_t htonl(uint32_t hostlong); #host to network long
uint32_t ntohl(uint32_t netlong); #network to host long
/* Conversion ascii/binaire */
int inet_aton(const char *ascii, struct in_addr *binaire);
char *inet_ntoa(struct in_addr binaire);
/* De même, mais portables v4/v6 */
int inet_pton(sa_family_t af, const char *ascii, void *binaire);
const char *inet_ntop(sa_family_t af, const void *binaire, char *ascii, socklen_t size);
```
## Structure pour IPv4
```c
struct in_addr {
/* ... */
__be32 s_addr;
}
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET6 */
struct in6_addr sin_addr;
__be16 sin_port;
}
```
## Structure pour ipv6
```c
struct in6_addr {
/* ... */
u8 s6_addr[16];
}
struct sockaddr_in {
sa_family_t sin6_family; /* AF_INET6 */
struct in6_addr sin6_addr;
__be16 sin6_port;
}
```
## API UDP
```c
/* Créer une socket */
int socket(int domain, int type, int protocol);
/* e.g. fd = socket(AF_INET, SOCK_DGRAM, 0); */
int bind(
int sockfd, # numéro de descripeur de socket
const struck sockaddr *addr # sock_addr peut être de type 4 ou 6 (cast)
socklen_t addrlen # taille de a structure sock_addr
)
/* Choisir ladresse distante (IP et port), optionnel, pour utiliser
send/write/recv/read plutôt que sendto/recvfrom */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Envoyer un datagramme UDP à un service dune machine donnée */
ssize_t sendto(
int sockfd,
const void *buf,
size_t len,
int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen
);
/* Recevoir un datagramme UDP depuis nimporte quelle machine */
ssize_t recvfrom(
int sockfd,
void *buf, #tampon de données (ici à recevoir)
size_t len,
int flags,
struct sockaddr *src_addr,
socklen_t *addrlen
);
/* Fermer la socket */
int close(int fd);
```
## API C TCP
```c
* voir man 7 tcp pour plus dexplications */
/* Créer une socket */
int socket(int domain, int type, int protocol);
/* e.g. fd = socket(AF_INET, SOCK_STREAM, 0); */
/*** Partie client ***/
/* Se connecter à un service dune machine donnée */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*** Partie serveur ***/
/* Attacher à un port local donné */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* Passer en mode serveur */
int listen(int sockfd, int backlog);
/* Accepter une nouvelle connexion */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* Fermer la socket */
int close(int fd);
/* Récupérer ladresse locale */
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* Récupérer ladresse distante */
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
```
# `write()`, `read()`, Tampons noyau
![les buffers noyau](./images/kernel_buffers.svg)
Pour chacune des connexions établies le noyau Linux créer des tampons : un pour
la lecture et un pour l'écriture. Pour la l'écriture, lorsqu'un processus
invoque un `write()`, les données sont d'abord écrites dans un tampon. S'il est
plein (sa taille est définie à l'avance), le noyau bloque l'appel `write()` et
lorsque de la place se libère, il le débloque.
De l'autre côté, les données sont reçues dans le tampon `read()` et lorsqu'elles
sont transférées au processus, un segment *ACK* est envoyé à l'expéditeur. le
destinataire averti l'expéditeur de la place disponible dans son tampon `read()`
par le biais du champ `window size` (octet 16 à 18 d'un sergent TCP) dans un
segment *ACK*. Si le tampon contient, par exemple 2ko mais que le `read()` en
demande 10ko, les 2Ko sont automatiquement transférés au processus.
Les tampons peuvent fausser complètement les outils de mesure de performance
réseau, en effet lors d'un appel `write()` les données sont transmise dans le
tampon très rapidement faussant alors la mesure du débit.
## visualiser les buffers.
Il est possible de visualiser les données en attentes dans les buffets avec la
commande `netstat` :
```shell
[root@eph-archbook ephase]# netstat
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 eph-archbook:47954 mail.gandi.net:imaps ESTABLISHED
tcp 0 0 eph-archbook:52264 imap.u-bordeaux.fr:imap ESTABLISHED
tcp 32 0 ?192.168.1.91:34456 vpn-0-24.aquilene:https CLOSE_WAIT
tcp 32 0 192.168.1.91:33424 vpn-0-24.aquilene:https CLOSE_WAIT
...
```
L'étal des tampons est indiqué par les champs `Recv-Q` et `Send-Q`.
## option de socket TCP
Les entêtes complète d'un datagramme TCP pèse au moins 54 octets (entêtes
Ethernet et IP comprises). Afin d'optimiser l'envoi de données, il est souvent
préférable de concaténer plusieurs appels `write()` ou d'utiliser `printf()` et
les mécanisme de tampons de la libc. Il est aussi possible de demander à TCP
d'attendre l'arrivée d'autres données avant d'envoyer un segment avec bien
évidemment la gestion d'un *timeout* pour ne pas attendre trop longtemps
(algorithme de [Nagle][l_nagle]). Ce n'est cependant pas adapté à tous les
protocoles : si ce système fonctionne bien pour HTTP, ce n'est pas le cas de SSH
ou la latence induite par ce mécanisme pose problèmes.
```C
#include <sys/socket.h>
int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int getsockopt (int sockfd, int level, int optname, void *optval, socklen_t optlen);
```
- `int level` : niveau de la pile ou appliquer l'option, `SOL_TCP` signifiera
la partie TCP.
- `int optname` : l'option à activer, par exemple `TCP_NODELAY` afin de
désactiver l'algorithme de Nagle. Il est possible aussi de mettre un
bouchon au tampon `write()` avec `TCP_CORK`. Il faudra alors le désactiver
pout permettre au noyau d'envoyer les segments depuis les données du
*buffer*
[l_nagle]:https://fr.wikipedia.org/wiki/Algorithme_de_Nagle
### l'option `SO_REUSEADDR` de `SOL_SOCKET`
Par défaut, le système empêchera la connexion à une socket si un service y
était quelques minutes auparavant. Dans le cadre du test d'un programme ou pour
du debug, il est parfois nécessaire de désactiver ce comportement, et c'est
possible avec :
```C
setsockopt ( fd, SOL_SOCKET, SO_REUSEADDR, "1");
```
# La Qos (Qualité de Service)
En réseau il est possible d'obtenir :
- une bonne latence
- un bon débit
- de la fiabilité
Il ne sera pas possible d'obtenir les trois. Il existe plusieurs solutions pour
prioriser les flux. Par exemple les petits paquets : leurs tailles indique
sûrement que l'on recherche une meilleure latence plutôt qu'un gros débit.
Il est aussi possible de faire du DPI *Deep Packet Inspection* que ne serait pas
très approprié du point de vue de l'éthique et de la loi (neutralité du net...)<
En TCP il existe le champs ToS *Type of service" Mais celui-ci est inutilisé du
fait de son implémentation différentes chez les fabricants de matériels et
logiciels.
Les équipements sur Internet ont tous un système de buffer, ce qui n'est pas
sans poser problèmes pour la *QoS* : s'il est facile de maitriser son routeur,
éventuellement le DSLAM si son fournisseur d'accès à Internet le permet, il est
est tout autres pour les équipements par lesquels transitent nos paquets.(voir
le [Bufferbloat][l_buffetbloat])
[l_bufferbloat]:https://en.wikipedia.org/wiki/Bufferbloat
# API réseau et threads
Il est tout à fait possible d'utiliser les *threads* et les *processus* pour
créer des buffers différents et ainsi paralléliser l'envoi / réception de
données et ainsi éviter de bloquer l'ensemble de son application avec un
`write()` ou un `read()`. Il est aussi possible d'utiliser l'API `SELECT` et
ainsi éviter de complexifier son code avec des threads / processus.
```C
void FD_ZERO(fd_set *set);
void FD_CLR(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/* Attend des données lisibles sur fd1 ou fd2, traite en conséquence */
int attend(int fd1, int fd2) {
fd_set set;
int max;
FD_ZERO(&set);
FD_SET(fd1, &set);
FD_SET(fd2, &set);
max = MAX(fd1, fd2);
if (select(max+1, &set, NULL, NULL, NULL) == -1)
return -1;
if (FD_ISSET(fd1, &set))
traite(fd1);
if (FD_ISSET(fd2, &set))
traite(fd2);
return 0;
}
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
```