Windows或mac支持本地抓包

先导入以下依赖:

复制代码
<dependencies>
    <!-- pcap4j 抓包核心库 -->
    <dependency>
        <groupId>org.pcap4j</groupId>
        <artifactId>pcap4j-core</artifactId>
        <version>1.7.5</version>
    </dependency>
    <dependency>
        <groupId>org.pcap4j</groupId>
        <artifactId>pcap4j-packetfactory-static</artifactId>
        <version>1.7.5</version>
    </dependency>
</dependencies>

具体代码实现:

复制代码
package com.example.testcasegenerator.util;

import org.pcap4j.core.BpfProgram.BpfCompileMode;
import org.pcap4j.core.*;
import org.pcap4j.packet.IpV4Packet;
import org.pcap4j.packet.Packet;
import org.pcap4j.packet.TcpPacket;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 跨平台本地 HTTP 抓包工具(Windows / Mac)。
 * <p>
 * 功能:
 * <ul>
 *   <li>可选指定目标 IP / 端口(都可省略,省略即不过滤该维度)</li>
 *   <li>按 TCP 四元组 + 方向做流重组,避免 HTTP 报文被 TCP 分片切断</li>
 *   <li>解析 HTTP 请求:请求方法 / URL / 查询参数 / 请求体</li>
 *   <li>解析 HTTP 响应:状态行 / 响应体(支持 Content-Length / chunked / 无长度关闭结束)</li>
 *   <li>请求响应按 srcIp:srcPort-dstIp:dstPort 关联展示</li>
 *   <li>抓包结果自动保存到 ./capture-yyyyMMdd-HHmmss.log(可 --out 指定路径)</li>
 *   <li>可选 --pcap 同时把原始报文保存为 pcap,便于 Wireshark 打开</li>
 *   <li>回车 或 Ctrl+C 优雅停止抓包并汇总</li>
 * </ul>
 * <p>
 * 运行前置条件:
 * <ul>
 *   <li>Windows:安装 Npcap(勾选 WinPcap 兼容模式),以管理员运行</li>
 *   <li>Mac / Linux:安装 libpcap(Mac 自带),需 sudo 运行</li>
 * </ul>
 * <p>
 * 使用方式(参数全部可选,顺序无关):
 * <pre>
 *   # 全抓(默认写 ./capture-*.log)
 *   sudo java com.example.testcasegenerator.util.CrossPlatformPacketCapture
 *
 *   # 只抓某 IP,不限端口
 *   sudo java ... --ip=192.168.1.10
 *
 *   # 只抓某端口,不限 IP
 *   sudo java ... --port=8080
 *
 *   # 指定 IP + 端口 + 网卡 + 输出文件 + 同时保存 pcap
 *   sudo java ... --ip=127.0.0.1 --port=8080 --nic=lo0 --out=./mycap.log --pcap
 * </pre>
 *
 * @author dongjinbi1
 */
public class CrossPlatformPacketCapture {

    private static final SimpleDateFormat TS = new SimpleDateFormat("HH:mm:ss.SSS");
    private static final SimpleDateFormat FILE_TS = new SimpleDateFormat("yyyyMMdd-HHmmss");

    /** 每条 TCP 单向流的缓冲区,key = srcIp:srcPort->dstIp:dstPort */
    private static final Map<String, StreamBuffer> STREAMS = new ConcurrentHashMap<>();
    /** 统计计数 */
    private static int reqCount = 0;
    private static int rspCount = 0;

    /** 流程分组:连续两次请求间隔 > FLOW_GAP_MS 视为新流程 */
    private static final long FLOW_GAP_MS = 60_000L;
    private static int flowNo = 0;
    private static long lastReqAt = 0L;

    private static final AtomicBoolean RUNNING = new AtomicBoolean(true);

    /** 文件日志输出 */
    private static PrintWriter LOG_WRITER;
    /** 原始 pcap dumper(可选) */
    private static PcapDumper PCAP_DUMPER;
    /** 抓包 handle(stop 时统一关闭) */
    private static PcapHandle HANDLE;
    /** 日志文件绝对路径(stop 时打印用) */
    private static Path LOG_FILE_PATH;
    /** pcap 文件绝对路径(stop 时打印用,可为 null) */
    private static Path PCAP_FILE_PATH;

