|
|
@@ -0,0 +1,420 @@
|
|
|
+package com.jttserver.service;
|
|
|
+
|
|
|
+import io.netty.bootstrap.ServerBootstrap;
|
|
|
+import io.netty.buffer.Unpooled;
|
|
|
+import io.netty.channel.*;
|
|
|
+import io.netty.channel.nio.NioEventLoopGroup;
|
|
|
+import io.netty.channel.socket.SocketChannel;
|
|
|
+import io.netty.channel.socket.nio.NioServerSocketChannel;
|
|
|
+import io.netty.handler.codec.http.*;
|
|
|
+import io.netty.handler.stream.ChunkedWriteHandler;
|
|
|
+import io.netty.util.CharsetUtil;
|
|
|
+
|
|
|
+import java.net.InetSocketAddress;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+import com.jttserver.config.ConfigManager;
|
|
|
+import com.jttserver.device.DeviceManager;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+public class ManagerWebServer {
|
|
|
+
|
|
|
+ private static final Logger logger = LoggerFactory.getLogger(ManagerWebServer.class);
|
|
|
+
|
|
|
+ private final int port = Integer.parseInt(ConfigManager.get("server.manager.port", "8099"));
|
|
|
+
|
|
|
+ private EventLoopGroup bossGroup;
|
|
|
+ private EventLoopGroup workerGroup;
|
|
|
+ private Channel webServerChannel;
|
|
|
+
|
|
|
+ // 设备管理功能开关状态(初始化时获取,避免频繁调用ConfigManager)
|
|
|
+ private final boolean deviceManagementEnabled = ConfigManager.isDeviceManagementEnabled();
|
|
|
+
|
|
|
+ public ManagerWebServer() {
|
|
|
+ // 检查资源文件是否存在(仅在功能启用时检查)
|
|
|
+ if (deviceManagementEnabled) {
|
|
|
+ checkResources();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查资源文件是否存在
|
|
|
+ */
|
|
|
+ private void checkResources() {
|
|
|
+ String[] resourceFiles = { "devices.html", "player.html", "playerWhthoutAudio.html", "offline_player.html", "mpegts.js", "jessibuca/demo.html", "jessibuca/jessibuca.js" };
|
|
|
+ for (String file : resourceFiles) {
|
|
|
+ try (InputStream inputStream = ManagerWebServer.class.getClassLoader().getResourceAsStream("web/" + file)) {
|
|
|
+ if (inputStream == null) {
|
|
|
+ logger.warn("找不到资源文件: {}", file);
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ logger.warn("无法访问资源文件: {}", file, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动Web服务器
|
|
|
+ */
|
|
|
+ public void start() throws InterruptedException {
|
|
|
+ // 检查功能开关
|
|
|
+ if (!deviceManagementEnabled) {
|
|
|
+ logger.info("Web管理界面服务器功能未启用");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ bossGroup = new NioEventLoopGroup(1);
|
|
|
+ workerGroup = new NioEventLoopGroup();
|
|
|
+
|
|
|
+ ServerBootstrap bootstrap = new ServerBootstrap();
|
|
|
+ bootstrap.group(bossGroup, workerGroup)
|
|
|
+ .channel(NioServerSocketChannel.class)
|
|
|
+ .localAddress(new InetSocketAddress(port))
|
|
|
+ .childHandler(new ChannelInitializer<SocketChannel>() {
|
|
|
+ @Override
|
|
|
+ protected void initChannel(SocketChannel ch) throws Exception {
|
|
|
+ ch.pipeline().addLast(new HttpServerCodec());
|
|
|
+ ch.pipeline().addLast(new HttpObjectAggregator(65536));
|
|
|
+ ch.pipeline().addLast(new ChunkedWriteHandler());
|
|
|
+ ch.pipeline().addLast(new WebRequestHandler(deviceManagementEnabled));
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ChannelFuture future = bootstrap.bind().sync();
|
|
|
+ webServerChannel = future.channel();
|
|
|
+
|
|
|
+ logger.info("Web管理界面服务器启动,监听端口: {}", port);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 停止Web服务器
|
|
|
+ */
|
|
|
+ public void stop() {
|
|
|
+ // 检查功能开关
|
|
|
+ if (!ConfigManager.isDeviceManagementEnabled()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (webServerChannel != null) {
|
|
|
+ webServerChannel.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bossGroup != null) {
|
|
|
+ bossGroup.shutdownGracefully();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (workerGroup != null) {
|
|
|
+ workerGroup.shutdownGracefully();
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("Web管理界面服务器已停止");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 兼容Java 8的readAllBytes实现
|
|
|
+ */
|
|
|
+ private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
|
|
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
|
+ int nRead;
|
|
|
+ byte[] data = new byte[4096];
|
|
|
+ while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
|
|
|
+ buffer.write(data, 0, nRead);
|
|
|
+ }
|
|
|
+ return buffer.toByteArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Web请求处理器
|
|
|
+ */
|
|
|
+ public static class WebRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
|
|
+ // 设备管理功能开关状态(避免频繁调用ConfigManager)
|
|
|
+ private final boolean deviceManagementEnabled;
|
|
|
+
|
|
|
+ public WebRequestHandler(boolean deviceManagementEnabled) {
|
|
|
+ this.deviceManagementEnabled = deviceManagementEnabled;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
|
|
|
+ // 检查功能开关
|
|
|
+ if (!deviceManagementEnabled) {
|
|
|
+ handleNotFound(ctx);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String uri = request.uri();
|
|
|
+
|
|
|
+ // 移除查询参数部分,只保留路径部分
|
|
|
+ int queryIndex = uri.indexOf('?');
|
|
|
+ if (queryIndex != -1) {
|
|
|
+ uri = uri.substring(0, queryIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认路径和 /devices.html 都显示设备详情页面
|
|
|
+ if ("/".equals(uri) || "/devices.html".equals(uri) || "/index.html".equals(uri)) {
|
|
|
+ handleDevicesPage(ctx);
|
|
|
+ } else if ("/player.html".equals(uri)) {
|
|
|
+ handlePlayerPage(ctx);
|
|
|
+ } else if ("/playerWhthoutAudio.html".equals(uri)) {
|
|
|
+ handlePlayerWithoutAudioPage(ctx);
|
|
|
+ } else if ("/offline_player.html".equals(uri)) {
|
|
|
+ handleOfflinePlayerPage(ctx);
|
|
|
+ } else if ("/mpegts.js".equals(uri)) {
|
|
|
+ handleMpegtsJs(ctx);
|
|
|
+ } else if (uri.startsWith("/jessibuca/")) {
|
|
|
+ handleJessibucaFile(ctx, uri);
|
|
|
+ } else if ("/devices".equals(uri)) {
|
|
|
+ handleDevicesApi(ctx);
|
|
|
+ } else {
|
|
|
+ handleNotFound(ctx);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从资源文件中读取HTML内容
|
|
|
+ */
|
|
|
+ private static String loadHtmlFile(String fileName) throws IOException {
|
|
|
+ try (InputStream inputStream = ManagerWebServer.class.getClassLoader()
|
|
|
+ .getResourceAsStream("web/" + fileName)) {
|
|
|
+ if (inputStream == null) {
|
|
|
+ throw new IOException("无法找到资源文件: " + fileName);
|
|
|
+ }
|
|
|
+ return new String(readAllBytes(inputStream), StandardCharsets.UTF_8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理设备详情页面请求
|
|
|
+ */
|
|
|
+ private void handleDevicesPage(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ String html = loadHtmlFile("devices.html");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理播放器测试页面请求
|
|
|
+ */
|
|
|
+ private void handlePlayerPage(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ String html = loadHtmlFile("player.html");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理无音频播放器测试页面请求
|
|
|
+ */
|
|
|
+ private void handlePlayerWithoutAudioPage(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ String html = loadHtmlFile("playerWhthoutAudio.html");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理离线播放器页面请求
|
|
|
+ */
|
|
|
+ private void handleOfflinePlayerPage(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ String html = loadHtmlFile("offline_player.html");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理 mpegts.js 静态资源请求
|
|
|
+ */
|
|
|
+ private void handleMpegtsJs(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ byte[] js = loadStaticFile("mpegts.js");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.wrappedBuffer(js));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/javascript; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从资源文件中读取静态二进制内容(例如 mpegts.js)
|
|
|
+ */
|
|
|
+ private static byte[] loadStaticFile(String fileName) throws IOException {
|
|
|
+ try (InputStream inputStream = ManagerWebServer.class.getClassLoader()
|
|
|
+ .getResourceAsStream("web/" + fileName)) {
|
|
|
+ if (inputStream == null) {
|
|
|
+ throw new IOException("无法找到资源文件: " + fileName);
|
|
|
+ }
|
|
|
+ return readAllBytes(inputStream);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将设备信息转换为JSON格式
|
|
|
+ */
|
|
|
+ private String deviceToJson(Map<String, Object> device) {
|
|
|
+ return "{" +
|
|
|
+ "\"channelId\":\"" + device.get("channelId") + "\"," +
|
|
|
+ "\"remoteAddress\":\"" + device.get("remoteAddress") + "\"," +
|
|
|
+ "\"simCardNumber\":\"" + (device.get("simCardNumber") != null ? device.get("simCardNumber") : "")
|
|
|
+ + "\"," +
|
|
|
+ "\"logicChannelNumber\":" + device.get("logicChannelNumber") + "," +
|
|
|
+ "\"connectTime\":" + device.get("connectTime") + "," +
|
|
|
+ "\"lastActiveTime\":" + device.get("lastActiveTime") +
|
|
|
+ "}";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理设备API请求
|
|
|
+ */
|
|
|
+ private void handleDevicesApi(ChannelHandlerContext ctx) {
|
|
|
+ Collection<DeviceManager.DeviceInfo> devices = DeviceManager.getConnectedDevices();
|
|
|
+ List<Map<String, Object>> deviceList = new ArrayList<>();
|
|
|
+
|
|
|
+ for (DeviceManager.DeviceInfo device : devices) {
|
|
|
+ Map<String, Object> deviceMap = new HashMap<>();
|
|
|
+ deviceMap.put("channelId", device.getChannelId());
|
|
|
+ deviceMap.put("remoteAddress", device.getRemoteAddress());
|
|
|
+ deviceMap.put("simCardNumber", device.getSimCardNumber());
|
|
|
+ deviceMap.put("logicChannelNumber", device.getLogicChannelNumber());
|
|
|
+ deviceMap.put("connectTime", device.getConnectTime());
|
|
|
+ deviceMap.put("lastActiveTime", device.getLastActiveTime());
|
|
|
+ deviceList.add(deviceMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ String jsonResponse = "[" + deviceList.stream()
|
|
|
+ .map(this::deviceToJson)
|
|
|
+ .reduce((a, b) -> a + "," + b)
|
|
|
+ .orElse("") + "]";
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.copiedBuffer(jsonResponse, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理404错误
|
|
|
+ */
|
|
|
+ private void handleNotFound(ChannelHandlerContext ctx) throws IOException {
|
|
|
+ // 如果功能未启用或资源文件不存在,返回简单404响应
|
|
|
+ try {
|
|
|
+ String html = loadHtmlFile("404.html");
|
|
|
+
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.NOT_FOUND,
|
|
|
+ Unpooled.copiedBuffer(html, CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ } catch (IOException e) {
|
|
|
+ // 如果无法加载notfound.html,则返回简单文本响应
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.NOT_FOUND,
|
|
|
+ Unpooled.copiedBuffer("404 - 页面未找到", CharsetUtil.UTF_8));
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
|
+
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理jessibuca目录下的文件请求
|
|
|
+ */
|
|
|
+ private void handleJessibucaFile(ChannelHandlerContext ctx, String uri) throws IOException {
|
|
|
+ // 提取jessibuca目录下的文件名部分
|
|
|
+ String fileName = uri.substring(uri.indexOf("/jessibuca/") + "/jessibuca/".length());
|
|
|
+ String resourcePath = "web/jessibuca/" + fileName;
|
|
|
+
|
|
|
+ try (InputStream inputStream = ManagerWebServer.class.getClassLoader().getResourceAsStream(resourcePath)) {
|
|
|
+ if (inputStream == null) {
|
|
|
+ handleNotFound(ctx);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ byte[] content = readAllBytes(inputStream);
|
|
|
+ FullHttpResponse response = new DefaultFullHttpResponse(
|
|
|
+ HttpVersion.HTTP_1_1,
|
|
|
+ HttpResponseStatus.OK,
|
|
|
+ Unpooled.wrappedBuffer(content));
|
|
|
+
|
|
|
+ // 根据文件扩展名设置内容类型
|
|
|
+ if (fileName.endsWith(".js")) {
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/javascript; charset=UTF-8");
|
|
|
+ } else if (fileName.endsWith(".html")) {
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
|
|
|
+ } else if (fileName.endsWith(".jpg")) {
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "image/jpeg");
|
|
|
+ } else if (fileName.endsWith(".png")) {
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "image/png");
|
|
|
+ } else {
|
|
|
+ response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
|
|
|
+ }
|
|
|
+
|
|
|
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, content.length);
|
|
|
+ ctx.writeAndFlush(response);
|
|
|
+ } catch (IOException e) {
|
|
|
+ logger.error("处理jessibuca文件失败: {}", uri, e);
|
|
|
+ handleNotFound(ctx);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
|
+ // 检查是否为连接中断异常
|
|
|
+ if (cause instanceof IOException &&
|
|
|
+ (cause.getMessage().contains("An established connection was aborted") ||
|
|
|
+ cause.getMessage().contains("软件中止了一个已建立的连接"))) {
|
|
|
+ logger.info("Web客户端主动断开连接: {}", ctx.channel().remoteAddress());
|
|
|
+ } else {
|
|
|
+ logger.error("Web请求处理异常", cause);
|
|
|
+ }
|
|
|
+ ctx.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|