Ceci est la 4ème introduction à l'API socket pour apprendre en langage C, que j'ai personnellement transformé en une série. Cette fois, nous allons vérifier le mécanisme de transmission / réception des données pour UDP et la différence avec TCP.
Avec les mouvements suivants concernant l'UDP, on peut dire que c'est un protocole de communication qui retient l'attention. Protocole QUIC de Google: migrer le Web de TCP vers UDP
UDP est une extension d'IP, qui est un protocole d'hôte à hôte, au niveau de l'application. UDP utilise un numéro de port pour l'adressage, et contrairement à TCP, il conserve les limites de chaque message.
De plus, UDP rejette le datagramme tel qu'il est lorsque les données sont corrompues, mais contrairement à TCP, c'est un protocole pour fournir le service de datagramme de type best effort, et il est fiable comme le traitement de retransmission de données. N'effectue aucun traitement connexe.
À première vue, il semble être inférieur à TCP, mais il y a beaucoup de bons points comme un fonctionnement plus rapide et une réception des messages plus facile que TCP. L'article ci-dessus aborde également les caractéristiques et la qualité d'UDP, c'est donc une bonne idée de le lire.
Et contrairement à TCP, UDP est un protocole sans connexion qui peut être utilisé sans établir de connexion. Ceci est également montré dans le code source ci-dessous.
Logiciel client Mac OS X 10.10.5 gcc CentOs 6.8 gcc du logiciel serveur x86_64
Je présenterai à la fois le logiciel serveur et le logiciel client, mais comme le logiciel serveur est plus simple, je le présenterai en premier.
udps.c
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), bind(), sendto(), recvfrom()
#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 MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int main(int argc, char* argv[]) {
const char *address = "";
unsigned short port = (unsigned short)atoi(argv[1]);
struct sockaddr servSockAddr, clitSockAddr;
char recvBuffer[MAX_BUFSIZE];
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
if (bind(server_sock, &servSockAddr, sizeof(servSockAddr)) < 0) {
perror("bind() failed.");
exit(EXIT_FAILURE);
}
while(1) {
int recvMsgSize = udp_receive(server_sock, recvBuffer, MAX_BUFSIZE, &clitSockAddr);
if (recvMsgSize == MSG_FAILURE) continue;
printf("message received from %s.\n", inet_ntoa(((struct sockaddr_in *)&clitSockAddr)->sin_addr));
int sendMsgSize = udp_send(server_sock, recvBuffer, recvMsgSize, &clitSockAddr);
if (sendMsgSize == MSG_FAILURE) continue;
}
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
Le processus de création de socket et le processus d'initialisation de la structure d'adresse sont séparés en fonctions appelées respectivement get_socket () et sockaddr_init ().
Quant à sockaddr_init (), il sépare simplement le processus de définition d'adresse précédent de la routine principale.
Dans get_socket (), nous utilisons l'appel système socket () comme nous l'avons fait avec TCP. La différence avec TCP est que le deuxième argument spécifie SOCK_DGRAM pour créer une socket de type datagramme, et le troisième argument spécifie IPPROTO_UDP, qui est un protocole de bout en bout de type datagramme. Encore une fois, ne spécifiez pas 0 et spécifiez-le.
Comme avec TCP, l'appel système bind () est utilisé pour lier l'adresse au socket, mais contrairement à TCP, les messages sont envoyés et reçus tels quels sans appeler listen () ou accept ().
Dans cet état, comme dans l'exemple, si vous vérifiez avec netstat
, vous pouvez voir que ʻudp 0 0 0.0.0.0:8080 0.0.0.0: *` est affiché.
En TCP, afin d'établir une connexion pour chaque client, un nouveau descripteur de socket est acquis par le traitement de accept () et les messages sont envoyés et reçus, mais dans le cas d'UDP, puisque la connexion n'est pas établie, bind () est effectué. Envoyez et recevez des messages en utilisant le socket lui-même.
De plus, comme pour le client, nous n'utilisons normalement pas l'appel système connect (). Je ne l'utilise pas normalement car il peut être utilisé.
À partir du nom connect (), vous pourriez penser qu'il s'agit d'un appel système TCP uniquement qui effectue le traitement pour établir une connexion, mais puisque connect () joue un rôle dans la détermination de l'adresse de la destination de la connexion lors de l'envoi et de la réception de messages, Il peut également être utilisé pour UDP.
Bien sûr, dans le cas de TCP, le paquet de la demande de connexion SYN est effectivement envoyé à l'autre serveur, de sorte que le processus de détermination de l'adresse de la destination de connexion et le processus d'établissement de la connexion sont effectués, mais dans le cas d'UDP, Les paquets pour de telles connexions de connexion ne seront pas envoyés, et la communication utilisant ce socket sera limitée à la communication avec le socket associé au numéro de port spécifique de l'hôte avec une adresse IP spécifique.
Il est séparé en une fonction appelée udp_receive (), mais c'est le processus de réception d'un message du client avec l'appel système recvfrom ().
Dans ce cas, comme recv (), il bloque l'exécution du programme jusqu'à ce que le datagramme arrive. Le comportement par défaut est celui décrit lors de l'appel système recv ().
L'argument est presque le même que l'appel système recv (), mais comme les informations du client qui est la source ne sont obtenues que lorsque le datagramme est reçu, le pointeur vers la structure sockaddr qui stocke les informations source est utilisé comme argument dans le cinquième argument. Passer au.
Ensuite, je passe un pointeur qui spécifie la longueur au 6ème argument, mais en le passant, la taille de la structure sockaddr est stockée, et lorsqu'elle revient du contrôle de recvfrom (), la taille réellement stockée Notez que est stocké.
En ce qui concerne le processus de réception vers le processus utilisateur, les données sont transférées de la file d'attente de réception du module socket de la même manière que TCP, mais comme les limites sont maintenues et les messages sont regroupés un par un, il se trompe. Vous ne recevrez pas plusieurs messages.
De plus, avec recvfrom (), si une charge utile supérieure à la taille de tampon de réception spécifiée est envoyée, les données de chaîne d'octets après la taille spécifiée seront supprimées.
Par conséquent, dans UDP, il est nécessaire de rendre la taille du tampon de réception sécurisé par recevfrom () suffisamment grande à l'avance en fonction des exigences de l'application. Vous avez besoin d'au moins une taille supérieure à la taille envoyée par sendto ().
À propos, la taille maximale de la charge utile du datagramme UDP est de 65507 (65535 (longueur de paquet IP) -20 (longueur d'en-tête IP) -8 (longueur d'en-tête UDP)) octets en raison de la restriction du protocole IP inférieur. Cependant, lorsqu'il est envoyé dans le réseau, le datagramme IP sera divisé et circulera à travers le réseau en raison de la restriction de la taille de la charge utile transmissible de la liaison de données (1500 octets dans le cas de MTU * Ethernet). ..
Lors de la copie d'un message dans le tampon de réception, si aucune erreur ne se produit, le traitement séquentiel se poursuit tel quel.
Affiche l'adresse IP source à partir des informations source stockées dans clitSockAddr.
Il est séparé en une fonction appelée udp_send (), mais l'appel système sendto () envoie un message du client.
Depuis recvfrom (), sendto () a une spécification similaire à l'appel système similaire send (), et est censé transmettre en plus les informations d'adresse de l'hôte de destination. Les informations d'adresse de l'hôte de destination utilisent la structure obtenue par recvfrom ().
Et contrairement à TCP, il ne renvoie pas en raison d'une erreur, il n'est donc pas nécessaire de conserver les données à envoyer dans le tampon. Par conséquent, lorsque le contrôle est renvoyé de sendto (), les données ont déjà été transmises au module inférieur.
En outre, la valeur maximale du tampon d'envoi UDP est souvent déterminée par le système, donc si vous souhaitez envoyer un tampon volumineux, vous devez utiliser un appel système distinct pour modifier les paramètres de socket. Je présenterai à nouveau cet appel système.
Vous n'avez pas besoin d'envoyer et de recevoir des données à plusieurs reprises jusqu'à ce que la connexion soit fermée, comme dans TCP, et que les messages de datagramme soient limités, donc sendto () et recvfrom () seront tous deux envoyés une fois s'ils ne sont pas supprimés. Il est possible d'obtenir le message correspondant en l'exécutant un par un. Cependant, la commande n'est pas garantie.
En passant, si vous utilisez connect (), vous pouvez utiliser les appels système send () et recv () comme vous l'avez fait avec TCP. Bien entendu, le principe de transmission / réception est différent de TCP.
Et comme UDP est un service du meilleur effort, il n'est pas toujours possible de recevoir des messages. Par conséquent, le code client suivant ajoute un processus de retransmission simple au niveau de l'application.
udpc.c
#include <stdio.h> //printf(), fprintf(), perror(), getc()
#include <sys/socket.h> //socket(), sendto(), recvfrom()
#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()
#include <signal.h> //sigatcion()
#include <errno.h> //erron, EINTR
#define MSG_FAILURE -1
#define MAX_MSGSIZE 1024
#define MAX_BUFSIZE (MAX_MSGSIZE + 1)
#define MAX_TRIES 5
#define TIMEOUT_SECOND 2
int get_socket(const char *);
void sockaddr_init (const char *, unsigned short, struct sockaddr *);
int udp_send(int, const char *, int, struct sockaddr *);
int udp_receive(int, char *, int, struct sockaddr *);
void socket_close(int);
int input(char *, int);
void remove_lf(char *, int);
void sigaction_init(struct sigaction *, void (*)(int));
void catchAlarm(int);
int udp_try_receive (int, struct sockaddr *, struct sockaddr *, char *, int, char *);
int check_correct_server (struct sockaddr *, struct sockaddr *);
int intTries = 0;
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
const char *address = argv[1];
unsigned short port = (unsigned short)atoi(argv[2]);
struct sockaddr servSockAddr, clitSockAddr;
struct sigaction action;
int server_sock = get_socket("udp");
sockaddr_init(address, port, &servSockAddr);
sigaction_init(&action, catchAlarm);
if (sigaction(SIGALRM, &action, NULL) < 0) {
perror("sigaction() failure");
exit(EXIT_FAILURE);
}
while(1){
char sendBuffer[MAX_BUFSIZE];
char receiveBuffer[MAX_BUFSIZE];
int inputSize = input(sendBuffer, MAX_BUFSIZE);
if (strcmp(sendBuffer, "quit\n") == 0) {
socket_close(server_sock);
break;
}
int receivedSize = udp_try_receive(server_sock, &servSockAddr, &clitSockAddr, sendBuffer, inputSize, receiveBuffer);
if (check_correct_server(&servSockAddr, &clitSockAddr) == -1) {
continue;
}
remove_lf(receiveBuffer, receivedSize);
printf("server return: %s\n", receiveBuffer);
}
return EXIT_SUCCESS;
}
int get_socket(const char *type) {
int sock;
if (strcmp(type, "udp") == 0) {
sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
} else if(strcmp(type, "tcp") == 0) {
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
}
if (sock < 0){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
return sock;
}
void sockaddr_init (const char *address, unsigned short port, struct sockaddr *sockaddr) {
struct sockaddr_in sockaddr_in;
sockaddr_in.sin_family = AF_INET;
if (inet_aton(address, &sockaddr_in.sin_addr) == 0) {
if (strcmp(address, "") == 0 ) {
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);
} else {
fprintf(stderr, "Invalid IP Address.\n");
exit(EXIT_FAILURE);
}
}
if (port == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
sockaddr_in.sin_port = htons(port);
*sockaddr = *((struct sockaddr *)&sockaddr_in);
}
int udp_send(int sock, const char *data, int size, struct sockaddr *sockaddr) {
int sendSize;
sendSize = sendto(sock, data, size, 0, sockaddr, sizeof(*sockaddr));
if (sendSize != size) {
perror("sendto() failed.");
return MSG_FAILURE;
}
return sendSize;
}
int udp_receive(int sock, char *buffer, int size, struct sockaddr *sockaddr) {
unsigned int sockaddrLen = sizeof(*sockaddr);
int receivedSize = recvfrom(sock, buffer, MAX_BUFSIZE, 0, sockaddr, &sockaddrLen);
if (receivedSize < 0) {
perror("recvfrom() failed.");
return MSG_FAILURE;
}
return receivedSize;
}
void socket_close(int server) {
if (close(server) < 0) {
perror("close() failed.");
exit(EXIT_FAILURE);
}
}
int input(char *buffer, int size) {
printf("please enter the characters:");
if (fgets(buffer, size, stdin) == NULL){
fprintf(stderr, "invalid input string.\n");
exit(EXIT_FAILURE);
}
//flush the stdin buffer
if (buffer[strlen(buffer)-1] != '\n') {
int c;
while((c = getc(stdin) != '\n') && (c != EOF)){}
}
return strlen(buffer);
}
void remove_lf(char *buffer, int bufferSize) {
buffer[bufferSize-1] = '\0';
}
void catchAlarm (int ignored) {
intTries += 1;
}
void sigaction_init(struct sigaction *action, void (*handler)(int) ) {
action->sa_handler = handler;
if (sigfillset(&(action->sa_mask)) < 0) {
perror("sigfillset() failure");
exit(EXIT_FAILURE);
}
action->sa_flags = 0;
}
int udp_try_receive (int sock, struct sockaddr *servSockAddr, struct sockaddr *clitSockAddr, char *sendBuffer, int sendSize, char *receiveBuffer) {
int sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
int receivedSize;
while (1) {
alarm(TIMEOUT_SECOND);
receivedSize = udp_receive(sock, receiveBuffer, MAX_BUFSIZE, clitSockAddr);
if (receivedSize == MSG_FAILURE) {
if (errno == EINTR) {
if (intTries <= MAX_TRIES) {
printf("timed out %d.\n", intTries);
sendedSize = udp_send(sock, sendBuffer, sendSize, servSockAddr);
if (sendedSize == MSG_FAILURE) break;
alarm(TIMEOUT_SECOND);
continue;
} else {
printf("total timed out %d.\n", MAX_TRIES);
exit(EXIT_FAILURE);
}
} else {
exit(EXIT_FAILURE);
}
}
break;
}
alarm(0);
return receivedSize;
}
int check_correct_server (struct sockaddr *sockaddr_1, struct sockaddr *sockaddr_2) {
if( ((struct sockaddr_in *)sockaddr_1)->sin_addr.s_addr != ((struct sockaddr_in *)sockaddr_2)->sin_addr.s_addr ) {
fprintf(stderr, "reveiceid from unknown server.\n");
} else if (ntohs(((struct sockaddr_in *)sockaddr_1)->sin_port) != ntohs(((struct sockaddr_in *)sockaddr_2)->sin_port)) {
fprintf(stderr, "reveiceid from unknown port.\n");
} else {
return EXIT_SUCCESS;
}
return MSG_FAILURE;
}
Comme pour le serveur, créez un socket et initialisez la structure d'adresse. Vous n'avez généralement pas besoin d'appeler bind () pour les mêmes raisons que pour les clients TCP.
C'est l'un des points de ce programme UDP, et nous nous préparons à détecter le timeout de transmission UDP par le signal. Le traitement est séparé en une fonction appelée sigaction_init (), mais le contenu du traitement consiste à créer une structure de sigaction.
Passez un pointeur vers cette structure à l'appel système sigaction () pour modifier l'action par défaut lorsque SIGALRM est notifié au processus. Si ce processus de changement est omis et que le processus par défaut est exécuté, dans le cas de ce programme, le programme se terminera au moment où SIGALRM est notifié au processus.
Pour expliquer brièvement ce qui se passe lorsque SIGALRM est notifié à un processus par le système d'exploitation, la fonction catchAlarm () est exécutée et bloque d'autres signaux pendant ce temps. En fait, pendant que la fonction catchAlarm () est en cours d'exécution, appuyer sur «Ctrl + C» (SIGINT), par exemple, ne termine pas le processus.
Bien que les signaux soient étroitement liés à la programmation réseau, je voudrais publier un autre article sur les signaux eux-mêmes.
J'ai écrit sur sendto () et recvfrom () dans le cas du client plus tôt, mais la fonction udp_try_receive () sur la 60e ligne est une série de sous-programmes pour l'envoi et la réception de traitement qui détecte le traitement du délai d'attente.
Tout d'abord, utilisez l'appel système alarm () pour que le système d'exploitation envoie SIGALRM après 2 secondes. Si recvfrom () ne peut pas recevoir le message, il attendra que le programme soit traité par défaut, mais cette fois, SIGALRM est envoyé au processus après environ 2 secondes et renvoie -1 ce qui signifie une erreur. Si vous souhaitez utiliser l'API socket en combinant des signaux, il semble nécessaire d'approfondir votre compréhension de la série de processus.
Et comme EINTR est défini comme errno, ce qui signifie un code d'erreur, le traitement est effectué sur la base de cette condition, et le traitement de retransmission est effectué jusqu'à ce que MAX_TRIES soit atteint. Nous ne garantissons pas la fiabilité, mais l'application fait quelque chose comme le traitement de retransmission TCP.
Si cela ne fonctionne pas, le programme sera arrêté une fois, en supposant que quelque chose ne va pas. Cela empêchera le programme d'être bloqué à l'infini.
Si vous pouvez recevoir le message sans attendre, spécifiez 0 pour l'argument alarm () pour désactiver l'alarme.
Nous avons ajouté une routine pour vérifier si la source de la fonction check_correct_server () du message est correcte.
Cela garantit que le client répond à partir de l'hôte et de l'application qui ont envoyé le paquet. Il est hautement improbable que les paquets proviennent d'autres hôtes, mais ils doivent être inclus aussi longtemps que cela est pratiquement possible.
Puisque la fin du message doit être un saut de ligne, nous avons ajouté un processus pour convertir le saut de ligne en un caractère nul.
La prochaine fois, j'aimerais examiner une méthode qui combine le traitement multi-processus en plus des sockets TCP, mais comme le concept de multi-processus devient plus important que la programmation de socket, multi-processus Jetons un coup d'œil au processus.
Recommended Posts