    public static void main(String[] args) throws Exception {
        // ============ 解析命令行参数(命名式,顺序无关) ============
        Map<String, String> opts = parseArgs(args);
        String targetIp = opts.get("ip");         // 可为 null
        Integer targetPort = opts.containsKey("port") ? Integer.parseInt(opts.get("port")) : null;
        String preferNic = opts.get("nic");
        String outPath = opts.get("out");         // 可为 null
        boolean writePcap = opts.containsKey("pcap");

        // 生成默认输出文件名,统一放到项目下 capture-logs/ 目录
        String tsSuffix = FILE_TS.format(new Date());
        Path saveDir = Paths.get("capture-logs").toAbsolutePath();
        Files.createDirectories(saveDir);

        Path logFile;
        if (outPath == null || outPath.isEmpty()) {
            logFile = saveDir.resolve("capture-" + tsSuffix + ".log");
        } else {
            Path p = Paths.get(outPath);
            // 绝对路径尊重用户;相对路径则挂到 capture-logs 下
            logFile = p.isAbsolute() ? p : saveDir.resolve(p);
            Files.createDirectories(logFile.getParent() == null ? saveDir : logFile.getParent());
        }
        LOG_WRITER = new PrintWriter(Files.newBufferedWriter(logFile,
                StandardCharsets.UTF_8), true);
        LOG_FILE_PATH = logFile;

        Path pcapFile = null;
        if (writePcap) {
            String name = logFile.getFileName().toString().replaceAll("\\.log$", "") + ".pcap";
            pcapFile = logFile.getParent().resolve(name);
            PCAP_FILE_PATH = pcapFile;
        }

        log("===============================================");
        log("  本地抓包工具 (基于 pcap4j)");
        log("  目标 IP  : " + (targetIp == null ? "(不限)" : targetIp));
        log("  目标端口 : " + (targetPort == null ? "(不限)" : targetPort));
        log("  操作系统 : " + System.getProperty("os.name"));
        log("  日志文件 : " + logFile);
        if (pcapFile != null) log("  Pcap文件 : " + pcapFile);
        log("===============================================\n");

        // ============ 获取网卡 ============
        List<PcapNetworkInterface> devices = Pcaps.findAllDevs();
        if (devices == null || devices.isEmpty()) {
            log("[X] 未识别到任何网卡!");
            log("    Windows: 请安装 Npcap  https://npcap.com/");
            log("    Mac/Linux: 请使用 sudo 运行本程序");
            closeResources();
            return;
        }

        PcapNetworkInterface nif = chooseNetworkInterface(devices, targetIp, preferNic);
        if (nif == null) {
            log("[X] 未选择到有效网卡,退出。");
            closeResources();
            return;
        }
        log("[√] 使用网卡: " + nif.getName()
                + (nif.getDescription() != null ? " (" + nif.getDescription() + ")" : ""));

        // ============ 打开网卡 & 设置过滤 ============
        int snapLen = 65536;
        int readTimeoutMs = 50;
        HANDLE = nif.openLive(snapLen,
                PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, readTimeoutMs);

        String bpf = buildBpf(targetIp, targetPort);
        if (!bpf.isEmpty()) {
            HANDLE.setFilter(bpf, BpfCompileMode.OPTIMIZE);
        }
        log("[√] BPF 过滤: " + (bpf.isEmpty() ? "(无,抓所有 TCP)" : bpf));
        log("[i] 提示: 仅能解析明文 HTTP;HTTPS 只能看到加密字节。");
        log("[i] 按 Ctrl+C 结束抓包。\n");

        // 打开 pcap dumper(可选)
        if (pcapFile != null) {
            PCAP_DUMPER = HANDLE.dumpOpen(pcapFile.toString());
        }

        // ============ 优雅停止:Ctrl+C -> JVM Shutdown Hook ============
        Runtime.getRuntime().addShutdownHook(new Thread(CrossPlatformPacketCapture::stop));

        // ============ 抓包主循环 ============
        try {
   while (RUNNING.get()) {
                Packet packet;
                try {
                    packet = HANDLE.getNextPacketEx();
                } catch (TimeoutException e) {
                    continue;
                } catch (NotOpenException | PcapNativeException e) {
                    break;
                }
                if (packet == null) continue;
                // 保存原始报文
                if (PCAP_DUMPER != null) {
                    try {
                        PCAP_DUMPER.dump(packet, HANDLE.getTimestamp());
                    } catch (Exception ignore) {
                    }
                }
                try {
                    handlePacket(packet);
                } catch (Exception ex) {
                    log("[!] 解析异常: " + ex.getMessage());
                }
            }
        } finally {
            summarize(logFile, pcapFile);
            closeResources();
        }
    }

