C/C++ で TCP ソケットから読み取る正しい方法は何ですか?

完全なアプリケーションを知らなければ、問題にアプローチする最善の方法を言うのは困難ですが、一般的な手法は、メッセージの残りの長さを示す固定長フィールドで始まるヘッダーを使用することです.

ヘッダーが、メッセージの残りの長さを示す 4 バイトの整数のみで構成されていると仮定します。次に、次の手順を実行します。

// 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;
    }
}

その後、コード内で

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;

これにはいくつかの仮定があります:

  • int は送信側と受信側で同じサイズです。
  • エンディアンネスは、送信側と受信側の両方で同じです。
  • 両側でプロトコルを制御できます
  • メッセージを送信するときに、前もって長さを計算できます。

ネットワーク経由で送信する整数のサイズを明示的に知りたいことがよくあるため、それらをヘッダー ファイルで定義し、次のように明示的に使用します。

// 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;

これにより、上記が次のように変更されます:

UInt32 length = 0;
char* buffer = 0;

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

// process

delete [] buffer;

これがお役に立てば幸いです。


いくつかのポインタ:

リモート ホストがソケットを閉じたことを示す 0 の戻り値を処理する必要があります。

非ブロッキング ソケットの場合は、エラーの戻り値 (-1) をチェックし、errno が予期される EINPROGRESS でないことを確認する必要もあります。

エラー処理を改善する必要があることは間違いありません。「buffer」が指すバッファをリークしている可能性があります。気がついたのですが、このコード スニペットのどこにも割り当てていません。

read() がバッファ全体を埋める場合、バッファがヌルで終了するC文字列ではないことについて、他の誰かが良い点を指摘しました。これは確かに問題であり、深刻な問題です。

バッファ サイズは少し小さいですが、256 バイトを超えて読み取ろうとしない限り、または割り当てたものを読み取らない限り、機能するはずです。

リモートホストが不正な形式のメッセージ (潜在的なサービス拒否攻撃) を送信したときに無限ループに陥ることが心配な場合は、select() をソケットでタイムアウトを指定して使用し、可読性を確認してください。

このようなものがあなたのために働くかもしれません:

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.
}

受信すると予想されるデータの量に応じて、メッセージ全体を繰り返しスキャンして「最後」を探す方法。トークンは非常に非効率的です。これは、ステート マシン ('e'->'n'->'d'->';' の状態) を使用して実行することをお勧めします。これにより、着信する各文字を 1 回だけ確認できます。

そして真剣に、これらすべてを行うライブラリを見つけることを検討する必要があります。正しく理解するのは簡単ではありません。


dirks の提案に従って実際にバッファを作成する場合:

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

バッファが完全にいっぱいになる可能性があり、文字列ストリームに抽出するときに依存する終端のゼロ文字を上書きする可能性があります。必要なもの:

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