web-dev-qa-db-fra.com

Quelle est la bonne façon de lire à partir d'un socket TCP en C / C ++?

Voici mon code:

// Not all headers are relevant to the code snippet.
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <cstdlib>
#include <cstring>
#include <unistd.h>

char *buffer;
stringstream readStream;
bool readData = true;

while (readData)
{
    cout << "Receiving chunk... ";

    // Read a bit at a time, eventually "end" string will be received.
    bzero(buffer, BUFFER_SIZE);
    int readResult = read(socketFileDescriptor, buffer, BUFFER_SIZE);
    if (readResult < 0)
    {
        THROW_VIMRID_EX("Could not read from socket.");
    }

    // Concatenate the received data to the existing data.
    readStream << buffer;

    // Continue reading while end is not found.
    readData = readStream.str().find("end;") == string::npos;

    cout << "Done (length: " << readStream.str().length() << ")" << endl;
}

C'est un peu de C et C++ comme vous pouvez le voir. Le BUFFER_SIZE est 256 - dois-je simplement augmenter la taille? Si oui, que faire? Est-ce que ça importe?

Je sais que si "end" n'est pas reçu pour quelque raison que ce soit, ce sera une boucle sans fin, ce qui est mauvais - donc si vous pouvez suggérer une meilleure façon, veuillez également le faire.

23
Nick Bolton

Sans connaître votre application complète, il est difficile de dire quelle est la meilleure façon d'aborder le problème, mais une technique courante consiste à utiliser un en-tête qui commence par un champ de longueur fixe, qui indique la longueur du reste de votre message.

Supposons que votre en-tête se compose uniquement d'un entier de 4 octets qui indique la longueur du reste de votre message. Ensuite, procédez simplement comme suit.

// This assumes buffer is at least x bytes long,
// and that the socket is blocking.
void ReadXBytes(int socket, unsigned int x, void* buffer)
{
    int bytesRead = 0;
    int result;
    while (bytesRead < x)
    {
        result = read(socket, buffer + bytesRead, x - bytesRead);
        if (result < 1 )
        {
            // Throw your error.
        }

        bytesRead += result;
    }
}

Plus tard dans le code

unsigned int length = 0;
char* buffer = 0;
// we assume that sizeof(length) will return 4 here.
ReadXBytes(socketFileDescriptor, sizeof(length), (void*)(&length));
buffer = new char[length];
ReadXBytes(socketFileDescriptor, length, (void*)buffer);

// Then process the data as needed.

delete [] buffer;

Cela fait quelques hypothèses:

  • les pouces sont de la même taille sur l'expéditeur et le récepteur.
  • L'endianisme est le même pour l'expéditeur et le destinataire.
  • Vous avez le contrôle du protocole des deux côtés
  • Lorsque vous envoyez un message, vous pouvez calculer la longueur à l'avance.

Comme il est courant de vouloir connaître explicitement la taille de l'entier que vous envoyez sur le réseau, définissez-les dans un fichier d'en-tête et utilisez-les explicitement, comme:

// These typedefs will vary across different platforms
// such as linux, win32, OS/X etc, but the idea
// is that a Int8 is always 8 bits, and a UInt32 is always
// 32 bits regardless of the platform you are on.
// These vary from compiler to compiler, so you have to 
// look them up in the compiler documentation.
typedef char Int8;
typedef short int Int16;
typedef int Int32;

typedef unsigned char UInt8;
typedef unsigned short int UInt16;
typedef unsigned int UInt32;

Cela changerait ce qui précède en:

UInt32 length = 0;
char* buffer = 0;

ReadXBytes(socketFileDescriptor, sizeof(length), (void*)(&length));
buffer = new char[length];
ReadXBytes(socketFileDescriptor, length, (void*)buffer);

// process

delete [] buffer;

J'espère que ça aide.

32
grieve

Plusieurs pointeurs:

Vous devez gérer une valeur de retour de 0, qui vous indique que l'hôte distant a fermé le socket.

Pour les sockets non bloquants, vous devez également vérifier une valeur de retour d'erreur (-1) et vous assurer que errno n'est pas EINPROGRESS, ce qui est attendu.

Vous avez certainement besoin d'une meilleure gestion des erreurs - vous risquez de perdre le tampon indiqué par "buffer". Ce que, j'ai remarqué, vous n'allouez nulle part dans cet extrait de code.

