[LINUX] Introduction à l'API Socket apprise en langage C, partie 1, édition serveur

Les ingénieurs Web utilisent TCP / IP, qu'ils en soient conscients ou non. C'est l'un des protocoles de communication indispensables pour Internet.

Ces dernières années, avec la diffusion de l'IoT, etc., il est devenu un savoir indispensable dans des domaines autres que les technologies Web classiques.

Par conséquent, il est devenu la norme de facto pour les API réseau. Je voudrais étudier à nouveau le réseau basé sur l'interface de socket BSD.

Il existe de nombreux livres et documents sur le mécanisme du réseau, en particulier TCP / IP, mais tant que je n'ai pas lu le code source du langage C en utilisant l'API socket, je ne pouvais pas comprendre l'image, quelle que soit l'explication, et le mécanisme n'était pas tellement. Je ne pouvais pas comprendre, donc je vais essentiellement suivre le flux du code source du langage C et apprendre le fonctionnement par parties en fonction des données et des traitements utilisés.

Bien sûr, je ne pense pas que ce soit parfait, alors je vous serais reconnaissant si vous pouviez signaler des erreurs.

Examinons d'abord le mécanisme de communication entre différents hôtes via un programme de communication TCP utilisant l'API socket orientée connexion.

Le code source que j'ai écrit pour la première fois est un programme serveur TCP.

Il s'agit d'un exemple d'héritage épuisé uniquement IPv4, mais veuillez noter qu'il est préférable d'être simple pour comprendre comment cela fonctionne. Finalement, nous verrons des exemples utilisant la structure sockaddr_in6 qui prend en charge IPv6, et des exemples utilisant des API telles que getaddrinfo qui ne dépendent pas de la famille d'adresses.

Postscript

Un exemple d'utilisation de getaddrinfo a été écrit dans Introduction to Modern Socket API Learned in C.

Environnement d'exécution

En gros, il s'agit de CentOs 6.8 de x86_64. Le compilateur est gcc sans aucune option spécifiée. Je pense qu'il existe des différences selon le processeur, le système d'exploitation et le compilateur, mais les bases devraient fonctionner de la même manière.

Code source

tcpd.c


#include <stdio.h> //printf(), fprintf(), perror()
#include <sys/socket.h> //socket(), bind(), accept(), listen()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset()
#include <unistd.h> //close()

#define QUEUELIMIT 5

int main(int argc, char* argv[]) {

	int servSock; //server socket descriptor
	int clitSock; //client socket descriptor
	struct sockaddr_in servSockAddr; //server internet socket address
	struct sockaddr_in clitSockAddr; //client internet socket address
	unsigned short servPort; //server port number
	unsigned int clitLen; // client internet socket address length

	if ( argc != 2) {
		fprintf(stderr, "argument count mismatch error.\n");
		exit(EXIT_FAILURE);
	}

	if ((servPort = (unsigned short) atoi(argv[1])) == 0) {
		fprintf(stderr, "invalid port number.\n");
		exit(EXIT_FAILURE);
	}

	if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ){
		perror("socket() failed.");
		exit(EXIT_FAILURE);
	}

	memset(&servSockAddr, 0, sizeof(servSockAddr));
	servSockAddr.sin_family      = AF_INET;
	servSockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servSockAddr.sin_port        = htons(servPort);

	if (bind(servSock, (struct sockaddr *) &servSockAddr, sizeof(servSockAddr) ) < 0 ) {
		perror("bind() failed.");
		exit(EXIT_FAILURE);
	}

	if (listen(servSock, QUEUELIMIT) < 0) {
		perror("listen() failed.");
		exit(EXIT_FAILURE);
	}

	while(1) {
		clitLen = sizeof(clitSockAddr);
		if ((clitSock = accept(servSock, (struct sockaddr *) &clitSockAddr, &clitLen)) < 0) {
			perror("accept() failed.");
			exit(EXIT_FAILURE);
		}

		printf("connected from %s.\n", inet_ntoa(clitSockAddr.sin_addr));
	  close(clitSock);
	}

	return EXIT_SUCCESS;
}

Lignes 1 à 6

Chargement des en-têtes requis. Le commentaire de droite décrit ce qui est lu pour utilisation. Certains d'entre eux peuvent être inclus et utilisés à partir de là, mais je les omettrai.

Lignes 12-17

