先导入以下依赖:
<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;
}
}
}