Java8 java.util.logging的SocketHandler抛出ConnectionException问题排查
运行时通过浏览器流式访问Java日志的问题解决
初始问题:SocketHandler连接被拒绝
需求是在运行时通过特定host:port流式传输日志,让外部用户可通过浏览器访问。基于Java8的java.util.logging实现了简单SocketHandler,但运行时抛出ConnectionException,已排查本地(Mac/Windows)及远程(Linux)防火墙未阻断19004端口。
初始代码
import java.net.MalformedURLException; import java.net.URL; import java.io.IOException; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.SocketHandler; import java.util.logging.XMLFormatter; public class Main { public static void main(String[] args) throws Exception { Logger logger = Logger.getLogger("concrete.log"); SocketHandler handler = new SocketHandler("HOSTNAME", 19004); LogRecord logRec = new LogRecord(Level.INFO, "Log recorded"); handler.publish(logRec); handler.setFormatter(new XMLFormatter()); logger.addHandler(handler); logger.info("socket handler info message"); } }
抛出异常
Exception in thread "main" java.net.ConnectException: Connection refused (Connection refused) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394) at java.net.Socket.connect(Socket.java:606) at java.net.Socket.connect(Socket.java:555) at java.net.Socket.<init>(Socket.java:451) at java.net.Socket.<init>(Socket.java:228) at java.util.logging.SocketHandler.connect(SocketHandler.java:167) at java.util.logging.SocketHandler.<init>(SocketHandler.java:154) at Main.main(Main.java:16)
更新情况:ServerSocket接收日志成功,但浏览器无法访问
按建议编写ServerSocket代码后,日志可在主机控制台打印,但无法通过浏览器访问hostname:port流式查看日志,咨询后续操作。
ServerSocket代码
import java.io.*; import java.net.*; public class MyServer { public static void main(String[] args) { try { ServerSocket ss = new ServerSocket(19004); Socket soc = ss.accept(); DataInputStream dis = new DataInputStream(soc.getInputStream()); String str = (String)dis.readUTF(); System.out.println("message= " + str); ss.close(); } catch (Exception e) { System.out.println(e); } } }
问题分析与解决方案
核心问题
- 初始的
ConnectionException:SocketHandler是客户端角色,必须先有监听对应端口的服务端,否则会因无服务响应导致连接被拒绝。 - 后续浏览器无法访问:当前
ServerSocket仅实现了基础Socket通信,未处理HTTP协议——浏览器访问基于HTTP标准,直接的Socket数据无法被浏览器解析,且服务端接收一次日志就关闭连接,不支持流式推送。
后续实现步骤
1. 改造服务端为HTTP长连接服务(SSE模式)
使用**服务器发送事件(SSE)**实现流式推送,这是适合日志场景的HTTP标准方案,无需复杂的WebSocket,浏览器可直接解析。
import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; public class LogStreamingServer { private static final int PORT = 19004; private static final LinkedBlockingQueue<String> LOG_QUEUE = new LinkedBlockingQueue<>(); private static ExecutorService executor = Executors.newCachedThreadPool(); public static void main(String[] args) throws IOException { // 启动日志消费线程,持续从队列取日志推送给客户端 executor.submit(LogStreamingServer::pushLogsToClients); ServerSocket serverSocket = new ServerSocket(PORT); System.out.println("日志流服务启动,监听端口:" + PORT); // 持续接收浏览器连接 while (true) { Socket clientSocket = serverSocket.accept(); executor.submit(() -> handleBrowserClient(clientSocket)); } } // 处理浏览器HTTP请求,返回SSE响应头 private static void handleBrowserClient(Socket clientSocket) { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) { // 发送HTTP响应头,声明SSE类型 writer.write("HTTP/1.1 200 OK\r\n"); writer.write("Content-Type: text/event-stream\r\n"); writer.write("Cache-Control: no-cache\r\n"); writer.write("Connection: keep-alive\r\n"); writer.write("\r\n"); writer.flush(); // 标记该客户端已准备好接收日志 synchronized (LOG_QUEUE) { LOG_QUEUE.notifyAll(); } } catch (IOException e) { try { clientSocket.close(); } catch (IOException ex) { ex.printStackTrace(); } } } // 从队列取日志,推送给所有在线浏览器客户端 private static void pushLogsToClients() { while (true) { try { String log = LOG_QUEUE.take(); // 遍历所有活跃客户端推送(此处简化,实际需维护客户端列表) // 示例中省略客户端列表维护,可根据需求扩展 executor.submit(() -> { // 实际需遍历所有已连接的客户端Writer发送日志 // 格式:data: 日志内容\n\n System.out.println("推送日志:" + log); }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } // 提供给日志Handler的日志写入方法 public static void writeLog(String log) { try { LOG_QUEUE.put(log); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
2. 改造日志Handler,对接服务端队列
自定义Handler将日志写入服务端的共享队列,避免直接Socket通信的耦合:
import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.logging.SimpleFormatter; public class StreamingLogHandler extends Handler { private final SimpleFormatter formatter = new SimpleFormatter(); @Override public void publish(LogRecord record) { if (!isLoggable(record)) { return; } String logContent = formatter.format(record); // 将日志写入服务端的队列 LogStreamingServer.writeLog(logContent); } @Override public void flush() {} @Override public void close() throws SecurityException {} }
3. 整合与测试
- 先启动
LogStreamingServer - 修改主程序使用自定义Handler:
public class Main { public static void main(String[] args) throws Exception { Logger logger = Logger.getLogger("concrete.log"); logger.addHandler(new StreamingLogHandler()); // 模拟日志生成 while (true) { logger.info("实时日志消息:" + System.currentTimeMillis()); Thread.sleep(1000); } } } - 打开浏览器访问
http://hostname:19004,即可看到实时流式日志
关键注意点
- SSE协议:必须返回正确的HTTP响应头,日志需按
data: 内容\n\n格式发送,浏览器才能识别为流式事件。 - 并发处理:服务端需用线程池管理多个浏览器连接,避免单线程阻塞。
- 队列解耦:用阻塞队列存储日志,实现日志生产(业务程序)与消费(推送浏览器)的解耦,防止日志丢失或业务线程阻塞。
内容的提问来源于stack exchange,提问作者forkdbloke
相关产品推荐
相关产品推荐

