[LINUX] Introduction à l'API Socket apprise en langage C 3e serveur / client TCP # 1

[Introduction à l'API Socket apprise dans la 2e édition du client en langage C] Il s'agit de la version serveur / client n ° 1 de la suite de (http://qiita.com/tajima_taso/items/fb5669ddca6e4d022c15). Il est peu probable que ce thème soit complété avec un seul article, j'ai donc décidé de le faire n ° 1.

Je vais continuer à baser la programmation des sockets sur le code source du langage C, mais comme j'ai couvert les choses principales telles que la création de sockets et l'établissement de connexions par TCP la dernière fois et deux fois avant, à moins qu'il n'y ait de nouvelles informations Je vais omettre l'explication de cette partie.

Cette fois, nous examinerons l'échange de messages réel entre le client et le serveur (données échangées par l'application réseau), montrant quelques signes de protocole d'application.

Environnement d'exécution

Logiciel client Mac OS X 10.10.5 gcc CentOs 6.8 gcc du logiciel serveur x86_64

Tout d'abord, regardons le logiciel client.

Code source du logiciel client

tcpc.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(), inet_aton()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset(), strcmp()
#include <unistd.h> //close()

#define MSGSIZE 1024
#define BUFSIZE (MSGSIZE + 1)

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

	int sock; //local socket descriptor
	struct sockaddr_in servSockAddr; //server internet socket address
	unsigned short servPort; //server port number
	char recvBuffer[BUFSIZE];//receive temporary buffer
	char sendBuffer[BUFSIZE]; // send temporary buffer

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

	memset(&servSockAddr, 0, sizeof(servSockAddr));

	servSockAddr.sin_family = AF_INET;

	if (inet_aton(argv[1], &servSockAddr.sin_addr) == 0) {
		fprintf(stderr, "Invalid IP Address.\n");
		exit(EXIT_FAILURE);
	}

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

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

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

	printf("connect to %s\n", inet_ntoa(servSockAddr.sin_addr));

	while(1){
		printf("please enter the characters:");
		if (fgets(sendBuffer, BUFSIZE, stdin) == NULL){
			fprintf(stderr, "invalid input string.\n");
			exit(EXIT_FAILURE);
		}

		if (send(sock, sendBuffer, strlen(sendBuffer), 0) <= 0) {
			perror("send() failed.");
			exit(EXIT_FAILURE);
		}

		int byteRcvd  = 0;
		int byteIndex = 0;
		while (byteIndex < MSGSIZE) {
			byteRcvd = recv(sock, &recvBuffer[byteIndex], 1, 0);
			if (byteRcvd > 0) {
				if (recvBuffer[byteIndex] == '\n'){
					recvBuffer[byteIndex] = '\0';
					if (strcmp(recvBuffer, "quit") == 0) {
						close(sock);
						return EXIT_SUCCESS;
					} else {
						break;
					}
				}
				byteIndex += byteRcvd;
			} else if(byteRcvd == 0){
				perror("ERR_EMPTY_RESPONSE");
				exit(EXIT_FAILURE);
			} else {
				perror("recv() failed.");
				exit(EXIT_FAILURE);
			}
		}
		printf("server return: %s\n", recvBuffer);
	}

	return EXIT_SUCCESS;
}

Lignes 1 à 50

L'en-tête, le type de données, la création de socket, l'établissement de la connexion, etc. sont les mêmes que la version client de Dernière fois, donc je vais l'omettre.

Lignes 51-56

La boucle while démarre et printf () sort une instruction pour inviter l'entrée de caractères à la sortie standard.

Ensuite, utilisez fgets pour recevoir la chaîne de l'entrée standard. Selon l'environnement, la méthode de saisie sera le clavier du terminal de l'utilisateur. fgets prendra l'entrée jusqu'à ce qu'il reçoive un code EOF ou de saut de ligne, mais gardez à l'esprit qu'il ajoute un caractère nul à la fin.

Notez que MSGSIZE est destiné à la taille maximale de la chaîne affichée et BUFSIZE est destiné à la taille maximale de la chaîne d'octets stockée, y compris les caractères nuls.

Lignes 58-61

Utilisez l'appel système send () pour envoyer des données à l'hôte distant auquel vous vous connectez.

Le premier argument spécifie le descripteur de socket pour lequel la connexion a été établie.

Le deuxième argument est le pointeur contenant le message à envoyer et le troisième argument est la longueur du message. Puisque la fonction strlen est utilisée pour spécifier la longueur du message, les octets de caractères nuls ajoutés par les fgets sont exclus.

