2010-12-02 25 views
4

Je suis assez novice avec C et l'écriture d'un serveur TCP, et je me demandais comment gérer les recv() d'un client qui enverrait des commandes auxquelles le serveur répondrait. Par souci de cette question, disons simplement que l'en-tête est le premier octet, l'identifiant de la commande est le deuxième octet, et la longueur de la charge utile est le troisième octet, suivi de la charge utile (le cas échéant).Gestion de plusieurs appels recv() et de tous les scénarios possibles

Quelle est la meilleure façon de recv() ces données? Je pensais appeler recv() pour lire les 3 premiers octets dans le tampon, vérifier que les identifiants d'en-tête et de commande sont valides, puis vérifier la longueur de la charge utile et appeler à nouveau recv() avec la longueur de charge utile arrière du tampon susmentionné. Cependant, il conseille d'utiliser "un tableau assez grand pour deux paquets [longueur max]" pour gérer des situations telles que l'obtention du paquet suivant.

Quelle est la meilleure façon de gérer ces types de recv()? Question fondamentale, mais je voudrais l'implémenter efficacement, en gérant tous les cas qui peuvent survenir. Merci d'avance.

Répondre

5

La méthode Beej fait allusion et AlastairG mentionne, fonctionne comme ceci:

Pour chaque connexion simultanée, vous maintenez un tampon de lecture, mais, non encore traitées les données. (Ceci est le tampon que Beej suggère de dimensionner à deux fois la longueur maximale du paquet). De toute évidence, le tampon commence vide:

unsigned char recv_buffer[BUF_SIZE]; 
size_t recv_len = 0; 

Chaque fois que votre prise est lisible, lu dans l'espace restant dans la mémoire tampon, puis essayez immédiatement et processus ce que vous avez:

result = recv(sock, recv_buffer + recv_len, BUF_SIZE - recv_len, 0); 

if (result > 0) { 
    recv_len += result; 
    process_buffer(recv_buffer, &recv_len); 
} 

Le process_buffer() sera essayez et traitez les données dans le tampon sous la forme d'un paquet. Si le tampon ne contient pas encore un paquet complet, il ne fait que retourner - sinon, il traite les données et les supprime du tampon. Donc, pour vous par exemple le protocole, il ressemblerait à quelque chose comme:

void process_buffer(unsigned char *buffer, size_t *len) 
{ 
    while (*len >= 3) { 
     /* We have at least 3 bytes, so we have the payload length */ 

     unsigned payload_len = buffer[2]; 

     if (*len < 3 + payload_len) { 
      /* Too short - haven't recieved whole payload yet */ 
      break; 
     } 

     /* OK - execute command */ 
     do_command(buffer[0], buffer[1], payload_len, &buffer[3]); 

     /* Now shuffle the remaining data in the buffer back to the start */ 
     *len -= 3 + payload_len; 
     if (*len > 0) 
      memmove(buffer, buffer + 3 + payload_len, *len); 
    } 
} 

(La fonction do_command() vérifierait pour un en-tête valide et octet de commande).

Ce genre de technique finit par être nécessaire, car touterecv() peut revenir à une courte longueur - avec votre méthode proposée, ce qui se passe si votre longueur de charge utile est de 500, mais la prochaine recv() ne vous retourne 400 octets? Vous devrez sauvegarder ces 400 octets jusqu'à la prochaine fois que la socket sera lisible de toute façon. Lorsque vous gérez plusieurs clients concurrents, vous en avez simplement un recv_buffer et recv_len par client, et les placez dans une structure par client (qui contient probablement d'autres choses - comme la socket du client, peut-être leur adresse source, l'état actuel etc.).

+0

+1 pour les lectures courtes! J'ajouterais + s mais je ne suis pas autorisé.Les données ont peut-être été réparties sur une limite de paquet et ne sont pas encore arrivées. Ou le programme peut avoir reçu un signal. –

+0

