在C / C ++中从TCP套接字读取的正确方法是什么?

时间:2009-03-20 15:21:31

标签: c++ c tcp

这是我的代码:

// 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和C ++。 BUFFER_SIZE是256 - 我应该增加大小吗?如果是这样,该怎么办?这有关系吗?

我知道如果因为某种原因没有收到“结束”,这将是一个无限循环,这是不好的 - 所以如果你能提出更好的方法,也请这样做。

8 个答案:

答案 0 :(得分:32)

在不知道您的完整应用程序的情况下,很难说解决问题的最佳方法是什么,但一种常见的技术是使用以固定长度字段开头的标头,表示消息其余部分的长度

假设您的标头仅包含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;

这做了一些假设:

  • 整数与发送方和接收方的大小相同。
  • 发送方和接收方的Endianess相同。
  • 您可以控制双方的协议
  • 发送消息时,您可以预先计算长度。

由于通常希望明确知道您通过网络发送的整数的大小,因此在头文件中定义它们并明确使用它们,例如:

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

我希望这会有所帮助。

答案 1 :(得分:9)

几点:

您需要处理返回值0,它告诉您远程主机关闭了套接字。

对于非阻塞套接字,您还需要检查错误返回值(-1)并确保errno不是EINPROGRESS,这是预期的。

您肯定需要更好的错误处理 - 您可能会泄漏“缓冲区”指向的缓冲区。我注意到,您没有在此代码段中分配任何内容。

如果read()填充整个缓冲区,其他人就如何缓冲区不是空终止的C字符串提出了一个很好的观点。这确实是一个问题,也是一个严重问题。

您的缓冲区大小有点小,但只要您不尝试读取超过256个字节或任何为其分配的内容,它就应该有效。

如果您担心在远程主机向您发送格式错误的消息(可能的拒绝服务攻击)时进入无限循环,那么您应该在套接字上使用带有超时的select()来检查可读性,并且只读数据是否可用,如果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' - &gt;'n' - &gt;'d' - &gt;';')可以做得更好,这样您只需查看每个传入的字符一次。

严肃地说,你应该考虑找一个图书馆为你做这一切。要做到这一点并不容易。

答案 2 :(得分:3)

如果按照dirks建议实际创建缓冲区,则:

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

可能会完全填充缓冲区,可能会覆盖提取到字符串流时依赖的终止零字符。你需要:

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

答案 3 :(得分:3)

1)其他人(尤其是dirkgently)已经注意到缓冲区需要分配一些内存空间。对于小的N值(例如,N <= 4096),您也可以在堆栈上分配它:

#define BUFFER_SIZE 4096
char buffer[BUFFER_SIZE]

这样可以省去确保您delete[]缓冲区应该抛出异常的担忧。

但请记住堆栈的大小是有限的(堆也是堆栈,但堆栈是finiter),所以你不想在那里放太多。

2)在-1返回代码上,你不应该只是立即返回(立即抛出异常更加粗略。)如果你的代码不仅仅是一个,那么你需要处理某些正常情况。简短的家庭作业。例如,如果非阻塞套接字上当前没有数据,则可以在errno中返回EAGAIN。看一下手册(2)。

答案 4 :(得分:1)

您在哪里为buffer分配内存?您调用bzero的行调用未定义的行为,因为缓冲区未指向任何有效的内存区域。

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

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

答案 5 :(得分:1)

这是我在使用套接字时总是提到的文章。

THE WORLD OF SELECT()

它将向您展示如何可靠地使用'select()'并在底部包含一些其他有用的链接,以获取有关套接字的更多信息。

答案 6 :(得分:0)

只需添加以上几个帖子中的内容:

read()-至少在我的系统上-返回ssize_t。就像size_t一样,除了带符号的。在我的系统上,它很长,而不是整数。如果使用int,则可能会收到编译器警告,具体取决于系统,编译器以及打开的警告。

答案 7 :(得分:0)

对于任何非平凡的应用程序(即应用程序必须接收并处理具有不同长度的不同类型的消息),针对您的特定问题的解决方案不一定只是编程解决方案-这是一个约定,即I.E.协议。

