成功接收后清空缓冲区

Empty buffer after successful recv

我正在用 C++ 在 Windows 上编写一个服务器,我在使用 recv() 时遇到了一个奇怪的行为。

我写了这个函数:

bool readN(SOCKET s, int size, char* buffer){
    fd_set readset;
    struct timeval tv;
    int left, res;
    FD_ZERO(&readset);
    FD_SET(s, &readset);
    left = size;
    std::cout << "-----called readN to read " << size << " byte" << std::endl;
    while (left > 0) {
        tv.tv_sec = MAXWAIT;
        tv.tv_usec = 0;
        res = select(0, &readset, NULL, NULL, &tv);
        if (res > 0) {
            res = recv(s, buffer, left, 0);
            if (res == 0) {//connection closed by client
                return false;
            }

            left -= res;
            std::cout << "\treceived " << res << " left " << left << std::endl;
            if (left != 0) {
                buffer += res;
            }

        }
        else if (res == 0) { //timer expired
            return false;
        }
        else { //socket error
            return false;
        }
    }
    std::cout << "\t" << buffer << std::endl;
    return true;
}

我这样称呼它:

std::unique_ptr<char[]> buffer = std::make_unique<char[]>(size_);
if (readN(sck, size_, buffer.get())) {
    std::cout << "----read message----" << std::endl;
    std::cout <<"\t"<< buffer.get()<< std::endl;
}

问题是即使recv()returns是正数,缓冲区还是空的。我错过了什么?

readN()的末尾有

std::cout << "\t" << buffer << std::endl;

问题是缓冲区现在指向 buffer + size 相对于 buffer 的原始值。该值已被

修改
buffer += res;

这应该输出缓冲区,

std::cout << "\t" << (buffer - size) << std::endl;

在用下面的 main() 试验 readN() 之后,如果套接字不是无效句柄(text/binary 由 ncat 发送的数据),似乎 readN() 可以工作。如果套接字是一个无效的句柄,函数returns很快。

#include <iostream>
#include <memory>
#include <string.h>

#ifdef _WIN64
#include <ws2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#endif

#include <errno.h>

#define MAXWAIT 5000

bool readN(SOCKET fd, int size, char *buffer)
{
    fd_set readset;
    struct timeval tv;
    int left, res;

    FD_ZERO(&readset);
    FD_SET(fd, &readset);

    left = size;
    std::cout << "-----called readN to read " << size << " byte" << std::endl;
    while (left > 0) {
        tv.tv_sec = MAXWAIT;
        tv.tv_usec = 0;
        res = select(fd + 1, &readset, NULL, NULL, &tv);

        if (res > 0) {
            res = recv(fd, buffer, left, 0);
            if (res == 0) { //connection closed by client
                return false;
            }

            left -= res;
            std::cout << "\treceived " << res << " left " << left << std::endl;
            buffer += res;
        } else if (res == 0) {  //timer expired
            std::cout << "\ttimer expired" << std::endl;
            return false;
        } else {        //socket error
            std::cout << "\tsocket error " << WSAGetLastError()  << std::endl;
            return false;
        }
    }
    std::cout << "Print the buffer now\n" << (buffer - size) << std::endl;
    return true;
}

int main(void)
{
    int err;
    SOCKET cfd = 0;
    SOCKET afd = 0;

    struct sockaddr_in addr;
    socklen_t clen;
    struct sockaddr_in caddr;

#ifdef _WIN64
    WORD ver = 0x202;
    WSADATA wsa_data;

    memset(&wsa_data, 0, sizeof(wsa_data));
    std::cout << "WSAStartup" << std::endl;
    err = WSAStartup(ver, &wsa_data);
    if (err < 0) goto error_exit;
#endif

    memset(&addr, 0, sizeof(addr));
    memset(&caddr, 0, sizeof(caddr));

    std::cout << "socket" << std::endl;
    afd = socket(AF_INET, SOCK_STREAM, 0);
    if (afd < 0) goto error_exit;

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(1234);

    std::cout << "bind" << std::endl;
    err = bind(afd, (struct sockaddr *)&addr, sizeof(addr));
    if (err < 0) goto error_exit;

    std::cout << "listen" << std::endl;
    listen(afd, 5);

    clen = sizeof(caddr);
    std::cout << "accept" << std::endl;
    cfd = accept(afd, (struct sockaddr *) &caddr, &clen);
    if (cfd == INVALID_SOCKET) goto error_exit;

    {
        int size_ = 1024;
        std::unique_ptr<char[]> buffer2 = std::make_unique<char[]>(size_);

        std::cout << "readN" << std::endl;
        if (readN(cfd, 1024, buffer2.get())) {
            std::cout << "----read message----" << std::endl;
            std::cout <<"\t"<< buffer2.get() << std::endl;
        }
    }
    return 0;
error_exit:
    std::cout << "Error!" << std::endl;
    std::cout << "\tsocket error " << WSAGetLastError()  << std::endl;
    return 1;
}

