基于 SpringBoot 实现一个 JAVA 代理 HTTP / WS

发布于:2025-07-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

1.环境信息

组件 版本
JDK 21
Springboot 3.5.3
netty-all 4.1.108.Final
smiley-http-proxy-servlet 2.0

2.项目结构和代码

2.1 项目结构

在这里插入图片描述

2.2 依赖包

<?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>

    <groupId>com.demo</groupId>
    <artifactId>ProxyAPP</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.5.3</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.108.Final</version>
        </dependency>
        <dependency>
            <groupId>org.mitre.dsmiley.httpproxy</groupId>
            <artifactId>smiley-http-proxy-servlet</artifactId>
            <version>2.0</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>my-proxy</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

2.3 代理配置类

启动类

package com.demo;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

/**
 * @author zhx && moon
 */
@EnableWebSocket
@SpringBootApplication
public class ProxyApp {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApp.class, args);
    }
}

2.3.1 HTTP 代理

用于 HTTP 代理转发

package com.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2024-12-12 PM 2:27
 */
@Configuration
public class ProxyConfig {

    @Value("${proxy.target.url}")
    private String targetUri;

    @Bean
    public ServletRegistrationBean proxyServlet() {
        ServletRegistrationBean servlet = new ServletRegistrationBean(new URITemplateProxyServletSUB());
        servlet.addUrlMappings("/*");
        servlet.addInitParameter("targetUri", targetUri);
        return servlet;
    }

}

转发参数处理类,如设置头、添加权限控制、转发记录等操作

package com.demo.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.config.SocketConfig;
import org.mitre.dsmiley.httpproxy.URITemplateProxyServlet;

import java.io.IOException;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2025-02-26 PM 5:44
 */
public class URITemplateProxyServletSUB extends URITemplateProxyServlet {

    @Override
    protected SocketConfig buildSocketConfig() {
        return SocketConfig.custom()
                .setSoTimeout(3600 * 1000)
                .setSoKeepAlive(true)
                .build();
    }

    @Override
    protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse, HttpRequest proxyRequest) throws IOException {
        //重置请求头
        proxyRequest.setHeader("Connection", "keep-alive");
        //调用父方法
        return super.doExecute(servletRequest, servletResponse, proxyRequest);
    }

}

2. WS 代理

基于 NETTY 实现 WS 协议,完成转发

NETTY 服务端定义

package com.demo.ws;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
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.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2025-02-28 PM 3:28
 */
@Component
public class WebSocketServer {

    Logger logger = LoggerFactory.getLogger(WebSocketServer.class);

    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

    @Value("${server.websocket.is-ws:true}")
    private boolean isWS;

    @Value("${server.websocket.port}")
    private int websocketPort;

    @Value("${server.websocket.path}")
    private String websocketPath;

    @Value("${server.websocket.remote-uri}")
    private String remoteUri;

    /**
     * 初始化本地 WS 服务
     * @throws Exception
     */
    @PostConstruct
    private void init() throws Exception {
        if (isWS){
            // 创建线程
            Thread thread = new Thread(() -> {
                try {
                    run();
                } catch (Exception e) {
                    logger.error("WebSocketServer init error: {}", e.getMessage());
                }
            });
            // 启动线程
            thread.setDaemon(true);
            thread.setName("WebSocketServer-Proxy");
            thread.start();
        }
    }

    /**
     * 构建本地 WS 服务
     * @throws Exception
     */
    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline = ch.pipeline();
                            // 添加 HTTP 编解码器
                            pipeline.addLast(new HttpServerCodec());
                            // 添加 HTTP 内容聚合器
                            pipeline.addLast(new HttpObjectAggregator(65536));
                            // 添加 WebSocket 处理器
                            pipeline.addLast(new WebSocketServerProtocolHandler(websocketPath));
                            // 添加自定义处理器
                            pipeline.addLast(new WebSocketProxyHandler(remoteUri));
                        }
                    });

            // 绑定端口并启动服务器
            Channel channel = bootstrap.bind(websocketPort).sync().channel();
            // 输出启动信息
            logger.info("webSocket server started on port {}", websocketPort);
            // 等待服务器 socket 关闭
            channel.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