    /** 构造 BPF:ip 与 port 均可选 */
    private static String buildBpf(String ip, Integer port) {
        StringBuilder sb = new StringBuilder("tcp");
        if (ip != null && !ip.isEmpty()) sb.append(" and host ").append(ip);
        if (port != null) sb.append(" and port ").append(port);
        return sb.toString();
    }

    /** 命令行参数解析:支持 --k=v 与 --flag */
    private static Map<String, String> parseArgs(String[] args) {
        Map<String, String> map = new HashMap<>();
        for (String a : args) {
            if (a == null) continue;
            if (a.startsWith("--")) {
                String kv = a.substring(2);
                int eq = kv.indexOf('=');
                if (eq > 0) {
                    map.put(kv.substring(0, eq).trim().toLowerCase(),
                            kv.substring(eq + 1).trim());
                } else {
                    map.put(kv.trim().toLowerCase(), "true");
                }
            }
        }
        return map;
    }

    /** 优雅停止 */
    private static void stop() {
        if (RUNNING.compareAndSet(true, false)) {
            try {
                if (HANDLE != null && HANDLE.isOpen()) {
                    HANDLE.breakLoop();
                }
            } catch (Exception ignore) {
            }
            log("");
            log("╔══════════════════════════════════════════════════════════════════════╗");
            log("║  [√] 抓包已停止(Ctrl+C)");
            if (LOG_FILE_PATH != null) {
                log("║  抓包日志文件已保存至:");
                log("║    " + LOG_FILE_PATH);
            }
            if (PCAP_FILE_PATH != null) {
                log("║  原始 pcap 文件已保存至:");
                log("║    " + PCAP_FILE_PATH);
            }
            log("╚══════════════════════════════════════════════════════════════════════╝");
        }
    }

    /** 关闭所有 IO 资源 */
    private static void closeResources() {
        try {
            if (PCAP_DUMPER != null) {
                PCAP_DUMPER.flush();
                PCAP_DUMPER.close();
            }
        } catch (Exception ignore) {
        }
        try {
            if (HANDLE != null && HANDLE.isOpen()) HANDLE.close();
        } catch (Exception ignore) {
        }
        try {
            if (LOG_WRITER != null) {
                LOG_WRITER.flush();
                LOG_WRITER.close();
            }
        } catch (Exception ignore) {
        }
    }

    /** 打印汇总 */
    private static void summarize(Path logFile, Path pcapFile) {
        log("\n=================== 抓包汇总 ===================");
        log("  HTTP 请求总数 : " + reqCount);
        log("  HTTP 响应总数 : " + rspCount);
        log("  流程总数      : " + flowNo);
        log("  日志文件      : " + logFile);
        if (pcapFile != null) log("  Pcap 文件     : " + pcapFile);
        log("================================================");
    }

