You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

客户端发送的C++数据未正确到达服务器问题求助

问题分析与修复方案

我来帮你拆解一下这个问题——核心矛盾在于服务器用了非阻塞套接字,但你的所有收发函数都是按照阻塞套接字的逻辑写的,再加上TCP流协议本身没有消息边界的特性,直接导致了首次登录的偶发失败,以及二次登录时的ProtocolException。

具体问题出在哪?

1. 非阻塞套接字的EAGAIN/EWOULDBLOCK错误没处理

服务器的套接字是非阻塞模式,当暂时没有数据可读时,read会返回-1,同时errno被设为EAGAIN或EWOULDBLOCK——这不是真正的错误,只是告诉你"现在没数据,稍后再试"。但你的代码把所有read <=0的情况都当成致命错误抛出,这会在数据还没准备好时就误判为协议错误,首次登录的偶发失败大概率就是这么来的。

2. 没处理TCP的流特性,默认一次读全数据

TCP是面向流的,read调用不一定能一次性拿到你想要的所有字节——比如你要读8字节的sz,可能只读到了3字节。你的代码完全没考虑这种情况,直接用不完整的sz去分配内存、读取字符串,结果自然是垃圾数据,二次登录时残留的不完整数据会直接触发异常。

3. 收发函数的写入逻辑同样有问题

不管是sendFlag还是sendString里的write调用,都没考虑非阻塞模式下缓冲区满导致的部分写入,也没处理EAGAIN,这可能导致数据发送不完整,客户端那边也会读错。

怎么修复?

我们需要重构收发函数,实现循环读写+错误码区分,确保不管是阻塞还是非阻塞模式,都能完整读取/写入指定字节数,同时正确处理非阻塞的等待情况。

第一步:封装通用的完整读写函数

先写两个工具函数,专门处理"必须读满/写满指定字节数"的场景,同时兼容阻塞和非阻塞模式:

#include <errno.h>
#include <unistd.h>

// 完整读取指定字节数,返回实际读取的字节数,-1表示真正的错误
ssize_t readAll(int fd, void* buf, size_t count) {
    size_t bytesRead = 0;
    char* ptr = static_cast<char*>(buf);
    while (bytesRead < count) {
        ssize_t res = read(fd, ptr + bytesRead, count - bytesRead);
        if (res == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下暂时无数据,返回已读的字节数
                return bytesRead;
            } else {
                // 真正的错误,比如连接中断
                return -1;
            }
        } else if (res == 0) {
            // 对方关闭了连接
            return bytesRead;
        }
        bytesRead += res;
    }
    // 成功读满所有字节
    return bytesRead;
}

// 完整写入指定字节数,返回实际写入的字节数,-1表示真正的错误
ssize_t writeAll(int fd, const void* buf, size_t count) {
    size_t bytesWritten = 0;
    const char* ptr = static_cast<const char*>(buf);
    while (bytesWritten < count) {
        ssize_t res = write(fd, ptr + bytesWritten, count - bytesWritten);
        if (res == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下缓冲区满,返回已写的字节数
                return bytesWritten;
            } else {
                return -1;
            }
        }
        bytesWritten += res;
    }
    return bytesWritten;
}

第二步:修改原有的收发函数

用上面的工具函数替换原来直接调用read/write的逻辑,同时完善错误判断:

修改receiveString

string receiveString(int fd) {
    unsigned long sz = 0;
    ssize_t readVal = readAll(fd, &sz, sizeof(sz));
    if (readVal != sizeof(sz)) {
        if (readVal == 0) {
            cout << "Connection closed while reading string size" << endl;
        } else {
            cout << "Incomplete string size read: got " << readVal << " bytes" << endl;
        }
        throw ProtocolException("Read string size trouble");
    }

    char *buf = new char[sz];
    buf[sz - 1] = '\0'; // 确保字符串结尾
    readVal = readAll(fd, buf, sz);
    if (readVal != sz) {
        cout << "Incomplete string read: got " << readVal << " bytes" << endl;
        delete[] buf;
        throw ProtocolException("Read chars trouble");
    }

    string res(buf);
    delete[] buf;
    return res;
}

修改receiveFlag

MessageType receiveFlag(int fd) {
    MessageType flag;
    ssize_t readVal = readAll(fd, &flag, sizeof(flag));
    if (readVal != sizeof(flag)) {
        cout << "Failed to read complete flag" << endl;
        throw ProtocolException("Read flag trouble");
    }
    return flag;
}

修改sendString

void sendString(int fd, string str) {
    unsigned long sz = str.size() + 1;
    ssize_t res = writeAll(fd, &sz, sizeof(sz));
    if (res != sizeof(sz)) {
        throw ProtocolException("SENDSTRING SEND SIZE TROUBLE");
    }
    res = writeAll(fd, str.c_str(), sz);
    if (res != sz) {
        throw ProtocolException("SENDSTRING SEND STRING TROUBLE");
    }
}

修改sendFlag

void sendFlag(int fd, MessageType flag) {
    ssize_t res = writeAll(fd, &flag, sizeof(flag));
    if (res != sizeof(flag)) {
        throw ProtocolException("SEND FLAG TROUBLE");
    }
}

第三步:服务器的poll循环要适配状态机

因为服务器是非阻塞的,你不能指望一次poll就能完成整个登录流程(比如读取flag、用户名、密码)。需要给每个客户端维护一个状态机,记录当前正在处理的步骤,比如:

enum ClientState {
    WAITING_FOR_CMD_FLAG,
    WAITING_FOR_LOGIN_USERNAME,
    WAITING_FOR_LOGIN_PASSWORD
};

struct ClientSession {
    int fd;
    ClientState state;
    // 用于保存未读完的部分数据
    unsigned long pendingSize;
    std::string pendingBuffer;
};

当readAll没有读满预期的字节数时(比如返回了小于sizeof(sz)的正数,说明是EAGAIN),就把当前状态和已读的数据保存下来,下次poll检测到这个fd有POLLIN事件时,再继续读取剩余的数据,而不是直接抛出异常。

这样就能避免因为非阻塞模式下的暂时无数据而误判为错误,同时保证协议数据的完整性。

额外提醒

  • 客户端是阻塞套接字,修改后的函数完全兼容,因为阻塞模式下read/write会一直等待直到数据就绪,不会返回EAGAIN。
  • 一定要检查errno来区分EAGAIN和真正的错误(比如ECONNRESET、EBADF),不要一看到-1就抛出异常。
  • 二次登录的异常本质是首次登录时残留了不完整的协议数据,修复完整读写逻辑后,这种情况就不会再发生了。

内容的提问来源于stack exchange,提问作者Dead Neo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:52:14