WS 处理器

package com.demo.ws;

import com.demo.ws.remote.WebSocketClientConnector;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2025-02-28 PM 3:29
 */
public class WebSocketProxyHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    Logger logger = LoggerFactory.getLogger(WebSocketProxyHandler.class);

    /**
     * 远端 WS 服务连接器
     */
    private final WebSocketClientConnector connector;

    /**
     * 远端 WS 服务会话
     */
    private WebSocketSession session;

    private AtomicReference<ChannelHandlerContext> ctx = new AtomicReference<>();

    /**
     * 构造 WS 消息处理器
     * @param uri
     */
    public WebSocketProxyHandler(String uri) {
        //添加目标服务
        connector = new WebSocketClientConnector(uri, ctx);
        //建立连接
        try {
            connector.connect();
        } catch (Exception e) {
            logger.info("connect to remote server failed", e);
        }
    }

    /**
     * 处理接收到的消息,并转发到远端服务
     * @param ctx
     * @param frame
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        if (frame instanceof TextWebSocketFrame) {
            // 处理文本帧
            String text = ((TextWebSocketFrame) frame).text();
            logger.info("proxy received text message: {}", text);
            // 获取 SESSION
            if (Objects.isNull(this.ctx.get())){
                this.ctx.set(ctx);
                this.session = connector.getRemoteSession();
            }
            // 转发到远端服务
            if (Objects.nonNull(this.session)){
                session.sendMessage(new TextMessage(text));
            } else {
                this.ctx.get().writeAndFlush(new TextWebSocketFrame("remote server connection failed!"));
            }
        } else if (frame instanceof BinaryWebSocketFrame) {
            // 处理二进制帧
            logger.info("received binary message");
        } else if (frame instanceof CloseWebSocketFrame) {
            // 处理关闭帧
            ctx.close();
        }
    }

    /**
     * 链接关闭处理
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 关闭远端连接
        connector.close();
        // 记录日志
        logger.info("client disconnected: {}", ctx.channel().remoteAddress());
    }

    /**
     * 异常处理
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("proxy exception caught", cause);
        ctx.close();
    }

}

NETTY 客户端,用于连接第三方 WS

package com.demo.ws.remote;

import io.netty.channel.ChannelHandlerContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.WebSocketConnectionManager;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2025-02-28 PM 6:27
 */
public class WebSocketClientConnector {

    Logger logger = LoggerFactory.getLogger(WebSocketClientConnector.class);

    private final String uri;
    private final AtomicReference<ChannelHandlerContext> ctx;
    private final WebSocketClient webSocketClient = new StandardWebSocketClient();
    private final WebSocketClientHandler webSocketClientHandler;
    private WebSocketConnectionManager connectionManager;

    /**
     * 构建访问远端 WS 服务的本地客户端
     * @param uri
     * @param ctx
     */
    public WebSocketClientConnector(String uri, AtomicReference<ChannelHandlerContext> ctx) {
        this.uri = uri;
        this.ctx = ctx;
        this.webSocketClientHandler = new WebSocketClientHandler(ctx);
    }

    /**
     * 连接远端服务
     */
    public void connect() {
        // 创建 WebSocket 连接管理器
        this.connectionManager = new WebSocketConnectionManager(
                webSocketClient,
                webSocketClientHandler,
                uri
        );
        // 启动连接
        this.connectionManager.start();
        // 记录日志
        logger.info("web socket client started and connecting to: {}", uri);
    }

    /**
     * 关闭连接
     */
    public void close(){
        this.connectionManager.stop();
    }

    /**
     * 获取与远端的会话 SESSION
     * @return
     */
    public WebSocketSession getRemoteSession(){
        if (Objects.nonNull(webSocketClientHandler.getSession())) {
            // 发送一条消息到服务器
            return webSocketClientHandler.getSession();
        }
        return null;
    }
}

