基于 SpringBoot 实现一个 JAVA 代理 HTTP / WS
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