Skip to content

TCP 数据转发 ​

tio-websockify 使用 tio-boot 提供 WebSocket 与 TCP 双向二进制转发,内置 noVNC 页面。

转发流程 ​

浏览器连接 /websockify 后,服务读取 EnvUtils.getStr("vnc-server"),为该浏览器建立独立的 TCP 连接。前端二进制消息封装为 BytePacket,后端原始字节封装为 WebSocketResponse.fromBytes(...)。两端关闭、断网、连接失败或发送失败时联动清理会话。文本帧会关闭连接。

properties
# Hostname, IPv4 or bracketed IPv6 with a port.
vnc-server=127.0.0.1:5901
server.port=8080

IPv6 示例为 [::1]:5901。配置只接受服务端指定的目标,不从浏览器查询参数读取目标地址。

实例与资源管理 ​

WebSocket handler 在路由注册时创建一次,并显式创建一个 RelayService。所有会话复用该服务的 TCP 客户端与配置,每个会话仍拥有独立的 TCP 连接,避免重复创建配置和定时任务。这个小型项目使用普通 Java 实例,不需要 Aop 容器。

RelaySession 保存两端连接,并通过原子状态保证关闭操作幂等。服务端连接监听器在 TioApplication.run 之前注册,覆盖未发送 WebSocket 关闭帧的断网情况。onClose 负责正常的 WebSocket 关闭帧;底层监听器负责连接生命周期。

启动与路由 ​

启动入口 ​

java
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");
  }
}

路由配置 ​

java
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 ​

java
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;
  }
}

连接关闭监听器 ​

java
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 二进制转发 ​

java
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。

MIT Licensed