WS 客户端处理器

package com.demo.ws.remote;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @Author zhx && moon
 * @Since 1.8
 * @Date 2025-02-28 PM 6:06
 */
public class WebSocketClientHandler extends TextWebSocketHandler {

    Logger logger = LoggerFactory.getLogger(WebSocketClientHandler.class);

    /**
     * WebSocket 会话
     */
    private WebSocketSession session;

    /**
     * 本地 WS 服务的 NIO 通道上下文
     */
    private final AtomicReference<ChannelHandlerContext> ctx;

    /**
     * 构造 WS 客户端消息处理器, 获取本第 WS 服务的 NIO 通道上下文
     * @param ctx
     */
    public WebSocketClientHandler(AtomicReference<ChannelHandlerContext> ctx) {
        this.ctx = ctx;
    }

    /**
     * 建立连接后操作
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 连接建立成功
        logger.info("connected to web socket server: {}", session.getUri());
        // Save the session
        this.session = session;
    }

    /**
     * 消息处理,接收远端服务
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理从服务器收到的消息
        logger.info("received message: {}", message.getPayload());
        // 转发到本地 WS 服务,并回写给其他连接
        if (Objects.nonNull(this.ctx.get())){
            this.ctx.get().writeAndFlush(new TextWebSocketFrame(message.getPayload()));
        }
    }

    /**
     * 连接关闭后处理
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        logger.info("disconnected from web socket server: {}", status.getReason());
    }

    /**
     * 报错处理
     * @param session
     * @param exception
     * @throws Exception
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        logger.error("web socket transport error: ", exception);
    }

    /**
     * 获取到远端服务的 WebSocket 会话
     * @return
     */
    public WebSocketSession getSession(){
        return this.session;
    }
}

2.4 YML 配置文件

server:
  port: 8081
  websocket:
    is-ws: true
    port: 8082
    path: /ws/conn
    remote-uri: ws://127.0.0.1:8080/ws/conn

proxy.target.url: http://127.0.0.1:8080

3.测试用第三方服务示例

3.1 项目结构

在这里插入图片描述

依赖包

<?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>

    <groupId>org.example</groupId>
    <artifactId>WebTest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.5.3</spring-boot.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>my-proxy</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

3.2 代码

启动类

package com.demo;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

/**
 * @author zhx && moon
 */
@EnableWebSocket
@SpringBootApplication
public class ProxyApp {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApp.class, args);
    }
}

HTTP 接口类

package org.example.controller;

import org.springframework.web.bind.annotation.*;

/**
 * @author zhuwd && moon
 * @Description
 * @create 2025-06-29 12:41
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    public String get(@RequestParam("params") String params){
        return "Hello " + params;
    }

    @PostMapping("/post")
    public String post(@RequestBody String params){
        return "Hello " + params;
    }

}

WS 配置

package org.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * @author zhuwd && moon
 * @Description
 * @create 2025-06-29 12:58
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler(), "/ws/conn")
                .setAllowedOrigins("*"); // 允许所有来源
    }

    public ServerWebSocketHandler webSocketHandler() {
        return new ServerWebSocketHandler();
    }
}

WS 处理器,将消息转大写返回

package org.example.config;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author zhuwd && moon
 * @Description
 * @create 2025-06-29 14:31
 */
public class ServerWebSocketHandler extends TextWebSocketHandler {

    // 存储所有活动会话
    private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    // 消息计数
    private static int messageCount = 0;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.put(session.getId(), session);
        System.out.println("连接建立: " + session.getId());

        // 向新连接的客户端发送欢迎消息
        session.sendMessage(new TextMessage(
                "{\"type\": \"system\", \"message\": \"连接服务器成功!发送 'broadcast' 可以广播消息\"}"
        ));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("收到消息 [" + session.getId() + "]: " + payload);
        messageCount++;