我发现您的代码中存在一些问题。

  1. 您并不是在每次调用 select() 时都重置 readset 变量。 select() 修改变量。对于单插槽情况,这还算不错,但您应该养成每次都重置变量的习惯。

  2. 您没有检查 recv() 返回的错误。您假设任何非正常断开都是成功的,但这并不总是正确的。

  3. readN()的末尾返回true之前,你输出的是buffer参数给std::cout,但是buffer将指向数据的 END,而不是 BEGINNING,因为它是由读取循环推进的。这可能是您对 "empty buffer" 感到困惑的来源。 readN() 本身根本不应该输出数据,因为你在 readN() 退出后才这样做,否则你最终会得到冗余的输出消息。

  4. 如果 readN() returns 为真,您将使用期望 null 的 operator<< 将最终的 buffer 传递给 std::cout已终止 char 字符串,但不保证您的缓冲区以 null 终止。

尝试更像这样的东西:

bool readN(SOCKET s, int size, char* buffer){
    fd_set readset;
    struct timeval tv;
    int res;
    std::cout << "-----called readN to read " << size << " byte(s)" << std::endl;
    while (size > 0) {
        FD_ZERO(&readset);
        FD_SET(s, &readset);
        tv.tv_sec = MAXWAIT;
        tv.tv_usec = 0;

        res = select(0, &readset, NULL, NULL, &tv);
        if (res > 0) {
            res = recv(s, buffer, size, 0);
            if (res == SOCKET_ERROR) {
                res = WSAGetLastError();
                if (res == WSAEWOULDBLOCK) {
                    continue; //call select() again
                }
                return false; //socket error
            }

            if (res == 0) {
                return false;  //connection closed by client
            }

            buffer += res;
            size -= res;

            std::cout << "\treceived " << res << " byte(s), " << size << " left" << std::endl;
        }

        /*
        else if (res == 0) {
            return false; //timer expired
        }
        else {
            return false; //socket error
        }
        */

        else {
            return false; //timer expired or socket error
        }
    }

    return true;
}

std::unique_ptr<char[]> buffer = std::make_unique<char[]>(size_);
if (readN(sck, size_, buffer.get())) {
    std::cout << "----read message----" << std::endl;
    std::cout << "\t";
    std::cout.write(buffer.get(), size_);
    std::cout << std::endl;
}

话虽如此,我建议 readN() 的替代实现,具体取决于您使用的是阻塞套接字还是非阻塞套接字。

如果阻塞,请使用 setsockopt(SO_RCVTIMEO) 而不是 select()。如果 recv() 因超时而失败,WSAGetLastError() 将报告 WSAETIMEDOUT:

sck = socket(...);

DWORD timeout = MAXWAIT * 1000;
setsockopt(sck, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));

bool readN(SOCKET s, int size, char* buffer){
    int res;
    std::cout << "-----called readN to read " << size << " byte(s)" << std::endl;
    while (size > 0) {
        res = recv(s, buffer, size, 0);
        if (res == SOCKET_ERROR) {
            /*
            res = WSAGetLastError();
            if (res == WSAETIMEDOUT) {
                return false; //timer expired
            }
            else {
                return false; //socket error
            }
            */
            return false; //timer expired or socket error
        }

        if (res == 0) {
            return false; //connection closed by client
        }

        buffer += res;
        size -= res;

        std::cout << "\treceived " << res << " byte(s), " << size << " left" << std::endl;
    }

    return true;
}

如果是非阻塞,请不要调用 select() 除非 recv() 要求您调用它:

bool readN(SOCKET s, int size, char* buffer){
    fd_set readset;
    struct timeval tv;
    int res;
    std::cout << "-----called readN to read " << size << " byte(s)" << std::endl;
    while (size > 0) {
        res = recv(s, buffer, size, 0);
        if (res == SOCKET_ERROR) {
            res = WSAGetLastError();
            if (res != WSAEWOULDBLOCK) {
                return false; //socket error
            }

            FD_ZERO(&readset);
            FD_SET(s, &readset);
            tv.tv_sec = MAXWAIT;
            tv.tv_usec = 0;

            res = select(0, &readset, NULL, NULL, &tv);
            if (res > 0) {
                continue; //call recv() again
            }

            /*
            else if (res == 0) {
                return false; //timer expired
            }
            else {
                return false; //socket error
            }
            */

            return false; //timer expired or socket error
        }

        if (res == 0) {
            return false; //connection closed by client
        }

        buffer += res;
        size -= res;

        std::cout << "\treceived " << res << " byte(s), " << size << " left" << std::endl;
    }

    return true;
}