    /**
     * 选择网卡:
     * 1) 命令行传入网卡名,优先使用
     * 2) 尝试匹配 targetIp 对应的网卡
     * 3) 否则交互式选择
     */
    private static PcapNetworkInterface chooseNetworkInterface(List<PcapNetworkInterface> devices,
                                                               String targetIp,
                                                               String preferNic) throws Exception {
        // 1) 显式指定
        if (preferNic != null && !preferNic.isEmpty()) {
            for (PcapNetworkInterface d : devices) {
                if (preferNic.equals(d.getName())) return d;
            }
            log("[!] 指定网卡 " + preferNic + " 未找到,改为自动选择。");
        }

        // 2) 按 targetIp 自动匹配
        if (targetIp != null && !targetIp.isEmpty()) {
            boolean loopback = "127.0.0.1".equals(targetIp)
                    || "localhost".equalsIgnoreCase(targetIp)
                    || targetIp.startsWith("127.");
            if (loopback) {
                for (PcapNetworkInterface d : devices) {
                    if (d.isLoopBack()) return d;
                    String name = d.getName();
                    if (name != null && (name.startsWith("lo") || name.contains("Loopback"))) return d;
                }
            } else {
                try {
                    InetAddress ia = InetAddress.getByName(targetIp);
                    for (PcapNetworkInterface d : devices) {
                        for (PcapAddress addr : d.getAddresses()) {
                            if (addr.getAddress() != null
                                    && addr.getAddress().getHostAddress().equals(ia.getHostAddress())) {
                                return d;
                            }
                        }
                    }
                } catch (Exception ignore) {
                }
            }
        }

        // 3) 交互式选择
        System.out.println("===== 网卡列表 =====");
        for (int i = 0; i < devices.size(); i++) {
            PcapNetworkInterface ni = devices.get(i);
            StringBuilder ips = new StringBuilder();
            for (PcapAddress a : ni.getAddresses()) {
                if (a.getAddress() != null) {
                    if (ips.length() > 0) ips.append(",");
                    ips.append(a.getAddress().getHostAddress());
                }
            }
            System.out.printf("[%d] %-25s %s  IPs=%s%n",
                    i, ni.getName(),
                    ni.getDescription() == null ? "" : ni.getDescription(),
                    ips);
        }
        System.out.print("请选择网卡序号: ");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = br.readLine();
        try {
            int idx = Integer.parseInt(line.trim());
            if (idx >= 0 && idx < devices.size()) return devices.get(idx);
        } catch (Exception ignore) {
        }
        return devices.get(0);
    }

    /** 处理单个数据包:按流缓冲,尝试解析 HTTP */
    private static void handlePacket(Packet packet) {
        IpV4Packet ipv4 = packet.get(IpV4Packet.class);
        TcpPacket tcp = packet.get(TcpPacket.class);
        if (ipv4 == null || tcp == null) return;

        String srcIp = ipv4.getHeader().getSrcAddr().getHostAddress();
        String dstIp = ipv4.getHeader().getDstAddr().getHostAddress();
        int srcPort = tcp.getHeader().getSrcPort().valueAsInt();
        int dstPort = tcp.getHeader().getDstPort().valueAsInt();

        boolean fin = tcp.getHeader().getFin();
        boolean rst = tcp.getHeader().getRst();

        Packet payload = tcp.getPayload();
        String flowKey = srcIp + ":" + srcPort + "->" + dstIp + ":" + dstPort;

        if (payload != null && payload.getRawData() != null && payload.getRawData().length > 0) {
            byte[] data = payload.getRawData();
            StreamBuffer buf = STREAMS.computeIfAbsent(flowKey,
                    k -> new StreamBuffer(srcIp, srcPort, dstIp, dstPort));
            buf.append(data);
            tryParse(buf);
        }

        if (fin || rst) {
            StreamBuffer buf = STREAMS.remove(flowKey);
            if (buf != null) {
                buf.markClosed();
                tryParse(buf);
            }
        }
    }