        // 处理特殊指令
        if ("broadcast".equalsIgnoreCase(payload)) {
            broadcastMessage("来自服务器的广播消息 (" + messageCount + ")");
        } else {
            // 默认回显消息
            String response = "{\"type\": \"echo\", \"original\": \"" +
                    escapeJson(payload) +
                    "\", \"modified\": \"" +
                    payload.toUpperCase() +
                    "\"}";
            session.sendMessage(new TextMessage(response));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session.getId());
        System.out.println("连接关闭: " + session.getId() + ", 状态: " + status);
    }

    // 广播消息给所有客户端
    private void broadcastMessage(String message) throws IOException {
        TextMessage msg = new TextMessage("{\"type\": \"broadcast\", \"message\": \"" + escapeJson(message) + "\"}");
        for (WebSocketSession session : sessions.values()) {
            if (session.isOpen()) {
                session.sendMessage(msg);
            }
        }
    }

    // 处理JSON转义
    private String escapeJson(String str) {
        return str.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\b", "\\b")
                .replace("\f", "\\f")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }
}

4.测试

HTML 实现一个 WS 客户端

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket测试工具</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #2b5876, #4e4376);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        
        header {
            text-align: center;
            padding: 20px 0;
            margin-bottom: 30px;
        }
        
        h1 {
            font-size: 2.5rem;
            background: linear-gradient(to right, #00d2ff, #3a7bd5);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            text-shadow: 0 2px 4px rgba(0,0,0,0.2);
        }
        
        .subtitle {
            color: #bbd4ff;
            margin-top: 10px;
            font-weight: 300;
        }
        
        .panel {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 25px;
            margin-bottom: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
            border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .connection-panel {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
        }
        
        .connection-status {
            display: flex;
            align-items: center;
        }
        
        .status-indicator {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            margin-right: 10px;
            background-color: #6c757d;
        }
        
        .status-connected {
            background-color: #28a745;
            box-shadow: 0 0 10px #28a745;
        }
        
        .status-disconnected {
            background-color: #dc3545;
        }
        
        .connection-controls {
            display: flex;
            gap: 15px;
        }
        
        input {
            padding: 12px 15px;
            border: none;
            border-radius: 8px;
            background: rgba(255, 255, 255, 0.1);
            color: #fff;
            font-size: 1rem;
            width: 100%;
            outline: none;
        }
        
        input:focus {
            background: rgba(255, 255, 255, 0.15);
            box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
        }
        
        button {
            padding: 12px 25px;
            border: none;
            border-radius: 8px;
            background: linear-gradient(to right, #2193b0, #6dd5ed);
            color: white;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            outline: none;
        }
        
        button:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }
        
        button:active {
            transform: translateY(0);
        }
        
        button:disabled {
            background: linear-gradient(to right, #5a6268, #6c757d);
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        
        .disconnect-btn {
            background: linear-gradient(to right, #f85032, #e73827);
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        label {
            display: block;
            margin-bottom: 8px;
            color: #b3d7ff;
        }
        
        .log-container {
            height: 400px;
            overflow-y: auto;
            background: rgba(0, 0, 0, 0.2);
            border-radius: 10px;
            padding: 15px;
            font-family: monospace;
            font-size: 0.9rem;
            margin-bottom: 15px;
        }
        
        .log-entry {
            margin-bottom: 8px;
            padding: 8px;
            border-radius: 5px;
            background: rgba(0, 0, 0, 0.1);
        }
        
        .incoming {
            border-left: 4px solid #4db8ff;
        }
        
        .outgoing {
            border-left: 4px solid #28a745;
        }
        
        .system {
            border-left: 4px solid #ffc107;
        }
        
        .error {
            border-left: 4px solid #dc3545;
            color: #ffabab;
        }
        
        .timestamp {
            color: #999;
            font-size: 0.8rem;
            margin-right: 10px;
        }
        
        .flex-container {
            display: flex;
            gap: 20px;
        }
        
        .flex-container > div {
            flex: 1;
        }
        
        .info-container {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }
        
        .info-box {
            background: rgba(0, 0, 0, 0.15);
            padding: 15px;
            border-radius: 10px;
        }
        
        .info-box h3 {
            margin-bottom: 10px;
            color: #80bdff;
            font-weight: 500;
        }
        
        .code-example {
            background: rgba(0, 0, 0, 0.2);
            padding: 15px;
            border-radius: 10px;
            margin-top: 30px;
            font-family: monospace;
            font-size: 0.9rem;
            color: #e9ecef;
            overflow-x: auto;
        }
        
        @media (max-width: 768px) {
            .flex-container {
                flex-direction: column;
            }
            
            .connection-controls {
                width: 100%;
                margin-top: 15px;
            }
            
            .connection-panel {
                flex-direction: column;
                align-items: flex-start;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>WebSocket 测试工具</h1>
            <p class="subtitle">测试、调试和监控您的WebSocket连接</p>
        </header>
        
        <div class="panel connection-panel">
            <div class="connection-status">
                <div class="status-indicator" id="statusIndicator"></div>
                <span id="statusText">未连接</span>
            </div>
            <div class="connection-controls">
                <input type="text" id="serverUrl" placeholder="ws://127.0.0.1:8082/ws/conn" value="ws://127.0.0.1:8082/ws/conn">
                <button id="connectBtn" class="connect-btn">连接</button>
                <button id="disconnectBtn" class="disconnect-btn" disabled>断开</button>
            </div>
        </div>
        
        <div class="flex-container">
            <div>
                <div class="panel">
                    <h2>发送消息</h2>
                    <div class="form-group">
                        <label for="messageInput">输入要发送的消息:</label>
                        <input type="text" id="messageInput" placeholder="输入消息内容..." disabled>
                    </div>
                    <button id="sendBtn" disabled>发送消息</button>
                </div>
                
                <div class="panel">
                    <h2>连接信息</h2>
                    <div class="info-container">
                        <div class="info-box">
                            <h3>当前状态</h3>
                            <p id="currentState">未连接</p>
                        </div>
                        <div class="info-box">
                            <h3>传输协议</h3>
                            <p id="protocol">-</p>
                        </div>
                        <div class="info-box">
                            <h3>消息计数</h3>
                            <p id="messageCount">已发送: 0 | 已接收: 0</p>
                        </div>
                    </div>
                </div>
            </div>
            
            <div>
                <div class="panel">
                    <h2>通信日志</h2>
                    <div class="log-container" id="logContainer"></div>
                    <button id="clearLogBtn">清除日志</button>
                </div>
            </div>
        </div>
        
        <div class="panel code-example">
            <h3>示例服务器URL:</h3>
            <p>本地测试服务器: ws://localhost:8082/ws/conn</p>
            <p>SSL测试服务器: wss://websocket-echo.com</p>
        </div>
    </div>
    
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 页面元素引用
            const statusIndicator = document.getElementById('statusIndicator');
            const statusText = document.getElementById('statusText');
            const serverUrl = document.getElementById('serverUrl');
            const connectBtn = document.getElementById('connectBtn');
            const disconnectBtn = document.getElementById('disconnectBtn');
            const messageInput = document.getElementById('messageInput');
            const sendBtn = document.getElementById('sendBtn');
            const logContainer = document.getElementById('logContainer');
            const clearLogBtn = document.getElementById('clearLogBtn');
            const currentState = document.getElementById('currentState');
            const protocol = document.getElementById('protocol');
            const messageCount = document.getElementById('messageCount');
            
            // 状态变量
            let socket = null;
            let messageCounter = { sent: 0, received: 0 };
            
            // 连接状态更新函数
            function updateConnectionStatus(connected) {
                if (connected) {
                    statusIndicator.className = 'status-indicator status-connected';
                    statusText.textContent = `已连接到: ${socket.url}`;
                    currentState.textContent = "已连接";
                    
                    // 启用发送控件
                    messageInput.disabled = false;
                    sendBtn.disabled = false;
                    connectBtn.disabled = true;
                    disconnectBtn.disabled = false;
                } else {
                    statusIndicator.className = 'status-indicator status-disconnected';
                    statusText.textContent = '未连接';
                    currentState.textContent = "未连接";
                    
                    // 禁用发送控件
                    messageInput.disabled = true;
                    sendBtn.disabled = true;
                    connectBtn.disabled = false;
                    disconnectBtn.disabled = true;
                }
            }
            
            // 日志函数
            function addLog(type, content) {
                const logEntry = document.createElement('div');
                logEntry.className = `log-entry ${type}`;
                
                const now = new Date();
                const timestamp = `[${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}]`;
                
                logEntry.innerHTML = `<span class="timestamp">${timestamp}</span> ${content}`;
                
                logContainer.prepend(logEntry);
            }
            
            // 连接WebSocket
            function connect() {
                const url = serverUrl.value.trim();
                
                if (!url) {
                    alert('请输入有效的WebSocket服务器URL');
                    return;
                }
                
                try {
                    socket = new WebSocket(url);
                    
                    // 连接打开
                    socket.addEventListener('open', (event) => {
                        updateConnectionStatus(true);
                        protocol.textContent = "WebSocket";
                        addLog('system', `连接已建立于: ${url}`);
                    });
                    
                    // 接收消息
                    socket.addEventListener('message', (event) => {
                        messageCounter.received++;
                        updateMessageCount();
                        addLog('incoming', `接收: ${event.data}`);
                    });
                    
                    // 错误处理
                    socket.addEventListener('error', (error) => {
                        addLog('error', `错误: ${error.message || '未知错误'}`);
                    });
                    
                    // 连接关闭
                    socket.addEventListener('close', (event) => {
                        updateConnectionStatus(false);
                        addLog('system', `连接关闭 (代码: ${event.code}, 原因: ${event.reason || '无'})`);
                        socket = null;
                    });
                    
                } catch (error) {
                    updateConnectionStatus(false);
                    addLog('error', `连接失败: ${error.message}`);
                }
            }
            
            // 断开WebSocket连接
            function disconnect() {
                if (socket) {
                    socket.close();
                    socket = null;
                }
            }
            
            // 发送消息
            function sendMessage() {
                if (!socket || socket.readyState !== WebSocket.OPEN) {
                    addLog('error', '错误: 连接未就绪,无法发送消息');
                    return;
                }
                
                const message = messageInput.value.trim();
                if (!message) {
                    alert('请输入要发送的消息');
                    return;
                }
                
                try {
                    socket.send(message);
                    messageCounter.sent++;
                    updateMessageCount();
                    addLog('outgoing', `发送: ${message}`);
                    messageInput.value = '';
                } catch (error) {
                    addLog('error', `发送失败: ${error.message}`);
                }
            }
            
            // 更新消息计数显示
            function updateMessageCount() {
                messageCount.textContent = `已发送: ${messageCounter.sent} | 已接收: ${messageCounter.received}`;
            }
            
            // 清除日志
            function clearLog() {
                logContainer.innerHTML = '';
            }
            
            // 设置事件监听器
            connectBtn.addEventListener('click', connect);
            disconnectBtn.addEventListener('click', disconnect);
            sendBtn.addEventListener('click', sendMessage);
            clearLogBtn.addEventListener('click', clearLog);
            
            // 支持按Enter键发送消息
            messageInput.addEventListener('keypress', (event) => {
                if (event.key === 'Enter' && !sendBtn.disabled) {
                    sendMessage();
                }
            });
            
            // 初始化日志
            addLog('system', 'WebSocket测试工具已准备就绪');
            addLog('system', '请连接到一个WebSocket服务器开始测试');
        });
    </script>
</body>
</html>

4.1 HTTP 和 WS 测试

分别启动测试服务和代理服务

测试服务

在这里插入图片描述

代理服务

在这里插入图片描述

HTTP 测试,8081 转发到 8080

Get

在这里插入图片描述
Post

在这里插入图片描述

WS 测试 8982 转发到 8080

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到