You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
        }
    }
}

问题分析与解决方案

核心问题

  1. 初始的ConnectionException:SocketHandler是客户端角色,必须先有监听对应端口的服务端,否则会因无服务响应导致连接被拒绝。
  2. 后续浏览器无法访问:当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 22:00:58