    /** 尝试从缓冲区里解析出完整的 HTTP 请求 / 响应 */
    private static void tryParse(StreamBuffer buf) {
        while (true) {
            String text = new String(buf.data(), StandardCharsets.ISO_8859_1);

            boolean isRequest = startsWithAnyMethod(text);
            boolean isResponse = text.startsWith("HTTP/");
            if (!isRequest && !isResponse) {
                if (buf.size() > 16 * 1024) buf.reset();
                return;
            }

            int headerEnd = text.indexOf("\r\n\r\n");
            if (headerEnd < 0) return;

            String header = text.substring(0, headerEnd);
            int bodyStart = headerEnd + 4;
            Map<String, String> headers = parseHeaders(header);

            int bodyLen;
            String cl = headers.get("content-length");
            String te = headers.get("transfer-encoding");
            if (cl != null) {
                try {
                    bodyLen = Integer.parseInt(cl.trim());
                } catch (NumberFormatException e) {
                    bodyLen = 0;
                }
                if (buf.size() < bodyStart + bodyLen) {
                    if (!buf.isClosed()) return;
                    bodyLen = Math.max(0, buf.size() - bodyStart);
                }
            } else if (te != null && te.toLowerCase().contains("chunked")) {
                int endIdx = text.indexOf("0\r\n\r\n", bodyStart);
                if (endIdx < 0) {
                    if (!buf.isClosed()) return;
                    bodyLen = buf.size() - bodyStart;
                } else {
                    bodyLen = endIdx + 5 - bodyStart;
                }
            } else {
                if (isRequest) {
                    bodyLen = 0;
                } else {
                    if (!buf.isClosed()) return;
                    bodyLen = buf.size() - bodyStart;
                }
            }

            int totalLen = bodyStart + bodyLen;
            if (buf.size() < totalLen) totalLen = buf.size();

            byte[] rawAll = buf.data();
            byte[] bodyBytes = new byte[Math.max(0, totalLen - bodyStart)];
            if (bodyBytes.length > 0) {
                System.arraycopy(rawAll, bodyStart, bodyBytes, 0, bodyBytes.length);
            }

            if (isRequest) {
                printRequest(buf, header, headers, bodyBytes);
                reqCount++;
            } else {
                printResponse(buf, header, headers, bodyBytes);
                rspCount++;
            }

            buf.consume(totalLen);
            if (buf.size() == 0) return;
        }
    }

    private static boolean startsWithAnyMethod(String s) {
        return s.startsWith("GET ") || s.startsWith("POST ") || s.startsWith("PUT ")
                || s.startsWith("DELETE ") || s.startsWith("HEAD ") || s.startsWith("OPTIONS ")
                || s.startsWith("PATCH ") || s.startsWith("TRACE ");
    }

    private static Map<String, String> parseHeaders(String header) {
        Map<String, String> map = new LinkedHashMap<>();
        String[] lines = header.split("\r\n");
        for (int i = 1; i < lines.length; i++) {
            String line = lines[i];
            int c = line.indexOf(':');
            if (c > 0) {
                map.put(line.substring(0, c).trim().toLowerCase(), line.substring(c + 1).trim());
            }
        }
        return map;
    }

    private static void printRequest(StreamBuffer buf, String header,
                                     Map<String, String> headers, byte[] bodyBytes) {
        // 计算流程号:与上一次请求间隔 > FLOW_GAP_MS 或第一次时,流程号 +1
        long now = System.currentTimeMillis();
        if (lastReqAt == 0L || (now - lastReqAt) > FLOW_GAP_MS) {
            flowNo++;
            if (lastReqAt != 0L) {
                log(">>>>>> 检测到间隔 > " + (FLOW_GAP_MS / 1000) + "s,进入 流程" + flowNo + " <<<<<<");
            }
        }
        lastReqAt = now;

        String[] lines = header.split("\r\n");
        String requestLine = lines.length > 0 ? lines[0] : "";
        String[] parts = requestLine.split(" ");
        String method = parts.length > 0 ? parts[0] : "?";
        String path = parts.length > 1 ? parts[1] : "?";
        String host = headers.getOrDefault("host", "");
        String fullUrl = "http://" + host + path;

        String query = "";
        int q = path.indexOf('?');
        if (q >= 0) query = path.substring(q + 1);

        log("========== [流程" + flowNo + "] HTTP REQUEST  " + TS.format(new Date()) + " ==========");
        log("  连接    : " + buf.srcIp + ":" + buf.srcPort + " -> " + buf.dstIp + ":" + buf.dstPort);
        log("  方法    : " + method);
        log("  URL     : " + fullUrl);
        if (!query.isEmpty()) {
            String decodedQ = urlDecode(query);
            log("  Query   : " + decodedQ);
        }
        String ct = headers.get("content-type");
        if (ct != null) log("  Content-Type: " + ct);
        if (bodyBytes != null && bodyBytes.length > 0) {
            Charset cs = charsetOf(ct);
            String body = new String(bodyBytes, cs);
            // 表单类做 URL 解码,其它 (json/xml/text) 直接展示
            if (ct != null && ct.toLowerCase().contains("x-www-form-urlencoded")) {
                log("  请求体  :");
                log(indent(decodeForm(body)));
            } else {
                log("  请求体  :");
                log(indent(body));
            }
        }
        log("=====================================================\n");
    }

