Lorsque nous avons commencé avec Introduction à l'API Socket en langage C, partie 1 Server Edition, nous avons commencé avec une API héritée limitée à IPv4 et l'avons progressivement développée. J'avais l'intention de le faire, mais à l'avenir, j'aimerais parler de la programmation de sockets en utilisant une API moderne, peu importe ce que je fais.
Par conséquent, cette fois, nous aborderons les détails en réécrivant le programme serveur d'écho qui renvoie la chaîne de caractères reçue du client à l'aide d'une API moderne. L'avantage des API modernes dans la programmation de socket par rapport aux exemples hérités n'est pas en termes de vitesse de traitement, mais dans la flexibilité de gérer divers cas, à la fois maintenant et à l'avenir.
Dans l'exemple hérité, il était uniquement possible de créer un socket qui ne peut prendre en charge que IPv4, mais dans l'exemple utilisant l'API moderne, les sockets IPv4 et IPv6 sont préparés et le traitement est ramifié en fonction du paquet de données reçu. Vous pouvez mettre en œuvre des fonctionnalités sexuelles.
De plus, il est possible de créer un programme qui ne dépend pas d'une famille d'adresses spécifique **, plutôt que d'être limité à IPv4 et IPv6. L'API moderne est mise en œuvre dans cet esprit.
Surtout pour ceux qui veulent devenir ingénieur iOS, Apple a déjà ** code interdit qui dépend d'IPv4 **. Pour cette raison, c'est une façon de penser et des compétences qui doivent être maîtrisées même dans le monde où IPv4 est encore monnaie courante.
Étant donné que l'explication de ce que j'ai essayé de frapper gros est compliquée, je vais la limiter à IPv4 cette fois, mais un programme qui prend en charge les paquets IPv6 peut également être modifié avec un petit changement. Il écrira quelque chose qui se concentre sur ce processus à une date ultérieure.
python
#include <sys/socket.h> //socket(), bind(), accept(), listen()
#include <stdlib.h> // exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <netdb.h> // getaddrinfo, getnameinfo, gai_strerror, NI_MAXHOST NI_MAXSERV
#include <string.h> //memset()
#include <unistd.h> //close()
#include <stdio.h> // IO Library
#define MAX_BUF_SIZE 1024
int get_socket(const char*);
void do_service(int);
static inline void do_concrete_service(int);
void echo_back(int);
static inline void usage(int);
int main (int argc, char *argv[]) {
int sock;
if (argc != 2) {
usage(EXIT_FAILURE);
}
if ((sock = get_socket(argv[1])) == -1){
fprintf(stderr, "get_socket() failure.\n");
exit(EXIT_FAILURE);
}
do_service(sock);
close(sock);
return EXIT_SUCCESS;
}
La fonction principale ressemble à celle ci-dessus. La spécification est telle que le numéro de port ou le nom du service est spécifié comme argument, mais s'il n'est pas spécifié ou si un supplémentaire est utilisé comme argument, la fonction suivante appelée usage sera exécutée pour y mettre fin. ..
python
static inline void usage (int status){
fputs ("\
argument count mismatch error.\n\
please input a service name or port number.\n\
", stderr);
exit (status);
}
Si l'argument est valide, créez une socket avec la fonction définie cette fois appelée get_socket et récupérez le descripteur de socket pour faire référence aux informations de contrôle.
Le descripteur de socket est exactement le même que le descripteur de fichier. J'ai écrit à ce sujet en détail dans Hacking a Linux file descriptor, donc si vous êtes intéressé, veuillez vous y référer.
Après avoir obtenu le descripteur de socket, le type de service réellement fourni en tant que serveur est résumé dans la fonction do_service définie cette fois.
J'ai fermé la socket à la fin, mais en fait do_service est dans une boucle infinie, donc ce programme n'atteint pas ici.
Lorsque vous transformez un tel programme en démon, implémentez-le de sorte que close puisse être appelé à un moment approprié en ajustant l'opération lorsqu'un signal spécifique est reçu à la fin. Nous verrons également comment implémenter le processus démon séparément.
Maintenant que nous connaissons le flux, examinons le traitement de la fonction get_socket. Cette fonction est responsable de la création du socket et du retour du descripteur de socket.
python
int get_socket (const char *port) {
struct addrinfo hints, *res;
int ecode, sock;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if ((ecode = getaddrinfo(NULL, port, &hints, &res) != 0)) {
fprintf(stderr, "failed getaddrinfo() %s\n", gai_strerror(ecode));
goto failure_1;
}
if ((ecode = getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) != 0){
fprintf(stderr, "failed getnameinfo() %s\n", gai_strerror(ecode));
goto failure_2;
}
fprintf(stdout, "port is %s\n", sbuf);
fprintf(stdout, "host is %s\n", hbuf);
if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
perror("socket() failed.");
goto failure_2;
}
if (bind(sock, res->ai_addr, res->ai_addrlen) < 0) {
perror("bind() failed.");
goto failure_3;
}
if (listen(sock, SOMAXCONN) < 0) {
perror("listen() failed.");
goto failure_3;
}
return sock;
failure_3:
close(sock);
failure_2:
freeaddrinfo(res);
failure_1:
return -1;
}
C'est un peu différent de l'exemple hérité IPv4 uniquement. Dans l'exemple hérité, nous devions définir une structure d'adresse sockaddr_in et spécifier les valeurs appropriées.
La méthode moderne consiste à transmettre une structure addrinfo qui stocke uniquement les informations qui sont la clé de la fonction getaddrinfo et à lui faire stocker toutes les informations manquantes en fonction de l'indice.
Je pense que vous pouvez ressentir l'atmosphère de l'exemple hérité, mais les différentes données spécifiées par l'API socket sont liées, et lorsqu'une donnée est spécifiée, les autres données nécessaires sont automatiquement déterminées. Parce que c'est le cas, c'est un algorithme qui tire parti de ses caractéristiques.
La structure addrinfo est une structure qui stocke les informations requises pour que l'API socket fasse référence aux informations d'adresse, et dans l'exemple hérité, elle inclut également la structure sockaddr qui a été utilisée directement.
python
/* Structure to contain information about address of a service provider. */
struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* Protocol family for socket. */
int ai_socktype; /* Socket type. */
int ai_protocol; /* Protocol for socket. */
socklen_t ai_addrlen; /* Length of socket address. */
struct sockaddr *ai_addr; /* Socket address for socket. */
char *ai_canonname; /* Canonical name for service location. */
struct addrinfo *ai_next; /* Pointer to next in list. */
};
Un membre du pointeur vers addrinfo appelé ai_next jouera un rôle important dans le futur. Si un membre d'une structure contient un pointeur vers la structure, il s'agit généralement d'une structure de données de liste concaténée. Comme c'est le cas dans ce cas, la personne avisée peut être arrivée à la conclusion que ce fait est lié à la programmation indépendante de la famille.
Désormais, étant donné que les informations des membres de cette structure addrinfo peuvent être passées en argument à l'API socket tel que bind et socket, une implémentation simple et flexible devient possible.
Dans la fonction getaddrinfo, spécifiez un pointeur vers le nom d'hôte ou la chaîne d'adresse IP dans le premier argument. Cette fois, spécifiez NULL, mais la raison sera décrite plus tard. Lors de la création d'un programme client, vous pouvez spécifier un pointeur vers un objet chaîne de caractères qui fait référence à un nom de domaine tel que "tajima-taso.jp".
Dans le deuxième argument, spécifiez un pointeur vers le numéro de port ou la chaîne de nom de service. Pour Linux, le nom du service est référencé via le serveur NIS ou à partir du fichier / etc / services sur l'hôte local.
Dans mon environnement, le numéro de port 8080 est ouvert, vous pouvez donc spécifier ./a.out 8080
, mais lorsque vous vérifiez / etc / services, 8080 est mappé vers webcache comme nom de service. Par conséquent, même si vous exécutez ./a.out webcache
, la même spécification sera obtenue.
Si AI_PASSIVE est spécifié dans le 3ème argument, la spécification NULL dans le 1er argument est équivalente à INADDR_ANY (IN6ADDR_ANY_INIT dans le cas d'IPv6) qui peut recevoir des connexions de tous les NIC comme dans l'exemple hérité. AI_PASSIVE sera décrit plus tard.
De plus, bien qu'elle puisse être limitée à la glibc, elle est implémentée de sorte que même si vous passez la chaîne de caractères "\ *", elle sera jugée NULL, donc même si vous spécifiez "\ *", cela fonctionnera de la même manière. Bien qu'implémenté dans le code source, il peut ne pas être polyvalent car il n'a pas été trouvé au niveau de la documentation.
Les spécifications AF_INET et SOCK_STREAM sont les mêmes que dans l'exemple hérité. Cela signifie respectivement système d'adresse IPv4 et protocole de type de flux = TCP.
Je pense que la spécification de AI_PASSIVE est souvent utilisée en combinaison avec le premier argument NULL. Spécifiez lorsque vous utilisez la structure addrinfo à compléter dans bind.
Puisque bind est un appel système utilisé lors de l'association d'une socket avec des informations d'adresse explicites, AI_PASSIVE agit comme un indicateur pour demander à la fonction getaddrinfo de compléter la structure addrinfo pour le serveur. .. Un socket qui écoute une telle connexion est appelé un socket passif.
Passez un pointeur sur le pointeur de la structure addrinfo comme quatrième argument. De là, vous pouvez faire référence au pointeur vers la structure addrinfo terminée par déréférencement. De plus, puisque la bibliothèque d'allocation de mémoire dynamique est utilisée en interne pour allouer la zone mémoire de la structure addrinfo, une fuite de mémoire se produira à moins que la zone ne soit libérée par la fonction freeaddrinfo chaque fois qu'elle n'est plus nécessaire. Je vais finir.
S'il y a une erreur, si vous transmettez l'entier qui sera la valeur de retour à la fonction gai_strerror, un pointeur vers la chaîne de caractères qui stocke le contenu d'erreur correspondant sera renvoyé, de sorte que le contenu est sorti vers la sortie d'erreur standard.
Ensuite, nous appelons la fonction getnameinfo, qui n'est pas nécessaire pour le processus de communication de socket lui-même. Cette fonction est utilisée pour faire référence au nom d'hôte (adresse IP) et au nom de service (numéro de port) de la structure d'adresse.
Le premier argument est un pointeur vers la structure de l'adresse de socket, le deuxième argument est la taille de l'objet, le troisième argument est un pointeur vers le tampon où le nom d'hôte doit être stocké, le quatrième argument est sa taille et le cinquième argument est service. Spécifiez un pointeur vers le tampon dans lequel le nom est stocké, sa taille dans le 6ème argument et divers indicateurs dans le 7ème argument.
La taille de hbuf est NI_MAXHOST, et la taille de sbuf est NI_MAXSERV, et la zone est sécurisée. Il est défini dans .h.
Dans mon environnement
netdb.h
# define NI_MAXHOST 1025
# define NI_MAXSERV 32
C'était.
Le 7ème argument définit les indicateurs de NI_NUMERICHOST et NI_NUMERICSERV, mais ce sont les indicateurs pour stocker le nom d'hôte au format numérique et le nom du service (numéro de port) au format numérique dans le tampon, respectivement.
Si vous l'exécutez en tant que ./a.out 8080
,
port is 8080
host is 0.0.0.0
Est affiché.
Effectuez ensuite un appel système de socket pour créer le socket. Dans l'exemple hérité, les trois valeurs passées comme arguments spécifiaient des littéraux constants, mais dans l'exemple moderne, les valeurs requises sont stockées dans les membres de la structure addrinfo acquise, utilisez-les donc. Créer. Cela vous permet d'écrire des programmes indépendants de la famille d'adresses.
En passant, en tant que traitement de socket, créez une structure de socket qui spécifie en interne les informations de protocole, stockez le pointeur vers l'objet fichier créé dans le membre qui stocke le pointeur vers l'objet fichier, et enfin fd_install l'objet fichier. Ce sera le flux de l'action.
Comme expliqué dans l'exemple hérité, bind est un appel système permettant de relier les sockets et les informations d'adresse. Le socket ne peut pas accepter les messages des hôtes distants à moins que les informations d'adresse ne soient liées en plus du protocole, il s'agit donc d'un processus nécessaire. Il ne devrait y avoir aucun problème avec l'adresse IP, mais s'il y a un numéro de port qui est déjà utilisé, le socket ne peut pas être lié, alors assurez-vous de spécifier un numéro de port libre ou un nom de service.
L'exemple d'appel de listen est le même que l'exemple hérité, sauf qu'il utilise une structure d'adresse.
À titre de révision, le programme de socket TCP est conçu pour créer un socket pour chaque connexion client afin de réaliser une communication qui établit une connexion.
Cette fois, la taille maximale de la file d'attente est spécifiée à l'aide d'une macro constante appelée SOMAXCONN définie dans bits / socket.h
.
SOMAXCONN signifie le nombre de files d'attente que le noyau définit comme valeur par défaut pour écouter. 128 dans mon environnement.
N'est-il pas possible que certains ingénieurs d'infrastructure arrivent à la conclusion lorsqu'ils voient cette valeur?
Dans le cas d'un serveur exécutant une application telle qu'une base de données avec de nombreuses demandes de connexion, ce nombre peut déborder de la file d'attente et interrompre la connexion, je pense donc qu'il existe de nombreux cas où cette valeur est augmentée de manière dynamique. ..
Après le traitement ci-dessus, le descripteur de socket qui attend la connexion est renvoyé à la fonction principale. Passez ensuite le descripteur de socket à la fonction do_service pour effectuer le service réel.
python
void do_service (int sock) {
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
struct sockaddr_storage from_sock_addr;
int acc_sock;
socklen_t addr_len;
for (;;) {
addr_len = sizeof(from_sock_addr);
if ((acc_sock = accept(sock, (struct sockaddr *) &from_sock_addr, &addr_len)) == -1) {
perror("accept() failed.");
continue;
} else {
getnameinfo((struct sockaddr *) &from_sock_addr, addr_len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV);
fprintf(stderr, "port is %s\n", sbuf);
fprintf(stderr, "host is %s\n", hbuf);
do_concrete_service(acc_sock);
close(acc_sock);
}
}
}
Dans la fonction do_service, une nouvelle socket est créée en exécutant l'appel système d'acceptation pour effectuer le traitement de communication réel avec le client, et le descripteur de socket est passé à la fonction do_concrete_service, qui est responsable du traitement du service spécifique.
Accepter est également comme expliqué dans l'exemple hérité. Le descripteur de socket est associé au membre qui stocke l'objet fichier de la structure de socket qui a réussi la négociation à trois (fonction fd_install) et le descripteur de socket est renvoyé.
Dans ce cas, les informations d'adresse du client sont stockées dans la variable from_sock_addr, donc getnameinfo est exécuté sur cette base pour afficher les informations d'adresse du client.
Dans l'exemple hérité, la structure sockaddr_in était utilisée pour stocker les informations du client, mais cette structure n'était utilisée que pour stocker les informations d'adresse IPv4. Par conséquent, afin de prendre en charge n'importe quelle famille d'adresses, nous passons à une méthode de définition de sockaddr_storage qui dispose de suffisamment d'espace de stockage pour stocker les informations d'adresse et les convertir si nécessaire. ..
static inline void do_concrete_service (int sock) {
echo_back(sock);
}
Dans la fonction do_concrete_service, la fonction centrée sur le traitement du service est exécutée. Dans ce cas, exécutez une fonction appelée echo_back qui renvoie le message du client tel quel. La raison de la séparation de ce processus est de faciliter la modification du contenu.
Au fait, dans mon environnement gcc, lorsque j'ai optimisé jusqu'à -O2, il s'est développé en ligne.
python
void echo_back (int sock) {
char buf[MAX_BUF_SIZE];
ssize_t len;
for (;;) {
if ((len = recv(sock, buf, sizeof(buf), 0)) == -1) {
perror("recv() failed.");
break;
} else if (len == 0) {
fprintf(stderr, "connection closed by remote host.\n");
break;
}
if (send(sock, buf, (size_t) len, 0) != len) {
perror("send() failed.");
break;
}
}
}
Quant au processus d'envoi / réception, il s'agit à l'origine d'une implémentation très abstraite, donc à ce stade, il est presque identique à l'exemple hérité.
Cette fois, le contenu de traitement lui-même est presque le même que celui de l'exemple hérité, mais nous avons pu créer un formulaire de base pour la programmation qui ne dépend pas du protocole ou de la famille d'adresses.
Après cela, lorsque je présenterai un exemple d'API socket, je le créerai à l'aide de cette API.
** "Je comprends comment utiliser la fonction getaddrinfo! Je veux juste savoir comment utiliser IPv4 ou IPv6!" **
Pour la méthode, RFC6555 décrit la méthode adoptée par Google Chrome et Mozilla Firefox, donc si vous êtes intéressé, veuillez vous référer à ce qui suit.
Happy Eyeballs: Success with Dual-Stack Hosts
Concernant la fonction getaddrinfo, Stack overflow vulnérabilité a été découverte il y a quelque temps (actuellement). Est corrigé), il est donc important de faire attention non seulement à getaddrinfo mais aussi aux bibliothèques telles que la glibc utilisée et les mises à jour logicielles.
Noyau Linux: 2.6.11 glibc:glibc-2.12.1 CPU:x86_64
OS CentOS release 6.8
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-17)
Recommended Posts