Question Quelle est la manière correcte de lire depuis 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 C et C ++ comme vous pouvez le constater. Le BUFFER_SIZE est 256 - devrais-je simplement augmenter la taille? Si oui, à quoi? 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 - alors si vous pouviez suggérer une meilleure façon, veuillez également le faire.


19
2018-03-20 15:21


origine


Réponses:


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 ne comporte qu'un entier de 4 octets indiquant la longueur du reste de votre message. Ensuite, faites simplement ce qui 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;
    }
}

Puis 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 tailles sont les mêmes sur l'expéditeur et le destinataire.
  • Endianess est la même à la fois sur 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 avant.

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, par exemple:

// 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 pour:

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.


29
2018-03-20 16:01



Plusieurs pointeurs:

Vous devez gérer une valeur de retour de 0, ce 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 pointé par 'buffer'. Qui, j'ai remarqué, vous n'allouez pas n'importe où dans cet extrait de code.

Quelqu'un d'autre a bien expliqué comment votre tampon n'est pas une chaîne C terminée par la valeur NULL si votre read () remplit tout le tampon. C'est effectivement un problème et un problème 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.

Si vous êtes inquiet de vous retrouver dans une boucle infinie lorsque l'hôte distant vous envoie un message mal formé (attaque potentielle par déni de service), vous devez utiliser select () avec un délai d'expiration sur le socket pour vérifier la lisibilité les données sont disponibles et renflouer 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 prévoyez de recevoir, la manière dont vous analysez le message en entier à plusieurs reprises pour la "fin"; le jeton est très inefficace. Ceci 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
2018-03-20 15:29



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 de terminaison zéro dont vous dépendez lors de l'extraction vers un flux de chaînes. Vous avez besoin:

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

3
2018-03-20 16:04



1) D’autres (spécialement dirkgently) ont noté qu’il fallait allouer de la mémoire à la mémoire tampon. Pour les petites valeurs de N (par exemple, N <= 4096), vous pouvez également l’allouer sur la pile:

#define BUFFER_SIZE 4096
char buffer[BUFFER_SIZE]

Cela vous évite le souci de vous assurer que delete[] le tampon devrait être une exception.

Mais rappelez-vous que les piles sont taille finie (il en est de même pour les tas, mais les piles sont plus fines), vous ne voulez donc pas en mettre trop.

2) Sur un code retour de -1, vous ne devez 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'un court devoir. . Par exemple, EAGAIN peut être renvoyé dans errno si aucune donnée n'est actuellement disponible sur un socket non bloquant. Regardez la page de manuel pour lire (2).


3
2018-03-21 03:38



Où allouez-vous la mémoire pour votre buffer? La ligne où vous invoquez 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
2018-03-20 15:27



C'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 d'autres liens utiles en bas pour plus d'informations sur les sockets.


1
2018-03-20 16:17