    private static void printResponse(StreamBuffer buf, String header,
                                      Map<String, String> headers, byte[] bodyBytes) {
        String[] lines = header.split("\r\n");
        String statusLine = lines.length > 0 ? lines[0] : "";
        log("---------- [流程" + (flowNo == 0 ? 1 : flowNo) + "] HTTP RESPONSE " + TS.format(new Date()) + " ----------");
        log("  连接    : " + buf.srcIp + ":" + buf.srcPort + " -> " + buf.dstIp + ":" + buf.dstPort);
        log("  状态行  : " + statusLine);
        String ct = headers.get("content-type");
        if (ct != null) log("  Content-Type: " + ct);
        String te = headers.get("transfer-encoding");
        String ce = headers.get("content-encoding");

        if (bodyBytes != null && bodyBytes.length > 0) {
            byte[] processed = bodyBytes;
            // 1) 先处理 chunked
            if (te != null && te.toLowerCase().contains("chunked")) {
                try {
                    processed = decodeChunked(processed);
                } catch (Exception e) {
                    log("  [!] chunked 解码失败: " + e.getMessage());
                }
            }
            // 2) 再处理 Content-Encoding: gzip / deflate
            if (ce != null) {
                try {
                    processed = decompress(processed, ce);
                } catch (Exception e) {
                    log("  [!] " + ce + " 解压失败: " + e.getMessage());
                }
            }
            Charset cs = charsetOf(ct);
            String body = new String(processed, cs);
            log("  响应体  :");
            log(indent(body));
        }
        log("-----------------------------------------------------\n");
    }

    /** 从 Content-Type 中解析 charset,默认 UTF-8 */
    private static Charset charsetOf(String contentType) {
        if (contentType == null) return StandardCharsets.UTF_8;
        String lower = contentType.toLowerCase();
        int i = lower.indexOf("charset=");
        if (i < 0) return StandardCharsets.UTF_8;
        String cs = contentType.substring(i + 8).trim();
        int end = cs.indexOf(';');
        if (end >= 0) cs = cs.substring(0, end).trim();
        cs = cs.replace("\"", "").replace("'", "");
        try {
            return Charset.forName(cs);
        } catch (Exception e) {
            return StandardCharsets.UTF_8;
        }
    }

    /** URL 解码,失败则原样返回 */
    private static String urlDecode(String s) {
        try {
            return URLDecoder.decode(s, "UTF-8");
        } catch (Exception e) {
            return s;
        }
    }

    /** application/x-www-form-urlencoded 解码:k=v&k=v -> 换行展示 */
    private static String decodeForm(String body) {
        StringBuilder sb = new StringBuilder();
        for (String kv : body.split("&")) {
            int eq = kv.indexOf('=');
            if (eq >= 0) {
                String k = urlDecode(kv.substring(0, eq));
                String v = urlDecode(kv.substring(eq + 1));
                sb.append(k).append(" = ").append(v).append('\n');
            } else {
                sb.append(urlDecode(kv)).append('\n');
            }
        }
        return sb.toString();
    }