caf, merci! Le code aide toujours beaucoup. L'explication de Beej, ainsi que celle d'Alastair, est beaucoup plus logique maintenant. Je suis sûr que cela aidera aussi les autres joueurs. Je vais mettre en œuvre quelque chose de très similaire à cela pour mes recv. Une question cependant, sur la ligne: result = recv (chaussette, recv_buffer + recv_len, BUF_SIZE - recv_len, 0); vous n'êtes pas en boucle correcte? Donc par exemple, si BUF_SIZE est 1024, vous essayez de faire un recv de 1024 octets et de traiter tout ce qui vient dans le tampon, même si c'est un octet? Aussi, quel est le hit de performance de memmove sur le buffer si j'ai beaucoup de commandes à traiter? – Jack

+0

@Jack: Droite - la boucle se produit autour de tout ce 'recv(); process_buffer(); 'segment (probablement avec un' select() 'dedans pour bloquer). 'process_buffer();' est appelé même dans le cas où il n'y a qu'un seul octet, parce que je pense qu'il vaut mieux garder tout le smarts pour "est-ce un message complet" dans un seul endroit (dans ce cas, 'process_buffer()' tomberait tout de suite, et vous finiriez dans 'select()', en attendant plus d'octets). – caf

5

Belle question. À quel point voulez-vous aller? Pour une solution tout en chantant toute la danse, utilisez des sockets asynchrones, lisez toutes les données que vous pouvez quand vous le pouvez, et chaque fois que vous obtenez de nouvelles données appelez une fonction de traitement des données sur le tampon.

Ceci vous permet de faire de grandes lectures. Si vous avez beaucoup de commandes en pipeline, vous pouvez potentiellement les traiter sans avoir à attendre de nouveau sur le socket, ce qui augmente les performances et le temps de réponse.

Faites quelque chose de similaire à l'écriture. C'est la fonction de traitement de commande écrit dans un tampon. S'il y a des données dans la mémoire tampon, alors lors de la vérification des sockets (sélectionnez ou interrogez) vérifiez l'écriture et écrivez autant que vous le pouvez, en vous rappelant de ne supprimer que les octets réellement écrits à partir du tampon.

Les tampons circulaires fonctionnent bien dans de telles situations.

Il existe des solutions plus simples et plus légères. Cependant celui-ci est un bon. Rappelez-vous qu'un serveur peut avoir plusieurs connexions et que les paquets peuvent être divisés. Si vous lisez d'une socket dans un tampon seulement pour trouver vous n'avez pas les données pour une commande complète, que faites-vous avec les données que vous avez déjà lu? Où le stockez-vous? Si vous le stockez dans une mémoire tampon associée à cette connexion, vous pourriez tout aussi bien parcourir tout le secteur et simplement lire dans le tampon comme décrit ci-dessus en premier lieu.

Cette solution évite également d'engendrer un thread séparé pour chaque connexion - vous pouvez gérer n'importe quel nombre de connexions sans réel problème. Générer un thread par connexion est un gaspillage inutile de ressources système - sauf dans certaines circonstances où plusieurs threads seraient recommandés de toute façon, et pour cela vous pouvez simplement avoir des threads de travail pour exécuter de telles tâches de blocage tout en gardant la socket socket threaded. Fondamentalement, je suis d'accord avec ce que vous dites Beej dit, mais ne lisez pas les bits tiddly à la fois. Lire de gros morceaux à la fois. L'écriture d'un serveur de socket comme celui-ci, l'apprentissage et la conception au fur et à mesure de mon expérience de socket et de pages de manuel, a été l'un des projets les plus amusants sur lesquels j'ai travaillé.

+0

Merci AlastairG. Essentiellement, j'essaie de le garder aussi simple que possible et de gérer simplement les situations les plus courantes, comme lire dans le paquet suivant ou lire un paquet partiel. J'utilise actuellement select pour surveiller le socket client pour les données entrantes (les commandes dans ce cas) donc je ne pense pas qu'il s'agisse de sockets asynchrones. Est-ce que ma méthode de lecture dans 3 octets, alors la charge utile ne fonctionne pas? La raison pour laquelle je n'ai pas voulu implémenter ce que Beej a dit c'est parce que c'est difficile à comprendre sans code/pseudo-code. Merci pour le conseil si :) – Jack

+0

Pour les écritures/envoie après que je traite les données entrantes, j'allais simplement utiliser une variante de la méthode sendall de Beej puisque je connais la longueur du paquet .. montré ici: http: // beej .us/guide/bgnet/sortie/html/multipage/advanced.html # sendall. J'étais plus préoccupé par les lectures. – Jack

