[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.
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.
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;
}
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.
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.
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é.
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.
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.
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;
}
À 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
Recommended Posts