    /** 合并 HTTP chunked 分块,返回真实 body 字节 */
    private static byte[] decodeChunked(byte[] data) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int i = 0;
        while (i < data.length) {
            // 读一行长度(16进制)
            int lineEnd = indexOfCRLF(data, i);
            if (lineEnd < 0) break;
            String sizeLine = new String(data, i, lineEnd - i, StandardCharsets.ISO_8859_1).trim();
            // chunk-ext (";xxx") 去掉
            int semi = sizeLine.indexOf(';');
            if (semi >= 0) sizeLine = sizeLine.substring(0, semi).trim();
            if (sizeLine.isEmpty()) {
                i = lineEnd + 2;
                continue;
            }
            int chunkSize;
            try {
                chunkSize = Integer.parseInt(sizeLine, 16);
            } catch (NumberFormatException e) {
                break;
            }
            i = lineEnd + 2;
            if (chunkSize == 0) break; // 结束
            int end = Math.min(i + chunkSize, data.length);
            out.write(data, i, end - i);
            i = end;
            // 跳过 chunk 结尾的 \r\n
            if (i + 1 < data.length && data[i] == '\r' && data[i + 1] == '\n') i += 2;
        }
        return out.toByteArray();
    }

    private static int indexOfCRLF(byte[] data, int from) {
        for (int i = from; i + 1 < data.length; i++) {
            if (data[i] == '\r' && data[i + 1] == '\n') return i;
        }
        return -1;
    }

    /** 解压 gzip / deflate */
    private static byte[] decompress(byte[] data, String encoding) throws Exception {
        String enc = encoding.toLowerCase().trim();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] tmp = new byte[4096];
        if (enc.contains("gzip")) {
            try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(data))) {
                int n;
                while ((n = gis.read(tmp)) > 0) out.write(tmp, 0, n);
            }
        } else if (enc.contains("deflate")) {
            try (InflaterInputStream iis = new InflaterInputStream(
                    new ByteArrayInputStream(data), new Inflater(false))) {
                int n;
                while ((n = iis.read(tmp)) > 0) out.write(tmp, 0, n);
            }
        } else {
            return data;
        }
        return out.toByteArray();
    }

    private static String indent(String s) {
        String[] lines = s.split("\n");
        StringBuilder sb = new StringBuilder();
        for (String l : lines) {
            sb.append("    ").append(l).append('\n');
        }
        return sb.toString();
    }

    /** 同时输出到控制台与日志文件 */
    private static synchronized void log(String msg) {
        System.out.println(msg);
        if (LOG_WRITER != null) {
            LOG_WRITER.println(msg);
        }
    }

    /** TCP 单向流缓冲区 */
    private static class StreamBuffer {
        final String srcIp, dstIp;
        final int srcPort, dstPort;
        private byte[] buf = new byte[0];
        private boolean closed = false;

        StreamBuffer(String srcIp, int srcPort, String dstIp, int dstPort) {
            this.srcIp = srcIp;
            this.dstIp = dstIp;
            this.srcPort = srcPort;
            this.dstPort = dstPort;
        }

        synchronized void append(byte[] more) {
            byte[] nb = new byte[buf.length + more.length];
            System.arraycopy(buf, 0, nb, 0, buf.length);
            System.arraycopy(more, 0, nb, buf.length, more.length);
            buf = nb;
        }

        synchronized void consume(int n) {
            if (n <= 0) return;
            if (n >= buf.length) {
                buf = new byte[0];
                return;
            }
            byte[] nb = new byte[buf.length - n];
            System.arraycopy(buf, n, nb, 0, nb.length);
            buf = nb;
        }

        synchronized void reset() {
            buf = new byte[0];
        }

        synchronized byte[] data() {
            return buf;
        }

        synchronized int size() {
            return buf.length;
        }

        void markClosed() {
            closed = true;
        }

        boolean isClosed() {
            return closed;
        }
    }
}
相关推荐
深念Y10 小时前
AMD 芯片组驱动触发高频 SSD 写入的问题及解决方案
windows·bug·ssd·日志·芯片·驱动·amd
做萤石二次开发的哈哈10 小时前
开发者如何调用 ERTC iOS API 完成 SDK 初始化配置?
macos·objective-c·cocoa
sukalot10 小时前
windows 驱动实例分析系列: wintun驱动分析-example篇(下)
windows
AUV110712 小时前
Mac 录屏自动缩放怎么设置才不乱跳:自动片段、固定区域与鼠标跟随调试
macos·计算机外设
怦怦蓝13 小时前
Windows 安装 Docker Desktop 踩坑完整实战指南(WSL2 故障排错实录)
windows·docker·容器
程序员良辰14 小时前
【Mac快速切换JDK】Mac 使用 Homebrew + jenv 快速切换 JDK 版本
java·开发语言·macos
海盗123415 小时前
微软技术日报·2026-08-12——Windows 11多通道累积更新推送十余项功能改进;.NET WebSocket DoS紧急修复
windows·microsoft·.net
for_ever_love__1 天前
python基础语法学习: 数据容器
windows·python·学习
huainingning1 天前
微软windows系统官网IPV6临时地址介绍
windows·microsoft·智能路由器