+0

Mon commentaire sur l'écriture était juste pour l'exhaustivité - au cas où quelqu'un d'autre veut savoir comment écrire un serveur de socket vraiment bon. Une chose que je vais dire est que j'ai essayé d'écrire des serveurs de socket qui sont petits et simples mais vous trouvez très rapidement que vous avez juste besoin de le faire comme je le décris. Pas toujours cependant. Lire 3 octets fonctionnera, mais pourquoi le faire de cette façon? – AlastairG

2

La solution décrite par Alastair est la meilleure en termes de performances. FYI - La programmation asynchrone est également connue sous le nom de programmation événementielle. En d'autres termes, vous attendez que les données arrivent sur le socket, les lisent dans un tampon, traitez ce que vous pouvez/quand vous le pouvez et répétez. Votre application peut faire d'autres choses entre lire les données et les traiter.

Un couple plus de liens que je trouve utile de faire quelque chose de très similaire:

La seconde est une grande bibliothèque pour aider à mettre en œuvre tout cela.

En ce qui concerne l'aide d'un tampon et la lecture autant que vous le pouvez, il est une autre chose que de la performance. Les lectures en bloc sont meilleures, moins les appels système (lectures). Vous traitez les données dans le tampon quand vous décidez que vous avez assez à traiter, mais assurez-vous de ne traiter qu'un seul de vos "paquets" (celui que vous avez décrit avec un en-tête de 3 octets) et ne détruisez pas d'autres données dans le tampon .

+0

Merci prêt, cela aide. Quand vous dites "lisez autant que vous le pouvez" pour de meilleures performances, voulez-vous dire simplement faire une recv avec MAX_BUF_SIZE comme longueur? (c'est-à-dire #define MAX_BUF_SIZE 1024 ... unsigned char buf [MAX_BUF_SIZE] ... recv (sockfd, buf, MAX_BUF_SIZE, 0))? Ou voulez-vous dire quelque chose d'autre? Quoi qu'il en soit, je suppose que je le fais dans un mode de programmation axé sur les événements comme décrit Alastair. La raison pour laquelle je voulais lire 3 octets en premier et ensuite le reste de la charge utile est que je connais alors la longueur exacte du paquet à passer en recv sur le second appel. – Jack

+0

@Jack: 1024 pense petit. Sur un réseau gigabit, vous pourriez facilement avoir besoin de mégaoctets de tampons pour une bande passante maximale. –

+0

Compris, pour l'amour de mon programme, cependant, je n'ai probablement pas besoin de quelque chose d'aussi gros. C'est certainement quelque chose que je vais considérer, merci! – Jack

1

Il existe essentiellement deux hypothèses si vous utilisez plusieurs connexions alors la meilleure façon de gérer plusieurs connexions (que ce soit écoute socket, readfd ou writefd) est avec select/poll/epoll. Vous pouvez utiliser l'un ou l'autre selon vos besoins. Sur votre deuxième requête comment gérer plusieurs recv() cette pratique peut être utilisée: chaque fois que les données sont arrivées juste jeter un coup d'oeil sur l'en-tête (il doit être de longueur fixe et le format que vous avez décrit).

buff_header = (char*) malloc(HEADER_LENGTH); 
    count = recv(sock_fd, buff_header, HEADER_LENGTH, MSG_PEEK); 
    /*MSG_PEEK if you want to use the header later other wise you can set it to zero 
     and read the buffer from queue and the logic for the code written below would 
     be changed accordingly*/ 

par ce que vous vous obtenu en-tête et vous pouvez vérifier le paramètre et extraire également toute la longueur msg. Après avoir obtenu la pleine longueur msg juste recevoir la pleine msg

msg_length=payload_length+HEADER_LENGTH; 
    buffer =(char*) malloc(msg_length); 
    while(msg_length) 
    { 
     count = recv(sock_fd, buffer, msg_length, 0); 
     buffer+=count; 
     msg_length-=count; 
    } 

donc de cette façon que vous avez besoin de ne pas prendre de réseau ayant une longueur fixe et vous pouvez facilement mettre en œuvre votre logique.