为了确定应传递给read调用的字节数,应建立应用程序接收的公共前缀或标头。这样,当套接字首次具有可用的读取功能时,您可以对预期的内容做出决定。

一个二进制示例可能看起来像这样:

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

enum MessageType {
    MESSAGE_FOO,
    MESSAGE_BAR,
};

struct MessageHeader {
    uint32_t type;
    uint32_t length;
};

/**
 * Attempts to continue reading a `socket` until `bytes` number
 * of bytes are read. Returns truthy on success, falsy on failure.
 *
 * Similar to @grieve's ReadXBytes.
 */
int readExpected(int socket, void *destination, size_t bytes)
{
    /*
    * Can't increment a void pointer, as incrementing
    * is done by the width of the pointed-to type -
    * and void doesn't have a width
    *
    * You can in GCC but it's not very portable
    */
    char *destinationBytes = destination;
    while (bytes) {
        ssize_t readBytes = read(socket, destinationBytes, bytes);
        if (readBytes < 1)
            return 0;
        destinationBytes += readBytes;
        bytes -= readBytes;
    }
    return 1;
}

int main(int argc, char **argv)
{
    int selectedFd;

    // use `select` or `poll` to wait on sockets
    // received a message on `selectedFd`, start reading

    char *fooMessage;
    struct {
        uint32_t a;
        uint32_t b;
    } barMessage;

    struct MessageHeader received;
    if (!readExpected (selectedFd, &received, sizeof(received))) {
        // handle error
    }
    // handle network/host byte order differences maybe
    received.type = ntohl(received.type);
    received.length = ntohl(received.length);

    switch (received.type) {
        case MESSAGE_FOO:
            // "foo" sends an ASCII string or something
            fooMessage = calloc(received.length + 1, 1);
            if (readExpected (selectedFd, fooMessage, received.length))
                puts(fooMessage);
            free(fooMessage);
            break;
        case MESSAGE_BAR:
            // "bar" sends a message of a fixed size
            if (readExpected (selectedFd, &barMessage, sizeof(barMessage))) {
                barMessage.a = ntohl(barMessage.a);
                barMessage.b = ntohl(barMessage.b);
                printf("a + b = %d\n", barMessage.a + barMessage.b);
            }
            break;
        default:
            puts("Malformed type received");
            // kick the client out probably
    }
}

您可能已经看到使用二进制格式的一个缺点-对于每个大于char的属性,您必须确保使用ntohl或{{ 1}}函数。

一种替代方法是使用字节编码的消息,例如简单的ASCII或UTF-8字符串,它们可以完全避免字节顺序问题,但需要额外的精力来解析和验证。

在C中,网络数据有两个最终考虑因素。

首先是某些C类型的宽度不固定。例如,谦虚的ntohs定义为处理器的字长,因此32位处理器将产生32位int,而64位处理器将产生64位int。好的可移植代码应使网络数据使用固定宽度类型,例如int中定义的类型。

第二个是结构填充。具有不同宽度成员的结构将在某些成员之间添加数据以保持内存对齐,从而使该结构在程序中更快地使用,但有时会产生令人困惑的结果。

stdint.h

在此示例中,其实际宽度将不是1 #include <stdio.h> #include <stdint.h> int main() { struct A { char a; uint32_t b; } A; printf("sizeof(A): %ld\n", sizeof(A)); } + 4 char = 5个字节,而是8:

uint32_t

这是因为在mharrison@mharrison-KATANA:~$ gcc -o padding padding.c mharrison@mharrison-KATANA:~$ ./padding sizeof(A): 8 之后添加了3个字节,以确保char a与内存对齐。

因此,如果您uint32_t bwrite,然后尝试读取另一边的struct Achar,则会得到uint32_t,一个uint32_t,其中前三个字节是垃圾,最后一个字节是您编写的实际整数的第一个字节。

要么将您的数据格式显式记录为C结构类型,要么更好地记录它们可能包含的任何填充字节。