Allouez une zone de mémoire pour le type de données requis. Comme d'habitude, en langage C, la taille du type autre que le type char n'est pas définie, elle peut donc différer en fonction de l'environnement, mais int est de 4 octets et short est de 2 octets. De plus, l'octet fait généralement 8 bits, mais comme il ne s'agit pas d'une définition stricte, il sera exprimé sous forme d'octet, qui est une représentation de données en communication, si nécessaire.

Lignes 19-22

Vérification des arguments. Le numéro de port associé à la socket peut être spécifié par n'importe quel nombre au moment de l'exécution.

Lignes 24-27

Le numéro de port, qui est une représentation sous forme de chaîne de caractères, est converti en une représentation entière par la fonction atoi. Atoi renvoie 0 si la conversion d'entiers échoue, donc dans ce cas, il semble qu'il sera plus facile à utiliser s'il juge que le nom du service est spécifié par la chaîne de caractères et les branches, mais cette fois nous n'accepterons que des nombres.

Lignes 29-31

L'API socket est enfin là.

socket () est un appel système qui demande au système d'exploitation de créer un socket. Comme la famille de protocoles est spécifiée dans le premier argument, spécifiez PF_INET, ce qui signifie utiliser la famille de protocoles TCP / IP.

Actuellement, AF_INET, qui sera décrit plus tard, a la même signification, mais PF_INET est utilisé en respectant le concept de conception de l'API de socket. (Honnêtement, je ne connais pas l'image du monde où PF_INET était censé être nécessaire, il serait donc utile que vous puissiez me dire si vous comprenez quel type d'implémentation de protocole est censé être.)

Le deuxième argument spécifie le type de socket. Cette fois, nous utiliserons TCP, qui est un protocole de type flux qui garantit une communication hautement fiable, alors spécifiez SOCK_STREAM.

Pour le troisième argument, spécifiez IPPROTO_TCP, qui signifie TCP, qui est le protocole à utiliser. Si 0 est spécifié, il sera automatiquement déterminé à partir de la famille de protocoles à utiliser et du type de socket, mais il devrait être explicitement spécifié au cas où le nombre de protocoles augmenterait à l'avenir.

Dans mon environnement, ce qui peut être spécifié pour chacun est écrit dans / usr / include / bits / socket.h ou / usr / include / netinet / in.h.

Lignes 34-37

Initialisation de la structure sockaddr_in côté serveur.

La structure sockaddr_in est un type de données utilisé pour associer des adresses telles que des adresses IP et des numéros de port aux sockets TCP / IP.

La structure sockaddr_in dans mon environnement ressemble à ceci:

python


struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;         /* Port number.  */
    struct in_addr sin_addr;        /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
               __SOCKADDR_COMMON_SIZE -
               sizeof (in_port_t) -
               sizeof (struct in_addr)];
};

python


__SOCKADDR_COMMON (sin_);

Par préprocesseur dans mon environnement

python


#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

Puisqu'il est remplacé par, il devient sa_family_t sin_family.

Puisque nous ne savons pas quelle valeur se trouve dans la zone de mémoire allouée, nous utiliserons memset pour le remettre à zéro, puis stocker la valeur requise dans chaque champ.

Pour sin_family, spécifiez AF_INET, qui indique qu'il s'agit d'une famille d'adresses Internet (IPv4).

Pour sin_addr, spécifiez la structure in_addr.

La structure in_addr ressemble à ceci dans mon environnement:

python


typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};  

Utilisez INADDR_ANY pour le champ s_addr. Cela permet d'accepter les connexions à toutes les adresses IP en fonction du numéro de port, même si le NIC du serveur a plusieurs adresses IP qui lui sont attribuées.

En raison de l'architecture Little Endian, x_86 nécessite une conversion en ordre d'octets au standard de réseau Big Endian lors de l'envoi de données multi-octets à un autre hôte. Utilisez des fonctions telles que htonl et htons pour convertir dans l'ordre des octets du réseau.

Endian possède une connaissance particulièrement importante pour la programmation réseau, mais pour le moment, cette fois [lien Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3% E3% 83% 87% E3% 82% A3% E3% 82% A2% E3% 83% B3) J'écrirai plus à ce sujet lorsque je commencerai à utiliser le champ bit de la structure.

À propos, INADDR_ANY semble signifier 0x00000000 dans mon environnement, il semble donc qu'il ne soit pas affecté par la conversion, mais je traite la fonction htonl pour rendre le codage cohérent. Pour autant que this soit vu, il est préférable de le spécifier pour le moment. Cela semble bon.

