作者 钟来

开发透传服务

正在显示 29 个修改的文件 包含 995 行增加0 行删除
... ... @@ -76,6 +76,10 @@
<groupId>com.zhonglai</groupId>
<artifactId>weixin-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
</dependency>
</dependencies>
<build>
... ...
... ... @@ -9,11 +9,21 @@ import com.ruoyi.common.utils.GsonConstructor;
import com.zhonglai.luhui.api.controller.test.dto.ClueData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.batik.transcoder.Transcoder;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.JPEGTranscoder;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
... ... @@ -90,6 +100,65 @@ public class TestController {
"}";
}
@ApiOperation("重写Highcharts导出")
@RequestMapping(value = "getFeishuTable")
public void getFeishuTable(HttpServletRequest request, HttpServletResponse response) throws IOException {
String type = request.getParameter("type");
String svg = request.getParameter("svg");
String filename = request.getParameter("filename");
filename = filename == null ? "chart" : filename;
response.setCharacterEncoding("utf-8");
response.addHeader("Content-Disposition", "attachment; filename=" + filename + "." + getFileExtension(type));
response.addHeader("Content-Type", type);
ServletOutputStream out = response.getOutputStream();
try {
if (type != null && svg != null) {
svg = svg.replaceAll(":rect", "rect");
Transcoder t = null;
if ("image/png".equals(type)) {
t = new PNGTranscoder();
} else if ("image/jpeg".equals(type)) {
t = new JPEGTranscoder();
}
if (t != null) {
TranscoderInput input = new TranscoderInput(new StringReader(svg));
TranscoderOutput output = new TranscoderOutput(out);
t.transcode(input, output);
} else if ("image/svg+xml".equals(type)) {
OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8");
writer.write(svg);
writer.flush();
writer.close();
} else {
out.write("Invalid type: ".getBytes());
}
} else {
response.setContentType("text/html");
out.write("Usage:\n\tParameter [svg]: The DOM Element to be converted.\n\tParameter [type]: The destination MIME type for the element to be transcoded.".getBytes());
}
} catch (TranscoderException e) {
response.reset();
response.setContentType("text/plain");
out.write("Problem transcoding stream. See server logs for details.".getBytes());
e.printStackTrace();
} finally {
out.flush();
out.close();
}
}
private String getFileExtension(String mimeType) {
if ("image/png".equals(mimeType)) return "png";
if ("image/jpeg".equals(mimeType)) return "jpg";
if ("image/svg+xml".equals(mimeType)) return "svg";
return "bin";
}
public static void main(String[] args) {
JSONArray sort = new JSONArray();
JSONObject field_name = new JSONObject();
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>lh-neutrino-proxy-client</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy-common</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
... ...
package com.zhonglai.luhui.neutrino.proxy.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhonglai.luhui.neutrino.proxy.common.RegisterMessage;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class ClientMain {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 9000);
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
ObjectMapper mapper = new ObjectMapper();
RegisterMessage register = new RegisterMessage("clientA", "abc123");
writer.println(mapper.writeValueAsString(register));
String response = reader.readLine();
System.out.println("服务端响应:" + response);
new Thread(new HeartbeatTask(socket)).start();
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 心跳线程(每30秒发送)
*/
public class HeartbeatTask implements Runnable {
private final Socket socket;
public HeartbeatTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
ObjectMapper mapper = new ObjectMapper();
while (!socket.isClosed()) {
Map<String, String> heartbeat = new HashMap<>();
heartbeat.put("type", "heartbeat");
heartbeat.put("clientId", "clientA");
writer.println(mapper.writeValueAsString(heartbeat));
Thread.sleep(30000); // 30秒心跳
}
} catch (Exception e) {
System.out.println("心跳发送失败:" + e.getMessage());
}
}
}
\ No newline at end of file
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>lh-neutrino-proxy-common</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
... ...
package com.zhonglai.luhui.neutrino.proxy.common;
/**
* 注册协议类
*/
public class RegisterMessage {
public String type = "register";
public String clientId;
public String token;
public RegisterMessage() {}
public RegisterMessage(String clientId, String token) {
this.clientId = clientId;
this.token = token;
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.common;
/**
* 隧道请求协议
*/
public class TunnelMessage {
public String type = "tunnel";
public String tunnelId; // UUID
public String clientId;
public int remotePort; // 公网端口
public String targetHost;
public int targetPort;
}
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>lh-neutrino-proxy-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy-common</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
</project>
\ No newline at end of file
... ...
package com.zhonglai.luhui.neutrino.proxy.server;
import io.netty.channel.Channel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务端维护客户端连接
*/
public class ClientSessionManager {
private static final Map<String, Channel> clientMap = new ConcurrentHashMap<>();
public static void register(String clientId, Channel channel) {
clientMap.put(clientId, channel);
System.out.println("注册客户端:" + clientId);
}
public static void remove(Channel channel) {
clientMap.entrySet().removeIf(entry -> entry.getValue().equals(channel));
}
public static void heartbeat(String clientId) {
// 更新最后心跳时间(后续可扩展)
System.out.println("收到心跳:" + clientId);
}
public static boolean isClientConnected(String clientId) {
return clientMap.containsKey(clientId);
}
public static Channel getChannel(String clientId) {
return clientMap.get(clientId);
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server;
import com.zhonglai.luhui.neutrino.proxy.server.httpservice.SimpleHttpServer;
import com.zhonglai.luhui.neutrino.proxy.server.proxy.ProxyServer;
public class ControlServer {
public static void main(String[] args) throws Exception {
//启动代理服务
ProxyServer.start(9000);
//启动接口服务
SimpleHttpServer.startHttpServer(8080);
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.zhonglai.luhui.neutrino.proxy.common.TunnelMessage;
import com.zhonglai.luhui.neutrino.proxy.server.ClientSessionManager;
import io.netty.channel.Channel;
import java.io.*;
import java.net.*;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
public class PortForwardManager {
public static final Map<Integer, ServerSocket> activeServers = new ConcurrentHashMap<>();
public static final Map<String, Integer> clientBandwidthMap = new ConcurrentHashMap<>();
public void startPortForward(int remotePort, String clientId, int targetPort) throws IOException {
if (activeServers.containsKey(remotePort)) return;
ServerSocket serverSocket = new ServerSocket(remotePort);
activeServers.put(remotePort, serverSocket);
new Thread(() -> {
System.out.println("端口转发监听启动: " + remotePort + " => " + clientId + ":" + targetPort);
while (true) {
try {
Socket externalSocket = serverSocket.accept(); // 有外部连接过来了
String tunnelId = UUID.randomUUID().toString();
// 1. 注册外部连接 socket(暂时只 createBridge)
TunnelBridgePool.createBridge(tunnelId, externalSocket);
// 2. 通过 control 通道通知客户端建立内网连接
Channel channel = ClientSessionManager.getChannel(clientId);
if (channel != null) {
ObjectMapper mapper = new ObjectMapper();
ObjectNode msg = mapper.createObjectNode();
msg.put("type", "create_tunnel");
msg.put("tunnelId", tunnelId);
msg.put("targetPort", targetPort);
channel.writeAndFlush(msg.toString() + "\n");
} else {
System.err.println("客户端未连接,无法建立隧道: " + clientId);
externalSocket.close();
}
// ✅ 注意:此时 **不要 bindAndStart**,等客户端连接进来后才执行。
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function;
import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.RateLimitedInputStream;
import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.RateLimitedOutputStream;
import com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit.TokenBucket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务端建立转发通道并绑定 tunnelId
*/
public class TunnelBridgePool {
private static final Map<String, Socket> bridgeMap = new ConcurrentHashMap<>();
private static final Map<String, Integer> bandwidthMap = new ConcurrentHashMap<>();
public static void createBridge(String tunnelId, Socket externalSocket) {
bridgeMap.put(tunnelId, externalSocket);
}
public static void bindAndStart(String tunnelId, Socket tunnelSocket, int rate) {
bandwidthMap.put(tunnelId, rate);
bindAndStart(tunnelId, tunnelSocket);
}
public static void bindAndStart(String tunnelId, Socket tunnelSocket) {
Socket externalSocket = bridgeMap.remove(tunnelId);
int rate = bandwidthMap.remove(tunnelId);
if (externalSocket != null) {
try {
TokenBucket upload = new TokenBucket(rate, rate);
TokenBucket download = new TokenBucket(rate, rate);
InputStream fromClient = new RateLimitedInputStream(tunnelSocket.getInputStream(), upload);
OutputStream toExternal = new RateLimitedOutputStream(externalSocket.getOutputStream(), upload);
InputStream fromExternal = new RateLimitedInputStream(externalSocket.getInputStream(), download);
OutputStream toClient = new RateLimitedOutputStream(tunnelSocket.getOutputStream(), download);
new Thread(() -> copy(fromClient, toExternal)).start();
new Thread(() -> copy(fromExternal, toClient)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void copy(InputStream in, OutputStream out) {
try {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
out.flush();
}
} catch (IOException ignored) {
}
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端独立启动数据转发端口(如 9100)
*/
public class TunnelSocketListener {
public void start() throws Exception {
ServerSocket serverSocket = new ServerSocket(9100);
System.out.println("数据转发端口监听中: 9100");
while (true) {
Socket socket = serverSocket.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String tunnelId = reader.readLine();
TunnelBridgePool.bindAndStart(tunnelId, socket);
}
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 限速 InputStream 和 OutputStream 封装类
*/
public class RateLimitedInputStream extends FilterInputStream {
private final TokenBucket bucket;
public RateLimitedInputStream(InputStream in, TokenBucket bucket) {
super(in);
this.bucket = bucket;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
try {
bucket.consume(len);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
}
return super.read(b, off, len);
}
}
\ No newline at end of file
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class RateLimitedOutputStream extends FilterOutputStream {
private final TokenBucket bucket;
public RateLimitedOutputStream(OutputStream out, TokenBucket bucket) {
super(out);
this.bucket = bucket;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
try {
bucket.consume(len);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
super.write(b, off, len);
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.function.ratelimit;
/**
* 核心限速器
*/
public class TokenBucket {
private final long capacity;
private final long refillRate;
private long tokens;
private long lastRefillTime;
public TokenBucket(long capacity, long refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public synchronized void consume(long bytes) throws InterruptedException {
refill();
while (tokens < bytes) {
Thread.sleep(5);
refill();
}
tokens -= bytes;
}
private void refill() {
long now = System.nanoTime();
long elapsed = now - lastRefillTime;
long refillTokens = (elapsed * refillRate) / 1_000_000_000L;
if (refillTokens > 0) {
tokens = Math.min(capacity, tokens + refillTokens);
lastRefillTime = now;
}
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.zhonglai.luhui.neutrino.proxy.server.function.PortForwardManager;
import com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto.ResponseMessage;
import java.io.*;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 端口映射操作
*/
public class PortForwardHandler implements HttpHandler {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
try {
Map<String,String> params = getParamsMap(httpExchange);
String operate = params.get("operate");
switch (operate)
{
case "map": //端口映射
map(httpExchange,params);
return;
case "updateBandwidth": //限制带宽
updateBandwidth(httpExchange,params);
return;
default:
response(httpExchange,new ResponseMessage("不支持的操作", 0));
}
}catch (Exception e)
{
response(httpExchange,new ResponseMessage("接口请求失败", 0));
}
}
private void map(HttpExchange httpExchange, Map<String, String> params)throws IOException
{
int serverPort = Integer.parseInt(params.get("serverPort"));
String clientId =params.get("clientId");
int targetPort = Integer.parseInt(params.get("targetPort"));
int bandwidth = null != params.get("bandwidth") ? Integer.parseInt(params.get("bandwidth")) : 100 * 1024;
PortForwardManager.clientBandwidthMap.put(clientId, bandwidth);
new PortForwardManager().startPortForward(serverPort, clientId, targetPort);
response(httpExchange,new ResponseMessage("映射成功", 1));
}
private void updateBandwidth(HttpExchange httpExchange, Map<String, String> params)throws IOException
{
String clientId = params.get("clientId");
int bandwidth = Integer.parseInt(params.get("bandwidth"));
PortForwardManager.clientBandwidthMap.put(clientId, bandwidth);
response(httpExchange,new ResponseMessage("带宽更新成功", 1));
}
private void response(HttpExchange httpExchange, ResponseMessage responseMessage) throws IOException
{
String response = JSON.toJSONString(responseMessage);
OutputStream os = httpExchange.getResponseBody();
httpExchange.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8");
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
httpExchange.sendResponseHeaders(200, bytes.length);
os.write(bytes);
os.close();
}
private Map<String, String> getQueryParams(HttpExchange exchange) throws UnsupportedEncodingException {
String query = exchange.getRequestURI().getQuery(); // 获取 name=张三&age=18
return parseQuery(query);
}
private Map<String, String> parseQuery(String query) throws UnsupportedEncodingException {
Map<String, String> result = new HashMap<>();
if (query == null || query.isEmpty()) return result;
for (String param : query.split("&")) {
String[] pair = param.split("=", 2);
if (pair.length == 2) {
result.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8.toString()),URLDecoder.decode(pair[1], StandardCharsets.UTF_8.toString()));
} else if (pair.length == 1) {
result.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8.toString()), "");
}
}
return result;
}
private Map<String, String> getParamsMap(HttpExchange httpExchange) throws IOException {
String method = httpExchange.getRequestMethod();
Map<String, String> params = null;
if ("POST".equalsIgnoreCase(method))
{
params = getPostParams(httpExchange);
} else if ("GET".equalsIgnoreCase(method))
{
params = getQueryParams(httpExchange);
}
return params;
}
private Map<String, String> getPostParams(HttpExchange exchange) throws IOException {
String body = readRequestBody(exchange);
return parseQuery(body); // 和 GET 参数一样解析
}
private String readRequestBody(HttpExchange exchange) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
public class SimpleHttpServer {
public static void startHttpServer(int port) throws IOException
{
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
// 映射 /static/ 到 static/ 目录
server.createContext("/static/", new StaticFileHandler("static"));
server.createContext("/portForward", new PortForwardHandler());
server.setExecutor(null); // 默认线程池
server.start();
System.out.println("Server started at http://localhost:" + port);
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 静态文件处理器
*/
public class StaticFileHandler implements HttpHandler {
private final Path basePath;
public StaticFileHandler(String rootDir) {
this.basePath = Paths.get(rootDir).toAbsolutePath();
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String uriPath = exchange.getRequestURI().getPath();
String other = uriPath.replaceFirst("^/[^/]+/", "");
Path filePath = basePath.resolve(other).normalize();
//生成
if (!filePath.startsWith(basePath) || !Files.exists(filePath)) {
exchange.sendResponseHeaders(404, -1);
return;
}
String contentType = guessContentType(filePath);
byte[] fileBytes = Files.readAllBytes(filePath);
exchange.getResponseHeaders().add("Content-Type", contentType);
exchange.sendResponseHeaders(200, fileBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(fileBytes);
}
}
private String guessContentType(Path path) {
String name = path.getFileName().toString().toLowerCase();
if (name.endsWith(".html")) return "text/html";
if (name.endsWith(".js")) return "application/javascript";
if (name.endsWith(".css")) return "text/css";
if (name.endsWith(".m3u8")) return "application/vnd.apple.mpegurl";
if (name.endsWith(".ts")) return "video/MP2T";
return "application/octet-stream";
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
public class MappingPort {
private Integer clientPort; //客服端端口
private Integer servicePort; //服务器端口
public Integer getClientPort() {
return clientPort;
}
public void setClientPort(Integer clientPort) {
this.clientPort = clientPort;
}
public Integer getServicePort() {
return servicePort;
}
public void setServicePort(Integer servicePort) {
this.servicePort = servicePort;
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
import java.util.List;
import java.util.Map;
/**
* 端口映射内容
*/
public class PortForwardMapBody {
private String clientId;
private List<MappingPort> mappingPortList;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public List<MappingPort> getMappingPortList() {
return mappingPortList;
}
public void setMappingPortList(List<MappingPort> mappingPortList) {
this.mappingPortList = mappingPortList;
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.httpservice.dto;
public class ResponseMessage {
private String msg;
private Integer code;
private Object data;
public ResponseMessage(String msg, Integer code) {
this.msg = msg;
this.code = code;
}
public ResponseMessage(String msg, Integer code, Object data) {
this.msg = msg;
this.code = code;
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.proxy;
import com.zhonglai.luhui.neutrino.proxy.server.proxy.handler.ControlHandler;
import com.zhonglai.luhui.neutrino.proxy.server.proxy.handler.HeartbeatTimeoutHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
/**
* 代理服务
*/
public class ProxyServer {
public static void start(int proxyPort) throws Exception
{
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new IdleStateHandler(60, 0, 0));
ch.pipeline().addLast(new HeartbeatTimeoutHandler());
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ControlHandler());
}
});
ChannelFuture future = bootstrap.bind(proxyPort).sync(); // bind 完成阻塞等待
future.channel().closeFuture().addListener((ChannelFutureListener) closeFuture -> {
System.out.println("Server channel closed.");
});
System.out.println("Netty server started on port: " + proxyPort);
// ✅ 注册优雅关闭钩子,不阻塞主线程
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown signal received. Closing Netty gracefully...");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}));
}
}
... ...
package com.zhonglai.luhui.neutrino.proxy.server.proxy.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhonglai.luhui.neutrino.proxy.common.RegisterMessage;
import com.zhonglai.luhui.neutrino.proxy.server.ClientSessionManager;
import com.zhonglai.luhui.neutrino.proxy.server.function.PortForwardManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
public class ControlHandler extends SimpleChannelInboundHandler<String> {
private static final ObjectMapper mapper = new ObjectMapper();
private String clientId;
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
JsonNode json = mapper.readTree(msg);
String type = json.get("type").asText();
switch (type) {
case "register":
clientId = json.get("clientId").asText();
ClientSessionManager.register(clientId, ctx.channel());
ctx.writeAndFlush("{\"type\":\"register_ack\",\"status\":\"ok\"}\n");
break;
case "heartbeat":
ClientSessionManager.heartbeat(json.get("clientId").asText());
break;
default:
System.out.println("未知消息类型: " + type);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ClientSessionManager.remove(ctx.channel());
System.out.println("客户端断开连接:" + clientId);
}
}
\ No newline at end of file
... ...
package com.zhonglai.luhui.neutrino.proxy.server.proxy.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class HeartbeatTimeoutHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent && ((IdleStateEvent)evt).state() == IdleState.READER_IDLE) {
System.out.println("心跳超时,断开客户端:" + ctx.channel().remoteAddress());
ctx.close();
} else {
super.userEventTriggered(ctx, evt);
}
}
}
\ No newline at end of file
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-modules</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>lh-neutrino-proxy</artifactId>
<modules>
<module>lh-neutrino-proxy-server</module>
<module>lh-neutrino-proxy-client</module>
<module>lh-neutrino-proxy-common</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<packaging>pom</packaging>
<description>
透传代理
</description>
</project>
\ No newline at end of file
... ...
... ... @@ -38,6 +38,7 @@
<module>lh-ssh-service-lesten</module>
<module>lh-deviceInfo-sync</module>
<module>lh-camera</module>
<module>lh-neutrino-proxy</module>
</modules>
<properties>
... ...
... ... @@ -369,6 +369,11 @@
<version>${ruoyi.version}</version>
</dependency>
<dependency>
<groupId>com.zhonglai.luhui</groupId>
<artifactId>lh-neutrino-proxy-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<dependency>
<groupId>com.zhonglai</groupId>
<artifactId>ServiceDao</artifactId>
<version>1.4.3</version>
... ... @@ -603,6 +608,13 @@
<artifactId>httpclient5</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
<version>1.18</version>
</dependency>
</dependencies>
... ...