基于Java实现TCP代理时SSL/HTTPS连接报错问题求助
嘿,这个问题我太熟悉了!之前帮朋友做Java代理的时候也踩过一模一样的坑,核心原因是你现在的代码只处理了普通HTTP请求,没正确适配HTTPS的CONNECT隧道模式,咱们一步步来搞定:
为什么会出现SSL_ERROR_RX_RECORD_TOO_LONG?
简单说,浏览器访问HTTPS网站时,先给代理发了个CONNECT请求要建隧道,但你的代理没识别这个请求,反而把后续浏览器发的SSL握手数据当成HTTP请求来解析,返回了HTTP格式的响应。浏览器收到这堆不符合SSL规范的数据,自然就报这个错了。
解决步骤:实现CONNECT请求处理
HTTPS代理和HTTP代理的逻辑完全不同,核心是先建立隧道再透明转发,具体要做这几件事:
1. 识别CONNECT请求
当客户端(浏览器)发来请求时,先读第一行,判断是不是以CONNECT开头的。比如请求格式是:
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
...
2. 建立到目标服务器的连接
从CONNECT请求里解析出目标主机和端口(比如上面的example.com和443),然后新建一个Socket连接到这个目标地址。
3. 返回隧道建立成功的响应
给客户端返回HTTP/1.1 200 Connection Established\r\n\r\n,注意必须有两个换行(\r\n\r\n)来结束HTTP响应头,否则浏览器会一直等。
4. 双向转发二进制数据
隧道打通后,代理就变成“透明管道”:
- 把客户端输入流的所有数据原封不动转发到目标服务器的输出流
- 把目标服务器输入流的所有数据原封不动转发到客户端的输出流
这里绝对不能解析或修改数据,因为后续都是SSL加密的通信,代理只负责转发就行。
给你一个简化的代码示例
下面是核心逻辑的代码片段,你可以参考整合到你的现有代码里:
public class ProxyHandler implements Runnable { private final Socket clientSocket; public ProxyHandler(Socket clientSocket) { this.clientSocket = clientSocket; } @Override public void run() { try (BufferedReader requestReader = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); OutputStream clientOut = clientSocket.getOutputStream()) { // 读取请求首行 String requestLine = requestReader.readLine(); if (requestLine == null) return; if (requestLine.startsWith("CONNECT")) { // 处理HTTPS隧道请求 processConnectRequest(requestLine, clientOut); } else { // 处理普通HTTP请求,用你原来的逻辑 processHttpRequest(requestLine, requestReader, clientOut); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void processConnectRequest(String requestLine, OutputStream clientOut) throws IOException, InterruptedException { // 解析目标主机和端口 String target = requestLine.split(" ")[1]; String[] hostPortPair = target.split(":"); String targetHost = hostPortPair[0]; int targetPort = hostPortPair.length > 1 ? Integer.parseInt(hostPortPair[1]) : 443; // 连接目标服务器 try (Socket targetSocket = new Socket(targetHost, targetPort)) { // 告诉浏览器隧道已建立 clientOut.write("HTTP/1.1 200 Connection Established\r\n\r\n".getBytes()); clientOut.flush(); // 启动双向转发线程 Thread clientToTarget = new Thread(() -> forwardStream(clientSocket.getInputStream(), targetSocket.getOutputStream())); Thread targetToClient = new Thread(() -> forwardStream(targetSocket.getInputStream(), clientSocket.getOutputStream())); clientToTarget.start(); targetToClient.start(); // 等待转发结束 clientToTarget.join(); targetToClient.join(); } catch (IOException e) { // 连接失败,返回503错误 clientOut.write("HTTP/1.1 503 Service Unavailable\r\n\r\n".getBytes()); clientOut.flush(); } } // 通用流转发方法 private void forwardStream(InputStream in, OutputStream out) { byte[] buffer = new byte[4096]; int bytesRead; try { while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); out.flush(); } } catch (IOException ignored) { // 连接断开时正常退出 } finally { try { in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } } } // 你的原有HTTP请求处理方法 private void processHttpRequest(String requestLine, BufferedReader requestReader, OutputStream clientOut) throws IOException { // 这里放你原来处理HTTP请求的代码逻辑 } }
额外注意事项
- 不要在CONNECT之后的转发阶段解析数据,哪怕你好奇也不行,SSL握手对数据完整性要求极高,一点点修改都会导致握手失败。
- 记得处理异常情况,比如连接目标服务器失败时要返回正确的HTTP错误响应,不然浏览器会一直卡着。
- 流转发用固定大小的字节数组(比如4096)效率更高,不要逐字节读。
内容的提问来源于stack exchange,提问作者reschiram
相关产品推荐
相关产品推荐