htonl signifie hôte à réseau long et convertit un entier de 4 octets dans votre environnement en ordre d'octets du réseau. De même, le numéro de port est traité par la fonction htons. htons est une abréviation de host to network short, qui convertit un entier de 2 octets dans votre environnement en ordre d'octets du réseau.

Lignes 39-42

L'adresse IP et le numéro de port sont liés au socket créé par l'appel système bind (). Le socket ne peut accepter les messages des hôtes distants que si le protocole, l'adresse IP et le numéro de port sont associés.

Puisque le protocole à utiliser est associé lors de la création du socket, transmettez la structure sockaddr_in créée précédemment et sa longueur à la fonction bind pour associer l'adresse IP et le numéro de port au socket.

Il convient de noter ici que la structure sockaddr_in est convertie en pointeur de la structure appelée sockaddr.

La structure sockaddr ressemble à ceci dans mon environnement:

python


struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];       /* Address data.  */
  };  
};  

La partie de __SOCKADDR_COMMON est remplacée par le préprocesseur, elle devient donc sa_family_t sa_family.

Les bits restants sont déclarés comme un tableau de type char, donc vous pourriez penser que quelque chose ne va pas, En bref, la structure sockaddr se compose de champs de famille d'adresses et d'une zone de stockage pouvant contenir 14 bits.

Alors que sockaddr est un type de données à usage général pour les API de socket, sockaddr_in est positionné comme un type de données spécialisé pour TCP / IP.

En conséquence, chaque API socket n'a besoin que d'accepter le pointeur de la structure sockaddr comme argument, et en examinant sa_family, la structure du champ de données de la structure peut être connue, de sorte que le traitement peut être ramifié de manière appropriée. Je vais. On peut dire que la mise en œuvre est consciente de la polyvalence.

Je pense personnellement que c'est l'un des exemples concrets de la procédure d'échange de données et du protocole qui négocie le contenu des données.

De plus, lorsque la liaison échoue, un message à cet effet est probablement affiché à partir de perror, mais c'est une bonne idée de vérifier si le port est déjà associé à une autre socket.

S'il s'exécute et s'arrête de manière répétée, il peut être nécessaire d'attendre un moment en tenant compte de l'état du délai d'attente du socket après la déconnexion de la connexion TCP. Utilisons netstat.

Lignes 44-47

L'appel système listen () est appelé et l'état de connexion du client est accepté pour la première fois. Toutes les demandes de connexion des clients qui arrivent plus tôt seront rejetées. Par exemple, si le programme exécutable est a.out, sur la ligne de commande,

./a.out 8080

Si vous essayez d'exécuter, il sera dans un état d'acceptation des connexions des clients par écoute, donc si vous exécutez une commande telle que netstat -tln sur un autre terminal

tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN

Vous pouvez voir que ça sort.

Cela signifie que ce programme serveur est prêt à accepter les demandes de connexion des clients.

S'il existe une demande de connexion (SYN) du client dans cet état, le serveur crée une nouvelle structure de socket qui stocke les informations du client en fonction du paquet.

Sur cette base, le module TCP côté serveur effectue une négociation en trois étapes. Si la prise de contact réussit, la nouvelle structure sera à l'état ESTABLISHED et sera mise en file d'attente dans la structure de liste jusqu'à ce qu'elle soit appelée par accept.

Lors de l'exécution du programme serveur à titre d'essai, spécifiez le numéro de port 8080 dans le navigateur Web du client, répétez l'accès et exécutez une commande telle que netstat -tnc côté serveur pour vérifier la progression de cet état. Je vais.

En passant, au début, j'étais en colère de ne pas accéder au numéro de port suspect du navigateur si je l'essayais avec un numéro de port approprié, il semble donc que le fournisseur de chaque navigateur limite la connexion du numéro de port de l'hôte distant. Bien entendu, il est nécessaire d'ouvrir le port correspondant côté serveur.

Lignes 49-58

Pour envoyer et recevoir des données réelles vers et depuis le client, accept prend la structure de socket ESTABLISHED de la file d'attente, lui affecte un descripteur et la renvoie au processus utilisateur.

Le descripteur de socket a la même valeur entière que le descripteur de fichier et correspond à l'index du tableau qui contient le pointeur de l'objet. Ce tableau est un tableau de pointeurs commun aux objets d'entrée / sortie utilisés dans le noyau, et est utilisé pour utiliser une interface abstraite pour l'entrée / la sortie. J'ai écrit à ce sujet dans Hacking a Linux file descriptor, donc si vous êtes intéressé, veuillez également voir ici.

