当 JS WebSocket 从 FireFox 连接时,Java HttpsServer 服务器挂起

问题描述

TL;DR:

我明白,java HttpsServer 更多的是服务于常规的 HTTPS 请求,但是当客户端做一些疯狂的事情时它不应该崩溃或挂起。特别是,当基于 HttpsServer 的项目使用 JDK v11-v15 编译,并且 FireFox Dev 87.0b3 与 JS WebSocket 对象连接时 - 池线程挂起并且每隔一个 websocket 连接将导致积压填充我的 PC 上的 cpu 负载增加约 6%每次 F5 页面刷新。

使用 Chrome 或 Edge 访问时,情况并非如此。使用 jdk v1.8.0_251、v16 或 v17 编译的同一个项目在所有 3 个浏览器上都可以正常工作,因此它是 v11 到 v15 版本中的一个错误,只能用 FF 重现。

问题:

  • 这是一个已知问题,有人可以指出错误报告或其他讨论吗?
  • 如何使其与损坏的 JDK 一起使用?

如何复制:

我拉皮条 this example 总是返回 404 错误。我还记录了一个线程名称并使用了 2 个池线程来证明只有一个具有 websocket 连接的线程挂起。

package test;

import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;

import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.net.InetSocketAddress;
import java.util.concurrent.ThreadPoolExecutor;

import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import java.io.FileInputStream;

import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import java.util.concurrent.SynchronousQueue;
import java.lang.Runnable;
import java.net.HttpURLConnection;
import java.io.IOException;

public class MyHttpsServer implements HttpHandler {

  public static void main(String[] args) {
    MyHttpsServer app = new MyHttpsServer();
  }

  public MyHttpsServer() {
    System.out.println("Starting server...");
    try {
      InetSocketAddress address = new InetSocketAddress("192.168.1.2",8080);
      HttpsServer httpsServer = HttpsServer.create(address,0);
      SSLContext sslContext = SSLContext.getInstance("TLS");
      char[] password = "qwerty".tochararray();
      KeyStore ks = KeyStore.getInstance("JKS");
      FileInputStream fis = new FileInputStream("my.keystore");
      ks.load(fis,password);
      KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
      kmf.init(ks,password);
      TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      tmf.init(ks);
      sslContext.init(kmf.getKeyManagers(),tmf.getTrustManagers(),null);
      httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
        public void configure(HttpsParameters params) {
          try {
            SSLContext c = SSLContext.getDefault();
            SSLEngine engine = c.createSSLEngine();
            params.setNeedClientAuth(false);
            params.setCipherSuites(engine.getEnabledCipherSuites());
            params.setProtocols(engine.getEnabledProtocols());
            SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
            params.setSSLParameters(defaultSSLParameters);
          } catch (Throwable t) {
            System.out.println("Failed to create HTTPS port: " + t.getLocalizedMessage());
          }
        }
      });
      httpsServer.createContext("/",this);
      ThreadPoolExecutor httpsServerExecutors = new ThreadPoolExecutor(2,2,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
      httpsServer.setExecutor(httpsServerExecutors);
      httpsServer.start();
      System.out.println("Server is running!");
    } catch (Throwable t) {
      System.out.println("Failed to create HTTPS server on port: " + t.getLocalizedMessage());
    }
  }

  @Override
  public void handle(HttpExchange httpsExchange) throws IOException {
    System.out.println("in " + Thread.currentThread().getName());
    OutputStream os = null;
    try {
      Thread.sleep(1000);
      httpsExchange.getResponseHeaders().add("Access-Control-Allow-Origin","*");
      httpsExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND,0);
      os = httpsExchange.getResponseBody();
      os.write("Page Not Found".getBytes(java.nio.charset.StandardCharsets.UTF_8));
    } catch (Throwable t) {
      System.out.println("Failed to handle: " + t.getLocalizedMessage());
    } finally {
      os.close();
    }
  }
}

这与 Java 8 和 11 完美配合,可以从 FF、Edge 和 Chrome 定期访问,无论您刷新页面多少次。

现在在桌面上添加一个“hack.http”文件

<html><head><script>
let socket = new WebSocket("wss://192.168.1.2:8080/WebSocket");
</script></head><body>OK</body></html>

将其放入 FireFox 并刷新页面两次。第三个不会出现在服务器日志中,坐在积压队列中。

可能的原因:

如果您在“do 循环”中的 sun.net.httpserver.SSLStreams::doClosure() 中放置断点并检查线程名称“Thread.currentThread().getName()”,它将与日志匹配。它旋转轮子,直到“\r\n”出现,这永远不会发生,因为 WebSocket 不会发送它。更糟糕的是,因为这个循环不检查中断,我什至无法停止这个线程。

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)