Quelqu'un d'autre a fait un bon point sur la façon dont votre tampon n'est pas une chaîne C terminée par null si votre read () remplit tout le tampon. C'est effectivement un problème, et grave.

Votre taille de mémoire tampon est un peu petite, mais devrait fonctionner tant que vous n'essayez pas de lire plus de 256 octets, ou tout ce que vous allouez pour cela.

Si vous avez peur de vous lancer dans une boucle infinie lorsque l'hôte distant vous envoie un message mal formé (une attaque potentielle par déni de service), vous devez utiliser select () avec un délai d'attente sur le socket pour vérifier la lisibilité, et ne lire que si les données sont disponibles et renflouent si select () expire.

Quelque chose comme ça pourrait fonctionner pour vous:

fd_set read_set;
struct timeval timeout;

timeout.tv_sec = 60; // Time out after a minute
timeout.tv_usec = 0;

FD_ZERO(&read_set);
FD_SET(socketFileDescriptor, &read_set);

int r=select(socketFileDescriptor+1, &read_set, NULL, NULL, &timeout);

if( r<0 ) {
    // Handle the error
}

if( r==0 ) {
    // Timeout - handle that. You could try waiting again, close the socket...
}

if( r>0 ) {
    // The socket is ready for reading - call read() on it.
}

Selon le volume de données que vous vous attendez à recevoir, la façon dont vous scannez le message entier à plusieurs reprises pour la "fin"; le jeton est très inefficace. Cela est mieux fait avec une machine à états (les états étant 'e' -> 'n' -> 'd' -> ';') afin que vous ne regardiez qu'une seule fois chaque caractère entrant.

Et sérieusement, vous devriez envisager de trouver une bibliothèque pour faire tout cela pour vous. Ce n'est pas facile de bien faire les choses.

9
Ori Pessach

1) D'autres (en particulier dirkgently) ont noté que le tampon doit être alloué un peu d'espace mémoire. Pour les petites valeurs de N (disons, N <= 4096), vous pouvez également l'allouer sur la pile:

#define BUFFER_SIZE 4096
char buffer[BUFFER_SIZE]

Cela vous évite d'avoir à vous assurer que vous delete[] le tampon si une exception est levée.

Mais rappelez-vous que les piles sont de taille finie (les tas aussi, mais les piles sont plus finies), donc vous ne voulez pas en mettre trop.

2) Sur un code de retour -1, vous ne devriez pas simplement retourner immédiatement (lancer une exception immédiatement est encore plus sommaire.) Il y a certaines conditions normales que vous devez gérer, si votre code doit être autre chose qu'une courte affectation aux devoirs . Par exemple, EAGAIN peut être retourné dans errno si aucune donnée n'est actuellement disponible sur un socket non bloquant. Jetez un œil à la page de manuel pour lire (2).

3
Dan Breslau

Si vous créez réellement le tampon selon la suggestion de dirks, alors:

  int readResult = read(socketFileDescriptor, buffer, BUFFER_SIZE);

peut remplir complètement le tampon, écrasant éventuellement le caractère zéro de fin dont vous dépendez lors de l'extraction dans un flux de chaînes. Vous avez besoin:

  int readResult = read(socketFileDescriptor, buffer, BUFFER_SIZE - 1 );
3
anon

Où allouez-vous de la mémoire pour votre buffer? La ligne où vous appelez bzero invoque un comportement indéfini car le tampon ne pointe vers aucune région de mémoire valide.

char *buffer = new char[ BUFFER_SIZE ];
// do processing

// don't forget to release
delete[] buffer;
1
dirkgently

Ceci est un article auquel je me réfère toujours lorsque je travaille avec des sockets ..

LE MONDE DE SELECT ()

Il vous montrera comment utiliser de manière fiable 'select ()' et contient quelques autres liens utiles en bas pour plus d'informations sur les sockets.

1
Arnold Spence

Pour ajouter quelque chose à plusieurs des articles ci-dessus:

read () - au moins sur mon système - renvoie ssize_t. C'est comme size_t, sauf est signé. Sur mon système, c'est un long, pas un int. Vous pouvez obtenir des avertissements du compilateur si vous utilisez int, selon votre système, votre compilateur et les avertissements que vous avez activés.

0
Joseph Larson