TCP 数据转发
tio-websockify 使用 tio-boot 提供 WebSocket 与 TCP 双向二进制转发,内置 noVNC 页面。
转发流程
浏览器连接 /websockify 后,服务读取 EnvUtils.getStr("vnc-server"),为该浏览器建立独立的 TCP 连接。前端二进制消息封装为 BytePacket,后端原始字节封装为 WebSocketResponse.fromBytes(...)。两端关闭、断网、连接失败或发送失败时联动清理会话。文本帧会关闭连接。
# Hostname, IPv4 or bracketed IPv6 with a port.
vnc-server=127.0.0.1:5901
server.port=8080IPv6 示例为 [::1]:5901。配置只接受服务端指定的目标,不从浏览器查询参数读取目标地址。
实例与资源管理
WebSocket handler 在路由注册时创建一次,并显式创建一个 RelayService。所有会话复用该服务的 TCP 客户端与配置,每个会话仍拥有独立的 TCP 连接,避免重复创建配置和定时任务。这个小型项目使用普通 Java 实例,不需要 Aop 容器。
RelaySession 保存两端连接,并通过原子状态保证关闭操作幂等。服务端连接监听器在 TioApplication.run 之前注册,覆盖未发送 WebSocket 关闭帧的断网情况。onClose 负责正常的 WebSocket 关闭帧;底层监听器负责连接生命周期。
启动与路由
启动入口
package nexus.io.websocket.ify;
import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.websocket.ify.config.WebHelloConfig;
import nexus.io.websocket.ify.listener.RelayConnectionListener;
public class WebsocketIfyApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioBootServer.me().setServerAioListener(new RelayConnectionListener());
TioApplication.run(WebsocketIfyApp.class, new WebHelloConfig(), args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}路由配置
package nexus.io.websocket.ify.config;
import nexus.io.context.BootConfiguration;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.boot.websocket.WebSocketRouter;
import nexus.io.tio.http.server.router.HttpRequestRouter;
import nexus.io.websocket.ify.handler.WebsocketIfyIndexHandler;
import nexus.io.websocket.ify.handler.WebsockifyHandler;
import nexus.io.websocket.ify.listener.RelayConnectionListener;
public class WebHelloConfig implements BootConfiguration {
public void config() {
TioBootServer server = TioBootServer.me();
server.setServerAioListener(new RelayConnectionListener());
HttpRequestRouter requestRouter = server.getRequestRouter();
if (requestRouter != null) {
WebsocketIfyIndexHandler helloHandler = new WebsocketIfyIndexHandler();
requestRouter.get("/websocketify/index", helloHandler::index);
}
WebSocketRouter router = TioBootServer.me().getWebSocketRouter();
if (router != null) {
router.add("/websockify", new WebsockifyHandler());
}
}
}WebSocket handler
package nexus.io.websocket.ify.handler;
import lombok.extern.slf4j.Slf4j;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.websocket.common.WebSocketRequest;
import nexus.io.tio.websocket.server.handler.IWebSocketHandler;
import nexus.io.websocket.ify.service.RelayService;
@Slf4j
public class WebsockifyHandler implements IWebSocketHandler {
private final RelayService service = new RelayService();
@Override
public void onAfterHandshaked(HttpRequest request, HttpResponse response, ChannelContext context) {
try {
service.connect(context);
} catch (Exception error) {
log.warn("Unable to connect VNC relay", error);
service.close(context, "VNC connection failed");
}
}
@Override
public Object onBytes(WebSocketRequest request, byte[] data, ChannelContext context) {
service.forward(context, data);
return null;
}
@Override
public Object onClose(WebSocketRequest request, byte[] data, ChannelContext context) {
service.close(context, "WebSocket closed");
return null;
}
@Override
public HttpResponse handshake(HttpRequest request, HttpResponse response, ChannelContext context) {
return response;
}
@Override
public Object onText(WebSocketRequest request, String text, ChannelContext context) {
service.close(context, "Only binary VNC frames are supported");
return null;
}
}连接关闭监听器
package nexus.io.websocket.ify.listener;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.DefaultAioListener;
import nexus.io.websocket.ify.model.RelaySession;
public class RelayConnectionListener extends DefaultAioListener {
@Override
public void onBeforeClose(ChannelContext context, Throwable error, String reason, boolean remove) {
RelaySession session = (RelaySession) context.getAttribute(RelaySession.ATTRIBUTE);
if (session != null) {
session.close("Relay peer disconnected");
}
}
}TCP 二进制转发
package nexus.io.websocket.ify.handler;
import java.nio.ByteBuffer;
import nexus.io.aio.BytePacket;
import nexus.io.aio.Packet;
import nexus.io.tio.client.intf.ClientAioHandler;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
import nexus.io.tio.core.TioConfig;
import nexus.io.tio.websocket.common.WebSocketResponse;
import nexus.io.websocket.ify.model.RelaySession;
public class TcpRelayClientHandler implements ClientAioHandler {
@Override
public Packet decode(ByteBuffer buffer, int limit, int position, int readableLength, ChannelContext ctx) {
byte[] bs = new byte[readableLength];
buffer.get(bs);
return new BytePacket(bs);
}
@Override
public ByteBuffer encode(Packet packet, TioConfig tioConfig, ChannelContext ctx) {
BytePacket pkt = (BytePacket) packet;
byte[] body = pkt.getBytes();
ByteBuffer buf = ByteBuffer.allocate(body.length);
buf.order(tioConfig.getByteOrder());
buf.put(body);
return buf;
}
@Override
public void handler(Packet packet, ChannelContext tcpCtx) throws Exception {
RelaySession session = (RelaySession) tcpCtx.getAttribute(RelaySession.ATTRIBUTE);
if (session == null || session.isClosed()) {
Tio.remove(tcpCtx, "WebSocket unavailable");
return;
}
byte[] bs = ((BytePacket) packet).getBytes();
WebSocketResponse wsResp = WebSocketResponse.fromBytes(bs);
if (!Tio.send(session.getWebsocket(), wsResp)) {
session.close("WebSocket send failed");
}
}
@Override
public Packet heartbeatPacket(ChannelContext channelContext) {
return null;
}
}构建和测试
在项目目录执行 build,按 build.txt 生产打包并跳过 GPG 签名。运行时指定 server.port 和 vnc-server,打开 /vnc.html 并输入 VNC 服务的密码。GET /websocketify/index 返回 RespBodyVo 状态响应;该接口不代表后端 VNC 已经连接。
mvn test -Dgpg.skip=true 验证目标配置解析及数据反序列化。打包后运行 python src/test/python/relay_integration.py,通过临时 TCP 服务和真实 WebSocket 连接验证二进制往返、重复连接、断网、后端 EOF、文本帧拒绝以及连接失败处理。
开源项目:tio-websockify。