Comme pour le recv précédent, 0 est spécifié pour le 4ème argument. C'est le comportement par défaut du blocage du programme jusqu'à ce que le message soit stocké dans le tampon d'envoi du module socket jusqu'à ce que la longueur de message spécifiée soit stockée.

Une situation de blocage possible est lorsque le tampon d'envoi du module socket est plein et que les messages ne peuvent pas être envoyés. Cela se produit car il n'y a pas d'espace libre dans le tampon de réception de l'hôte distant auquel se connecter, de sorte que les données ne peuvent pas être envoyées du tampon d'envoi vers le module inférieur en raison du contrôle de flux basé sur la taille de la fenêtre TCP et le transfert de données est attendu. Faire.

De plus, le tampon d'envoi de la socket est réalisé par la structure de données de la file d'attente, et il peut également être confirmé comme SendQ dans netstat.

Ce qui est important ici, c'est que le succès de send () est qu'il a demandé au système d'exploitation de traiter la transmission et a stocké le message dans le tampon de transmission du module de socket, et il était possible d'envoyer réellement les données à l'hôte distant. Cela ne veut pas dire ça.

Après le processus d'envoi (), divers processus sont exécutés par le module TCP, le module IP et le pilote de périphérique dans le système d'exploitation, et le paquet (chaîne d'octets) est soumis à une conversion physique à partir du matériel tel que la carte d'interface réseau, et un support physique est utilisé. Après cela, il atteindra l'hôte connecté.

Lignes 64 à 64

Définit un entier qui spécifie le nombre d'octets dans le tampon reçu (byteRcvd) et l'index du tableau dans le tampon de réception (byteIndex). Notez que byteIndex correspond au nombre total d'octets reçus dans cette implémentation.

Lignes 65-86

Tant que le nombre total d'octets reçus est dans la taille MSG, recv répète la réception.

J'ai abordé l'appel système recv () la dernière fois, mais le premier argument est le descripteur de la socket où la connexion est établie, le deuxième argument est le pointeur du message à recevoir, le troisième argument est la longueur du message à recevoir et le quatrième argument. Spécifiez 0 pour indiquer le comportement par défaut du comportement de bloc.

Le comportement par défaut consiste à bloquer l'exécution du programme jusqu'à ce qu'une chaîne d'octets jusqu'à la longueur de message spécifiée puisse être extraite du tampon de réception du socket. Le tampon de réception de la socket est réalisé par la structure de données de la file d'attente et peut être confirmé comme RecvQ dans netstat,

Comme send (), recv () est un appel système qui demande au système d'exploitation de transférer une chaîne d'octets du tampon de réception stocké dans le socket vers le processus utilisateur, et est un module de transport de couche inférieure (dans ce cas). Reçoit juste les octets passés par le module TCP) via l'API socket.

Puisque le signal physique qui atteint le matériel tel que la carte d'interface réseau ne reçoit que les données traitées par la pile de protocoles en raison de l'interruption matérielle ou de l'interruption logicielle, recv () effectue le traitement de réception des données. Cela ne veut pas dire que vous êtes là.

En d'autres termes, le processus de réception de paquets lui-même peut ou non exécuter recv (), mais cela se fait constamment, et si le tampon de chaque module est plein ou que les données sont corrompues, elles seront rejetées. ..

Les messages corrects qui n'ont pas été rejetés jusqu'à l'étape de post-traitement du module TCP sont stockés dans le tampon de réception du socket, donc recv () transmet le message au processus utilisateur s'il y a des données.

Revenez au code source.

Cette fois, chaque octet est transféré vers le tableau recvBuffer.

De plus, si la chaîne d'octets reçue de type flux se termine par \ n, le protocole d'application est conçu comme un bloc de données significatives jusqu'à ce point, donc lorsque \ n est reçu, le saut de ligne est écrasé par un caractère nul. La fermeture ou non est déterminée à l'aide de la fonction strcmp.

S'il correspond à quit, nous mettrons fin à l'opération, donc fermez le socket du client et terminez le programme.

Si ce n'est pas le cas, quittez la boucle while et affichez la chaîne reçue avec des sauts de ligne. Puisqu'un caractère nul est ajouté à la fin de la chaîne d'octets, il peut être généré par la fonction de sortie de chaîne de caractères du langage C.

