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

如何在TCP通信中正确读取长度未知且以分隔符标记结束的数据?

基于分隔符的非阻塞TCP/SSL读取实现

针对你需要处理分隔符标记结束的不定长数据(如HTTP头)的需求,完全可以在非阻塞模式下实现高效读取,无需逐字节读取,也不用硬编码超时。核心思路是维护内部缓冲区缓存未处理数据,每次尽可能多读取数据后再查找分隔符,配合事件监听(如select/poll/epoll)来等待可读事件,而非硬编码超时。

实现步骤与代码示例

1. 调整套接字为非阻塞模式

首先需要将SSL关联的套接字设置为非阻塞,这样SSL_read不会阻塞等待数据:

#include <fcntl.h>

// 在初始化SSL连接后调用
void TcpClient::SetNonBlocking() {
    int fd = SSL_get_fd(ssl);
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

2. 新增内部缓冲区缓存未处理数据

在TcpClient类中添加成员变量,用于存储尚未找到分隔符的剩余数据:

class TcpClient {
private:
    SSL* ssl;
    std::vector<unsigned char> m_recv_buf; // 内部缓存,存储未处理的剩余数据
    // ... 其他成员
};

3. 实现基于分隔符的非阻塞读取函数

这个函数会先检查内部缓冲区是否已有完整的分隔符数据,若没有则尝试读取更多数据,非阻塞下若需要等待则返回标记,由外部事件循环处理等待:

#include <algorithm>

// out_buf:输出找到的分隔符前的数据(不含分隔符)
// delimiter:分隔符(如HTTP头的"\r\n\r\n")
// 返回值:>0表示成功读取的数据长度;-1表示连接错误/关闭;-2表示需要等待套接字可读(非阻塞下无数据)
int TcpClient::RecvUntilDelimiter(std::vector<unsigned char>& out_buf, const std::vector<unsigned char>& delimiter) {
    out_buf.clear();

    // 先检查内部缓存中是否存在分隔符
    auto it = std::search(m_recv_buf.begin(), m_recv_buf.end(), delimiter.begin(), delimiter.end());
    if (it != m_recv_buf.end()) {
        // 提取分隔符前的数据到out_buf
        out_buf.assign(m_recv_buf.begin(), it);
        // 移除已处理的数据(包括分隔符)
        m_recv_buf.erase(m_recv_buf.begin(), it + delimiter.size());
        return out_buf.size();
    }

    // 内部缓存没有分隔符,尝试读取更多数据
    const size_t READ_BUF_SIZE = 4096; // 每次读取的缓冲区大小,可根据场景调整
    std::vector<unsigned char> read_buf(READ_BUF_SIZE);

    while (true) {
        int nRecv = SSL_read(ssl, read_buf.data(), READ_BUF_SIZE);
        if (nRecv > 0) {
            // 将读取到的数据追加到内部缓存
            m_recv_buf.insert(m_recv_buf.end(), read_buf.begin(), read_buf.begin() + nRecv);
            // 再次检查分隔符
            it = std::search(m_recv_buf.begin(), m_recv_buf.end(), delimiter.begin(), delimiter.end());
            if (it != m_recv_buf.end()) {
                out_buf.assign(m_recv_buf.begin(), it);
                m_recv_buf.erase(m_recv_buf.begin(), it + delimiter.size());
                return out_buf.size();
            }
        } else {
            // 处理SSL_read的错误
            int ssl_err = SSL_get_error(ssl, nRecv);
            if (ssl_err == SSL_ERROR_WANT_READ) {
                // 非阻塞模式下无数据可读,需要等待套接字可读
                return -2;
            } else {
                // 连接关闭或其他错误
                return -1;
            }
        }
    }
}

4. 配合事件循环使用

在外部事件循环中,通过select/poll/epoll监听套接字的可读事件,当事件触发时再调用RecvUntilDelimiter,无需硬编码超时:

// 示例:用select监听可读事件
fd_set read_fds;
FD_ZERO(&read_fds);
int fd = SSL_get_fd(ssl);
FD_SET(fd, &read_fds);

// 这里的超时可以由外部灵活设置,而非硬编码在读取函数中
struct timeval timeout;
timeout.tv_sec = 5; // 示例超时5秒,可根据业务调整
timeout.tv_usec = 0;

int ret = select(fd + 1, &read_fds, nullptr, nullptr, &timeout);
if (ret > 0 && FD_ISSET(fd, &read_fds)) {
    std::vector<unsigned char> out_buf;
    std::vector<unsigned char> delimiter = {'\r', '\n', '\r', '\n'}; // HTTP头结束分隔符
    int recv_ret = client.RecvUntilDelimiter(out_buf, delimiter);
    if (recv_ret > 0) {
        // 处理读取到的HTTP头数据
    } else if (recv_ret == -1) {
        // 处理连接错误
    }
} else if (ret == 0) {
    // 超时处理
} else {
    // select错误
}

关键优势

  • 高效读取:每次读取尽可能多的数据(4096字节),避免逐字节读取的低效问题。
  • 非阻塞无硬编码超时:超时逻辑由外部事件循环控制,读取函数本身不固定超时时间,灵活性更高。
  • 数据完整性:内部缓冲区自动缓存未处理的剩余数据,不会因为TCP拆包导致数据丢失。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 12:43:16