如何在C++的TLS连接中对Socket进行多次连续读取?
解决TLS连接中读取Server Cert的问题
你的核心问题是误解了TCP流和TLS握手消息的传输方式——TCP是无边界的字节流,不需要“清空”Socket,而是要按照TLS协议的结构来解析数据,而不是用ioctl(FIONREAD)读取所有可用字节。
为什么你的代码会出问题
第一次读取Server Hello后,Socket缓冲区里很可能已经包含了后续的Server Cert(甚至其他握手消息),你的第二个ioctl会直接读取这些混合的数据,导致无法区分独立的握手消息。而且read可能返回部分数据,你当前的代码没有处理这种情况,会导致数据不完整。
正确的处理方式
TLS协议通过记录层和握手层的长度字段来划分消息边界,步骤如下:
- 先读取5字节的TLS记录头部,从中获取记录的类型、版本和长度。
- 根据记录长度读取完整的记录内容。
- 在记录内容中解析握手消息:每个握手消息都有1字节类型、3字节长度,根据长度读取对应消息内容。
代码示例
首先实现一个辅助函数,确保读取到指定长度的字节(处理read返回部分数据的情况):
#include <vector> #include <cstdint> #include <arpa/inet.h> // 用于ntohs/ntohl ssize_t read_all(int sockfd, void* buf, size_t len) { size_t total_read = 0; uint8_t* ptr = static_cast<uint8_t*>(buf); while (total_read < len) { ssize_t n = read(sockfd, ptr + total_read, len - total_read); if (n < 0) { // 读取错误 return -1; } else if (n == 0) { // 连接已关闭 return total_read; } total_read += n; } return total_read; }
然后按TLS协议解析握手消息:
// 读取TLS记录头部 uint8_t record_header[5]; if (read_all(sockfd, record_header, 5) != 5) { throw std::runtime_error("Failed to read TLS record header"); } uint8_t content_type = record_header[0]; uint16_t record_len = ntohs(*reinterpret_cast<uint16_t*>(&record_header[3])); // 验证是否为握手消息(TLS握手消息的content_type是22) if (content_type != 22) { throw std::runtime_error("Received non-handshake TLS record"); } // 读取完整的记录内容 std::vector<uint8_t> record_content(record_len); if (read_all(sockfd, record_content.data(), record_len) != record_len) { throw std::runtime_error("Failed to read TLS record content"); } // 解析记录中的握手消息 size_t offset = 0; while (offset < record_len) { uint8_t handshake_type = record_content[offset]; // 握手消息长度是3字节,需要转成主机字节序 uint32_t handshake_len = ntohl(*reinterpret_cast<uint32_t*>(&record_content[offset+1])) & 0x00FFFFFF; offset += 4; if (handshake_type == 2) { // Server Hello类型 std::vector<uint8_t> server_hello(record_content.begin() + offset, record_content.begin() + offset + handshake_len); // 在这里处理Server Hello数据 offset += handshake_len; } else if (handshake_type == 11) { // Certificate类型 std::vector<uint8_t> server_cert(record_content.begin() + offset, record_content.begin() + offset + handshake_len); // 在这里处理Server Cert数据 offset += handshake_len; } // 可以继续扩展处理其他握手消息,比如Server Hello Done(类型14)等 }
关键要点
- 永远不要依赖
FIONREAD来划分应用层消息边界,TCP流没有天然的消息分隔符,必须靠协议本身的长度字段。 - 必须处理
read返回部分数据的情况,用循环读取确保拿到完整的协议数据。 - TLS握手消息可能被打包在同一个TLS记录里,也可能分多个记录传输,需要循环解析直到处理完所有需要的消息。
内容的提问来源于stack exchange,提问作者Jark
相关产品推荐
相关产品推荐

