cours/content/reseau/4_API_C/index.md

11 KiB
Raw Permalink Blame History

title date modify tags categories
Programmation réseau avec l'API C 2018-09-26 2018-10-03
programmation
C
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.

Il faut donc penser à utiliser des fonctions de conversion :

/* 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

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

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

/* 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

* 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

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 :

[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). 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.

#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'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 :

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])

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.

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);