高TPS场景下TLS连接TCP断包处理失败,求Java底层解决方法
问题
开发一个通过TLS连接向服务器发送交易请求的客户端应用,发送指定字节数数据,接收固定1182字节的响应。提升TPS前一切正常,但提升后部分响应出现断包,客户端单次读取无法完整接收,调用unwrap解密时抛出异常并终止TLS会话:
javax.net.ssl.SSLException: Unrecognized record version (D)TLS-0.0 , plaintext connection? at java.base/sun.security.ssl.SSLEngineInputRecord.bytesInCompletePacket(SSLEngineInputRecord.java:98) at java.base/sun.security.ssl.SSLEngineInputRecord.bytesInCompletePacket(SSLEngineInputRecord.java:64) at java.base/sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:544) at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:441) at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:420) at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:634) at MyClass.handleEncryptedTransaction(MyClass.java:297)
尝试用缓冲区累积断包,但解密前无法查看包内容,只能通过大小判断是否完整,当前实现代码如下:
this.peerNetData.clear(); int bytesRead = socketChannel.read(this.peerNetData); if (bytesRead == DEFAULT_TLS_HANDSHAKE_SIZE) { handleEncryptedTransaction(socketChannel, engine); } else if (bytesRead > 0) { // TLS packet buffering byte[] justRead = new byte[this.peerNetData.position()]; this.peerNetData.flip(); this.peerNetData.get(justRead); this.incompleteTransactionsBuffer.put(justRead); // DEFAULT_TLS_TRANSACTION_SIZE == 1182 if (this.incompleteTransactionsBuffer.position() >= DEFAULT_TLS_TRANSACTION_SIZE) { this.incompleteTransactionsBuffer.flip(); // flipping to read mode while (this.incompleteTransactionsBuffer.remaining() >= DEFAULT_TLS_TRANSACTION_SIZE) { byte[] fullTransaction = new byte[DEFAULT_TLS_TRANSACTION_SIZE]; this.incompleteTransactionsBuffer.get(fullTransaction, 0, fullTransaction.length); this.peerNetData.clear(); this.peerNetData.put(fullTransaction); // This method uses this.peerNetData to unwrap data and handleEncryptedTransaction(socketChannel, engine); } this.incompleteTransactionsBuffer.compact(); // wipe out bytes that had been read and free space of the buffer } }
疑问点:
- 是否有方法判断TLS之上的TCP包是否完整?尝试读取前1182字节无效。
- 当响应包含多个包(如N*1182字节,N为2到7)时代码能正常工作,是否应该等待下一次socket读取获取更多数据?
- 推测问题由高流量导致的包重传引发,Java底层socket连接中是否有其他处理TLS包重传的方法?
解决方案
1. 基于TLS记录格式判断包完整性
TLS记录本身有固定的头部结构(共5字节),可以通过解析头部确定完整的TLS包长度,而非依赖业务层的1182字节:
- 第1字节:内容类型(握手、应用数据等)
- 第2-3字节:TLS版本号
- 第4-5字节:记录长度
具体操作:
- 维护一个累积缓冲区,先读取至少5字节解析头部
- 根据头部的记录长度,继续读取直到缓冲区数据量满足完整TLS记录的要求
- 再将完整的TLS记录传入
unwrap处理
2. 正确处理SSLEngine.unwrap的返回状态
unwrap会返回SSLEngineResult,需根据状态调整操作:
- 若返回
NEED_READ:说明当前缓冲区数据不足以解密,需继续从socket读取数据,再重试unwrap - 若返回
OK:解密成功,可处理应用层数据 - 禁止直接将不完整的TLS数据传入
unwrap,这会直接触发会话终止异常
3. 拆分TLS处理与业务数据处理逻辑
当前代码混淆了TLS记录边界和业务数据边界,需拆分两步:
- 先累积并解析完整的TLS记录,解密得到应用层数据
- 再累积应用层数据,直到拿到完整的1182字节业务响应
4. 关于TCP重传的说明
TCP层的重传由操作系统内核自动处理,Java Socket无需手动干预。你的问题本质是未正确处理TLS记录边界,和TCP重传本身无关。
修正后的核心逻辑示例
// 维护TLS数据累积缓冲区,大小可根据实际调整 private ByteBuffer tlsAccumulator = ByteBuffer.allocate(8192); // 维护业务数据累积缓冲区 private ByteBuffer businessAccumulator = ByteBuffer.allocate(4096); public void processSocketData(SocketChannel socketChannel, SSLEngine engine) throws IOException { // 读取socket数据到TLS累积缓冲区 int bytesRead = socketChannel.read(tlsAccumulator); if (bytesRead == -1) { // 连接关闭,处理收尾逻辑 return; } tlsAccumulator.flip(); // 循环处理累积的TLS数据 while (tlsAccumulator.remaining() >= 5) { // 标记当前位置,解析头部后可回滚 tlsAccumulator.mark(); // 解析TLS头部 byte contentType = tlsAccumulator.get(); short version = tlsAccumulator.getShort(); short recordLength = tlsAccumulator.getShort(); // 检查是否有足够的完整TLS记录数据 if (tlsAccumulator.remaining() < recordLength) { tlsAccumulator.reset(); break; } // 提取完整的TLS记录 ByteBuffer completeTlsRecord = ByteBuffer.allocate(5 + recordLength); tlsAccumulator.reset(); tlsAccumulator.get(completeTlsRecord.array(), 0, 5 + recordLength); completeTlsRecord.flip(); // 准备解密缓冲区 ByteBuffer appData = ByteBuffer.allocate(engine.getApplicationBufferSize()); SSLEngineResult result = engine.unwrap(completeTlsRecord, appData); appData.flip(); switch (result.getStatus()) { case OK: // 处理解密后的应用层数据 processBusinessData(appData); break; case NEED_READ: // 需要更多数据,退出循环等待下一次读取 tlsAccumulator.compact(); return; default: // 处理其他状态(如CLOSED、BUFFER_OVERFLOW等) break; } } // 压缩缓冲区,保留未处理的数据 tlsAccumulator.compact(); } private void processBusinessData(ByteBuffer appData) { businessAccumulator.put(appData); businessAccumulator.flip(); // 处理完整的1182字节业务响应 while (businessAccumulator.remaining() >= 1182) { byte[] fullResponse = new byte[1182]; businessAccumulator.get(fullResponse); handleEncryptedTransaction(fullResponse); } businessAccumulator.compact(); }
内容的提问来源于stack exchange,提问作者leandro.marques
相关产品推荐
相关产品推荐