À l'origine, j'aimerais l'utiliser pour envoyer et recevoir des données vers et depuis le client, mais cette fois je les détruis dès que la socket devient disponible.

À propos, les variables clitSockAddr et clitLen stockent les informations du client, donc cette fois nous les utilisons pour afficher l'adresse IP du client. inet_ntoa génère une chaîne décimale à points à partir de la structure in_addr et renvoie l'adresse de début de sa zone de stockage.

En supposant que le domaine du serveur est hogehoge.com, la connexion sera fermée dès que vous accéderez à http: //hogehoge.com: 8080 avec un navigateur, donc Chrome n'affichera que quelque chose comme ERR_EMPTY_RESPONSE, mais sur le serveur La sortie sera comme "connecté à partir de xxx.xxx.xxx.xxx.`.

Ce qui était intéressant, c'est que dans Chrome et Safari, le message ci-dessus était de 3 lignes dans un accès, mais dans Firefox, il y avait 13 lignes. Il peut être intéressant d'analyser le type d'échange effectué en une seule requête avec tcpdump.

Cette fois, j'ai utilisé un navigateur Web comme logiciel client TCP, mais comme il est plus facile de vérifier diverses choses si j'ai également créé le logiciel client moi-même, 2nd Je voudrais créer un logiciel client TCP.

Livre de référence

Recommended Posts

Introduction à l'API Socket apprise en langage C, partie 1, édition serveur
Introduction à l'API Socket apprise en langage C 2e édition client
Introduction à l'API Socket apprise en langage C 3e serveur / client TCP # 1
Introduction à l'API Socket apprise en langage C 4e édition serveur / client UDP # 1
Une introduction à l'API de socket moderne pour apprendre en C
Aller à la langue pour voir et se souvenir du langage Partie 7 C en langage GO
Introduction à Protobuf-c (langage C ⇔ Python)
Résumé du chapitre 2 de l'introduction aux modèles de conception appris en langage Java
Chapitre 4 Résumé de l'introduction aux modèles de conception appris en langage Java
Résumé du chapitre 3 de l'introduction aux modèles de conception appris en langage Java
Méthode de contrôle exclusive multi-processus en langage C
Configurer un serveur UDP en langage C
Exemple de serveur d'API qui reçoit JSON dans Golang
Essayez de créer un module Python en langage C
Résumé du début au chapitre 1 de l'introduction aux modèles de conception appris en langage Java
Introduction au langage Python
Introduction à PyQt4 Partie 1
Comment limiter la publication de l'API dans la bibliothèque partagée en langage C de Linux
API C en Python 3
J'ai essayé d'illustrer le temps et le temps du langage C
Intégration du langage machine en langage C
Tri de tas fait en langage C
Introduction à Ansible Part «Inventaire»
Introduction à Ansible Part ④'Variable '
Résolution de l'introduction d'AOJ aux algorithmes et aux structures de données en Python -Partie1-
Résolution de l'introduction d'AOJ aux algorithmes et aux structures de données en Python -Partie2-
Résolution de l'introduction d'AOJ aux algorithmes et aux structures de données en Python -Partie4-
Accédez à l'API New Relic en Python pour obtenir l'état du serveur
Résolution de l'introduction d'AOJ aux algorithmes et aux structures de données en Python -Partie3-
Test de module multi-instance en langage C
Introduction à Ansible Partie 2 'Grammaire de base'
Introduction à TensorFlow - Hello World Edition
Introduction à Python Hands On Partie 1
Réaliser une classe d'interface en langage C
Comment envelopper C en Python
Introduction à la préparation python-environnement (édition Mac)
Optimisation apprise avec OR-Tools Part0 [Introduction]
Introduction au Deep Learning ~ Dropout Edition ~
Segfo avec 16 caractères en langage C
Introduction à Python Django (2) Édition Mac
Introduction à Ansible Partie 1 Hello World !! '
De l'introduction de l'API GoogleCloudPlatform Natural Language à son utilisation
Langage C pour voir et se souvenir de la partie 2 Appeler le langage C à partir de la chaîne Python (argument)
[Langage C] Je souhaite générer des nombres aléatoires dans la plage spécifiée
Langage C pour voir et se souvenir de la partie 1 Appeler le langage C depuis Python (bonjour le monde)
Langage C pour voir et se souvenir de la partie 4 Appelez le langage C depuis Python (argument) double
Langage C pour voir et se souvenir de la partie 5 Appel du langage C à partir du tableau Python (argument)