Cette fois, recv () est exécuté octet par octet pour simplifier le programme, mais comme recv () est un appel système, la surcharge de basculement entre le mode noyau et le mode utilisateur du CPU n'est pas un peu encourue.

Par conséquent, il devrait améliorer les performances d'exécution en stockant une chaîne d'octets assez grande dans la mémoire tampon du processus utilisateur avec un recv () et en la traitant un octet à la fois, mais avec cette quantité de données Je ne pense pas que cela changera dans l'expérience humaine.

Comme nous le vérifierons dans le programme serveur suivant, le message envoyé par le serveur est la chaîne de caractères entrée par le client lui-même, donc la même chaîne de caractères qui a été entrée sera sortie vers la sortie standard telle quelle.

Ensuite, regardons le logiciel serveur. Le logiciel serveur est cette fois plus simple que le logiciel client.

Code source du logiciel serveur

tcps.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
#define MSGSIZE 1024
#define BUFSIZE (MSGSIZE + 1)

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
	char recvBuffer[BUFSIZE];//receive temporary buffer
	int recvMsgSize, sendMsgSize; // recieve and send buffer size

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

		while(1) {
			if ((recvMsgSize = recv(clitSock, recvBuffer, BUFSIZE, 0)) < 0) {
				perror("recv() failed.");
				exit(EXIT_FAILURE);
			} else if(recvMsgSize == 0){
				fprintf(stderr, "connection closed by foreign host.\n");
				break;
			}

			if((sendMsgSize = send(clitSock, recvBuffer, recvMsgSize, 0)) < 0){
				perror("send() failed.");
				exit(EXIT_FAILURE);
			} else if(sendMsgSize == 0){
				fprintf(stderr, "connection closed by foreign host.\n");
				break;
			}
		}

		close(clitSock);
	}

	close(servSock);

	return EXIT_SUCCESS;
}

Lignes 61-77

À partir de la 60e ligne, c'est le même que le programme serveur de la dernière fois.

Les fonctions recv () et send () introduites dans le programme client sont telles que décrites ci-dessus. Le programme serveur est mis en œuvre pour renvoyer le message reçu du programme client au client tel quel.

Send () et recv () sont tous deux traités lorsque la valeur de retour est 0, mais ce faisant, même si la connexion est déconnectée involontairement par le logiciel client, accept () sera à nouveau utilisé. Vous pouvez créer un état qui attend une nouvelle connexion et vous pouvez continuer à exécuter le programme serveur.

Le programme serveur actuel ne peut pas répondre aux demandes de plusieurs clients en même temps, il n'est donc pas facile à utiliser en tant que programme serveur.

En fait, si vous exécutez deux ou plusieurs de ces programmes clients, le programme lancé ultérieurement continuera à attendre le traitement lorsque vous entrez des caractères avec fgets et exécutez send (), et le programme client en Une fois terminé, le traitement commence, et ainsi de suite.

Même si les gens arrivent régulièrement, il n'y a qu'un seul guichet automatique, il semble donc que les gens qui arrivent plus tard attendent.

Afin de gérer cet inconvénient, nous présenterons dans le futur ceux étendus au multi-processus, au multi-thread, au même processus, au traitement multiple par thread, etc.

En ce qui concerne la prochaine fois, j'ai vu l'autre jour l'article suivant sur UDP, donc je vérifierai une fois le mécanisme de transmission et de réception des données sur UDP et la différence avec TCP. Protocole QUIC de Google: migrer le Web de TCP vers UDP

Livre de référence

Recommended Posts

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
Introduction à l'API Socket apprise en langage C 2e édition client
Introduction à l'API Socket apprise en langage C, partie 1, édition serveur
Une introduction à l'API de socket moderne pour apprendre en C
Exemple de serveur d'API qui reçoit JSON dans Golang
Introduction à PyQt4 Partie 1
Comment créer un simple script serveur / client TCP
API C en Python 3
[Introduction à cx_Oracle] (13e) Connexion utilisant le pool de connexions (côté client)
Aller à la langue pour voir et se souvenir du langage Partie 7 C en langage GO
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ésumé du chapitre 2 de l'introduction aux modèles de conception appris en langage Java
Présentation de la création d'un socket serveur et de la création d'un socket client
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
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-
Introduction à Ansible Partie 2 'Grammaire de base'
Introduction à Python Hands On Partie 1
Introduction à Protobuf-c (langage C ⇔ Python)
Comment envelopper C en Python
Optimisation apprise avec OR-Tools Part0 [Introduction]
Introduction à Ansible Partie 1 Hello World !! '