Android NIO读取大数据时SocketChannel报BUFFER_UNDERFLOW异常求助
问题分析与解决方案
你的问题核心在于当SSLEngine返回BUFFER_UNDERFLOW时,没有正确触发从SocketChannel读取更多数据的逻辑。当处理大于16KB的数据时,单次读取的网络数据不足以完成TLS包的解析,此时需要继续从socket读取补充数据,但你的代码在handleBufferUnderflow后仍停留在unwrap循环里,导致缓冲区数据不足,触发了TLS头解析失败的异常。
修复后的读取方法
下面是修正后的read方法,关键调整了BUFFER_UNDERFLOW的处理逻辑,确保触发再次读取socket数据:
protected ByteBuffer read(SocketChannel socketChannel, SSLEngine engine) throws Exception { // 初始化缓冲区(如果未初始化) if (mPeerAppData == null) { mPeerAppData = ByteBuffer.allocate(engine.getSession().getApplicationBufferSize()); } if (mPeerNetData == null) { mPeerNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); } boolean exitReadLoop = false; mPeerAppData.clear(); while (!exitReadLoop) { // 从Socket读取数据到网络缓冲区 int bytesRead = socketChannel.read(mPeerNetData); if (bytesRead > 0) { mPeerNetData.flip(); // 切换为读模式 boolean needMoreData = false; while (mPeerNetData.hasRemaining() && !needMoreData) { SSLEngineResult result = engine.unwrap(mPeerNetData, mPeerAppData); switch (result.getStatus()) { case OK: // 处理剩余未解析的网络数据 if (!mPeerNetData.hasRemaining()) { mPeerNetData.clear(); } else { mPeerNetData.compact(); // 保留未处理的数据,避免丢失 } // 当应用缓冲区有数据时标记可以退出 if (mPeerAppData.position() > 0) { exitReadLoop = true; } break; case BUFFER_OVERFLOW: // 扩容应用缓冲区 mPeerAppData = enlargeApplicationBuffer(engine, mPeerAppData); break; case BUFFER_UNDERFLOW: // 网络数据不足,需要读取更多数据 mPeerNetData = handleBufferUnderflow(engine, mPeerNetData); needMoreData = true; // 跳出unwrap循环,回到Socket读取步骤 break; case CLOSED: closeConnection(socketChannel, engine); return null; default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } } } else if (bytesRead < 0) { handleEndOfStream(socketChannel, engine); return null; } // 未获取到足够数据时短暂等待后重试 if (!exitReadLoop) { Thread.sleep(50); } } mPeerAppData.flip(); // 切换为读模式返回给调用方 return mPeerAppData; }
关键修改说明
BUFFER_UNDERFLOW处理:设置needMoreData = true,跳出unwrap循环,回到外层逻辑重新读取Socket数据,补充到网络缓冲区。- 缓冲区安全管理:使用
compact()保留网络缓冲区中未处理的TLS片段,避免数据丢失。 - 循环条件优化:不再在第一次
OK时强制退出,确保处理完所有可用的网络数据,只有当应用缓冲区有有效数据时才标记退出。
Conscrypt PSK TLS 完整通信示例
下面是一个基于Conscrypt的PSK TLS客户端完整示例,涵盖SSLContext初始化、SSLEngine配置和标准读写循环:
import org.conscrypt.Conscrypt; import javax.net.ssl.*; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.net.InetSocketAddress; import java.security.Security; public class ConscryptPskTlsClient { private static final String PSK_IDENTITY = "my_psk_identity"; private static final byte[] PSK_KEY = "my_pre_shared_key_1234".getBytes(); static { // 注册Conscrypt作为优先安全提供者 Security.insertProviderAt(Conscrypt.newProvider(), 1); } public static void main(String[] args) throws Exception { // 自定义PSK密钥管理器 PSKKeyManager pskKeyManager = new PSKKeyManager() { @Override public byte[] getKey(String identityHint, String identity) { return PSK_IDENTITY.equals(identity) ? PSK_KEY : null; } @Override public String getIdentity(String identityHint) { return PSK_IDENTITY; } @Override public String[] getSupportedIdentityHints() { return new String[]{PSK_IDENTITY}; } }; // 初始化SSLContext SSLContext sslContext = SSLContext.getInstance("TLSv1.3", "Conscrypt"); sslContext.init(new KeyManager[]{pskKeyManager}, null, null); // 连接服务器 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8443)); socketChannel.configureBlocking(false); // 配置SSLEngine SSLEngine engine = sslContext.createSSLEngine(); engine.setUseClientMode(true); engine.beginHandshake(); // 完成TLS握手 doHandshake(socketChannel, engine); // 读取服务器响应 ByteBuffer response = read(socketChannel, engine); if (response != null) { byte[] data = new byte[response.remaining()]; response.get(data); System.out.println("Received response: " + new String(data)); } // 关闭连接 closeConnection(socketChannel, engine); } private static void doHandshake(SocketChannel socketChannel, SSLEngine engine) throws Exception { ByteBuffer netBuffer = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); ByteBuffer appBuffer = ByteBuffer.allocate(engine.getSession().getApplicationBufferSize()); SSLEngineResult.HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { switch (handshakeStatus) { case NEED_UNWRAP: int bytesRead = socketChannel.read(netBuffer); if (bytesRead > 0) { netBuffer.flip(); SSLEngineResult result = engine.unwrap(netBuffer, appBuffer); netBuffer.compact(); handshakeStatus = result.getHandshakeStatus(); } break; case NEED_WRAP: netBuffer.clear(); SSLEngineResult result = engine.wrap(appBuffer, netBuffer); netBuffer.flip(); while (netBuffer.hasRemaining()) { socketChannel.write(netBuffer); } handshakeStatus = result.getHandshakeStatus(); break; case NEED_TASK: Runnable task; while ((task = engine.getDelegatedTask()) != null) { task.run(); } handshakeStatus = engine.getHandshakeStatus(); break; default: throw new IllegalStateException("Unexpected handshake status: " + handshakeStatus); } Thread.sleep(10); } } private static ByteBuffer read(SocketChannel socketChannel, SSLEngine engine) throws Exception { ByteBuffer peerNetData = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); ByteBuffer peerAppData = ByteBuffer.allocate(engine.getSession().getApplicationBufferSize()); boolean exitReadLoop = false; peerAppData.clear(); while (!exitReadLoop) { int bytesRead = socketChannel.read(peerNetData); if (bytesRead > 0) { peerNetData.flip(); boolean needMoreData = false; while (peerNetData.hasRemaining() && !needMoreData) { SSLEngineResult result = engine.unwrap(peerNetData, peerAppData); switch (result.getStatus()) { case OK: if (!peerNetData.hasRemaining()) { peerNetData.clear(); } else { peerNetData.compact(); } if (peerAppData.position() > 0) { exitReadLoop = true; } break; case BUFFER_OVERFLOW: peerAppData = enlargeApplicationBuffer(engine, peerAppData); break; case BUFFER_UNDERFLOW: peerNetData = handleBufferUnderflow(engine, peerNetData); needMoreData = true; break; case CLOSED: closeConnection(socketChannel, engine); return null; default: throw new IllegalStateException("Invalid SSL status: " + result.getStatus()); } } } else if (bytesRead < 0) { handleEndOfStream(socketChannel, engine); return null; } if (!exitReadLoop) { Thread.sleep(50); } } peerAppData.flip(); return peerAppData; } private static ByteBuffer enlargeApplicationBuffer(SSLEngine engine, ByteBuffer buffer) { int newCapacity = engine.getSession().getApplicationBufferSize(); if (newCapacity <= buffer.capacity()) return buffer; ByteBuffer newBuffer = ByteBuffer.allocate(newCapacity); buffer.flip(); newBuffer.put(buffer); return newBuffer; } private static ByteBuffer handleBufferUnderflow(SSLEngine engine, ByteBuffer buffer) { int newCapacity = engine.getSession().getPacketBufferSize(); if (newCapacity <= buffer.capacity()) { buffer.compact(); return buffer; } ByteBuffer newBuffer = ByteBuffer.allocate(newCapacity); buffer.flip(); newBuffer.put(buffer); return newBuffer; } private static void closeConnection(SocketChannel socketChannel, SSLEngine engine) throws Exception { engine.closeOutbound(); ByteBuffer netBuffer = ByteBuffer.allocate(engine.getSession().getPacketBufferSize()); while (!engine.isOutboundDone() || !engine.isInboundDone()) { SSLEngineResult result = engine.wrap(ByteBuffer.allocate(0), netBuffer); netBuffer.flip(); while (netBuffer.hasRemaining()) { socketChannel.write(netBuffer); } netBuffer.clear(); if (result.getStatus() == SSLEngineResult.Status.CLOSED) break; Thread.sleep(10); } socketChannel.close(); } private static void handleEndOfStream(SocketChannel socketChannel, SSLEngine engine) throws Exception { engine.closeInbound(); closeConnection(socketChannel, engine); } }
示例说明
- PSK配置:自定义
PSKKeyManager实现,返回预共享密钥和身份标识,适配Conscrypt的PSK认证逻辑。 - 握手处理:专门的
doHandshake方法处理TLS握手的全流程,覆盖NEED_WRAP/NEED_UNWRAP/NEED_TASK三种状态。 - 标准读写逻辑:严格遵循SSLEngine状态机,正确处理缓冲区溢出/不足的场景,确保大体积TLS数据能被完整解析。
按照这个逻辑调整后,应该能解决你遇到的大体积数据读取异常问题。
内容的提问来源于stack exchange,提问作者user3615458
相关产品推荐
相关产品推荐

