基于SpringBoot實(shí)現(xiàn)一個(gè)JAVA代理HTTP/ WS的方法
1.環(huán)境信息
| 組件 | 版本 |
|---|---|
| JDK | 21 |
| Springboot | 3.5.3 |
| netty-all | 4.1.108.Final |
| smiley-http-proxy-servlet | 2.0 |
2.項(xiàng)目結(jié)構(gòu)和代碼
2.1 項(xiàng)目結(jié)構(gòu)

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 代理轉(zhuǎn)發(fā)
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;
}
}
轉(zhuǎn)發(fā)參數(shù)處理類,如設(shè)置頭、添加權(quán)限控制、轉(zhuǎn)發(fā)記錄等操作
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");
//調(diào)用父方法
return super.doExecute(servletRequest, servletResponse, proxyRequest);
}
}
2.3.2 WS 代理
基于 NETTY 實(shí)現(xiàn) WS 協(xié)議,完成轉(zhuǎn)發(fā)
NETTY 服務(wù)端定義
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 服務(wù)
* @throws Exception
*/
@PostConstruct
private void init() throws Exception {
if (isWS){
// 創(chuàng)建線程
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();
}
}
/**
* 構(gòu)建本地 WS 服務(wù)
* @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 內(nèi)容聚合器
pipeline.addLast(new HttpObjectAggregator(65536));
// 添加 WebSocket 處理器
pipeline.addLast(new WebSocketServerProtocolHandler(websocketPath));
// 添加自定義處理器
pipeline.addLast(new WebSocketProxyHandler(remoteUri));
}
});
// 綁定端口并啟動服務(wù)器
Channel channel = bootstrap.bind(websocketPort).sync().channel();
// 輸出啟動信息
logger.info("webSocket server started on port {}", websocketPort);
// 等待服務(wù)器 socket 關(guān)閉
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);
/**
* 遠(yuǎn)端 WS 服務(wù)連接器
*/
private final WebSocketClientConnector connector;
/**
* 遠(yuǎn)端 WS 服務(wù)會話
*/
private WebSocketSession session;
private AtomicReference<ChannelHandlerContext> ctx = new AtomicReference<>();
/**
* 構(gòu)造 WS 消息處理器
* @param uri
*/
public WebSocketProxyHandler(String uri) {
//添加目標(biāo)服務(wù)
connector = new WebSocketClientConnector(uri, ctx);
//建立連接
try {
connector.connect();
} catch (Exception e) {
logger.info("connect to remote server failed", e);
}
}
/**
* 處理接收到的消息,并轉(zhuǎn)發(fā)到遠(yuǎn)端服務(wù)
* @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();
}
// 轉(zhuǎn)發(fā)到遠(yuǎn)端服務(wù)
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) {
// 處理二進(jìn)制幀
logger.info("received binary message");
} else if (frame instanceof CloseWebSocketFrame) {
// 處理關(guān)閉幀
ctx.close();
}
}
/**
* 鏈接關(guān)閉處理
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 關(guān)閉遠(yuǎn)端連接
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;
/**
* 構(gòu)建訪問遠(yuǎn)端 WS 服務(wù)的本地客戶端
* @param uri
* @param ctx
*/
public WebSocketClientConnector(String uri, AtomicReference<ChannelHandlerContext> ctx) {
this.uri = uri;
this.ctx = ctx;
this.webSocketClientHandler = new WebSocketClientHandler(ctx);
}
/**
* 連接遠(yuǎn)端服務(wù)
*/
public void connect() {
// 創(chuàng)建 WebSocket 連接管理器
this.connectionManager = new WebSocketConnectionManager(
webSocketClient,
webSocketClientHandler,
uri
);
// 啟動連接
this.connectionManager.start();
// 記錄日志
logger.info("web socket client started and connecting to: {}", uri);
}
/**
* 關(guān)閉連接
*/
public void close(){
this.connectionManager.stop();
}
/**
* 獲取與遠(yuǎn)端的會話 SESSION
* @return
*/
public WebSocketSession getRemoteSession(){
if (Objects.nonNull(webSocketClientHandler.getSession())) {
// 發(fā)送一條消息到服務(wù)器
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 服務(wù)的 NIO 通道上下文
*/
private final AtomicReference<ChannelHandlerContext> ctx;
/**
* 構(gòu)造 WS 客戶端消息處理器, 獲取本第 WS 服務(wù)的 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;
}
/**
* 消息處理,接收遠(yuǎn)端服務(wù)
* @param session
* @param message
* @throws Exception
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 處理從服務(wù)器收到的消息
logger.info("received message: {}", message.getPayload());
// 轉(zhuǎn)發(fā)到本地 WS 服務(wù),并回寫給其他連接
if (Objects.nonNull(this.ctx.get())){
this.ctx.get().writeAndFlush(new TextWebSocketFrame(message.getPayload()));
}
}
/**
* 連接關(guān)閉后處理
* @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());
}
/**
* 報(bào)錯(cuò)處理
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.error("web socket transport error: ", exception);
}
/**
* 獲取到遠(yuǎn)端服務(wù)的 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.測試用第三方服務(wù)示例
3.1 項(xiàng)目結(jié)構(gòu)

依賴包
<?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 處理器,將消息轉(zhuǎn)大寫返回
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<>();
// 消息計(jì)數(shù)
private static int messageCount = 0;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.put(session.getId(), session);
System.out.println("連接建立: " + session.getId());
// 向新連接的客戶端發(fā)送歡迎消息
session.sendMessage(new TextMessage(
"{\"type\": \"system\", \"message\": \"連接服務(wù)器成功!發(fā)送 '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("來自服務(wù)器的廣播消息 (" + messageCount + ")");
} else {
// 默認(rèn)回顯消息
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("連接關(guān)閉: " + session.getId() + ", 狀態(tài): " + 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轉(zhuǎn)義
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 實(shí)現(xiàn)一個(gè) 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">測試、調(diào)試和監(jiān)控您的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>發(fā)送消息</h2>
<div class="form-group">
<label for="messageInput">輸入要發(fā)送的消息:</label>
<input type="text" id="messageInput" placeholder="輸入消息內(nèi)容..." disabled>
</div>
<button id="sendBtn" disabled>發(fā)送消息</button>
</div>
<div class="panel">
<h2>連接信息</h2>
<div class="info-container">
<div class="info-box">
<h3>當(dāng)前狀態(tài)</h3>
<p id="currentState">未連接</p>
</div>
<div class="info-box">
<h3>傳輸協(xié)議</h3>
<p id="protocol">-</p>
</div>
<div class="info-box">
<h3>消息計(jì)數(shù)</h3>
<p id="messageCount">已發(fā)送: 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>示例服務(wù)器URL:</h3>
<p>本地測試服務(wù)器: ws://localhost:8082/ws/conn</p>
<p>SSL測試服務(wù)器: 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');
// 狀態(tài)變量
let socket = null;
let messageCounter = { sent: 0, received: 0 };
// 連接狀態(tài)更新函數(shù)
function updateConnectionStatus(connected) {
if (connected) {
statusIndicator.className = 'status-indicator status-connected';
statusText.textContent = `已連接到: ${socket.url}`;
currentState.textContent = "已連接";
// 啟用發(fā)送控件
messageInput.disabled = false;
sendBtn.disabled = false;
connectBtn.disabled = true;
disconnectBtn.disabled = false;
} else {
statusIndicator.className = 'status-indicator status-disconnected';
statusText.textContent = '未連接';
currentState.textContent = "未連接";
// 禁用發(fā)送控件
messageInput.disabled = true;
sendBtn.disabled = true;
connectBtn.disabled = false;
disconnectBtn.disabled = true;
}
}
// 日志函數(shù)
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服務(wù)器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}`);
});
// 錯(cuò)誤處理
socket.addEventListener('error', (error) => {
addLog('error', `錯(cuò)誤: ${error.message || '未知錯(cuò)誤'}`);
});
// 連接關(guān)閉
socket.addEventListener('close', (event) => {
updateConnectionStatus(false);
addLog('system', `連接關(guān)閉 (代碼: ${event.code}, 原因: ${event.reason || '無'})`);
socket = null;
});
} catch (error) {
updateConnectionStatus(false);
addLog('error', `連接失敗: ${error.message}`);
}
}
// 斷開WebSocket連接
function disconnect() {
if (socket) {
socket.close();
socket = null;
}
}
// 發(fā)送消息
function sendMessage() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
addLog('error', '錯(cuò)誤: 連接未就緒,無法發(fā)送消息');
return;
}
const message = messageInput.value.trim();
if (!message) {
alert('請輸入要發(fā)送的消息');
return;
}
try {
socket.send(message);
messageCounter.sent++;
updateMessageCount();
addLog('outgoing', `發(fā)送: ${message}`);
messageInput.value = '';
} catch (error) {
addLog('error', `發(fā)送失敗: ${error.message}`);
}
}
// 更新消息計(jì)數(shù)顯示
function updateMessageCount() {
messageCount.textContent = `已發(fā)送: ${messageCounter.sent} | 已接收: ${messageCounter.received}`;
}
// 清除日志
function clearLog() {
logContainer.innerHTML = '';
}
// 設(shè)置事件監(jiān)聽器
connectBtn.addEventListener('click', connect);
disconnectBtn.addEventListener('click', disconnect);
sendBtn.addEventListener('click', sendMessage);
clearLogBtn.addEventListener('click', clearLog);
// 支持按Enter鍵發(fā)送消息
messageInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter' && !sendBtn.disabled) {
sendMessage();
}
});
// 初始化日志
addLog('system', 'WebSocket測試工具已準(zhǔn)備就緒');
addLog('system', '請連接到一個(gè)WebSocket服務(wù)器開始測試');
});
</script>
</body>
</html>
4.1 HTTP 和 WS 測試
分別啟動測試服務(wù)和代理服務(wù)
測試服務(wù)

代理服務(wù)

HTTP 測試,8081 轉(zhuǎn)發(fā)到 8080
Get

Post

WS 測試 8982 轉(zhuǎn)發(fā)到 8080

到此這篇關(guān)于基于SpringBoot實(shí)現(xiàn)一個(gè)JAVA代理HTTP/ WS的方法的文章就介紹到這了,更多相關(guān)SpringBoot 代理HTTP/ WS內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Web Filter 過濾器學(xué)習(xí)教程(推薦)
Filter也稱之為過濾器,它是Servlet技術(shù)中最激動人心的技術(shù).這篇文章主要介紹了Java Web Filter 過濾器學(xué)習(xí)教程的相關(guān)資料,需要的朋友可以參考下2016-05-05
Spring Boot環(huán)境下Mybatis Plus的快速應(yīng)用操作
這篇文章主要介紹了Spring Boot環(huán)境下Mybatis Plus的快速應(yīng)用操作,具有很好的價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
IDEA使用SequenceDiagram插件繪制時(shí)序圖的方法
這篇文章主要介紹了IDEA使用SequenceDiagram插件繪制時(shí)序圖的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
java隊(duì)列實(shí)現(xiàn)方法(順序隊(duì)列,鏈?zhǔn)疥?duì)列,循環(huán)隊(duì)列)
下面小編就為大家分享一篇java隊(duì)列實(shí)現(xiàn)方法(順序隊(duì)列,鏈?zhǔn)疥?duì)列,循環(huán)隊(duì)列),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12
Java 內(nèi)置接口 Serializable示例詳解
這篇文章主要為大家介紹了Java 內(nèi)置接口 Serializable示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Spring聲明式事務(wù)@Transactional注解實(shí)現(xiàn)元數(shù)據(jù)驅(qū)動的事務(wù)管理
這篇文章主要為大家介紹了Spring聲明式事務(wù)@Transactional注解實(shí)現(xiàn)元數(shù)據(jù)驅(qū)動的事務(wù)管理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

