Skip to content

HTTP 与 WebSocket 反向代理 ​

网关的转发能力来自 tio-boot 本身,不使用网关模块也可以直接用它们写一个反向代理。

HTTP:TioHttpProxy ​

TioHttpProxy.forward(url, request, response) 把当前请求发到目标地址:

  • 支持 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS。
  • 请求体按收到的原始字节转发,JSON、表单、文件上传和二进制数据都原样到达后端。
  • 去掉 Connection、Host 等逐跳请求头,设置 X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host。
  • 有长度的响应整体返回;text/event-stream 和没有长度的分块响应边收边发,客户端断开时取消后端请求。
  • 流式响应在最后一块发出后才按客户端的 Connection: close 关闭连接,放在 nginx 等反向代理之后同样完整。
  • 后端连不上时抛出 IOException,调用方可以换一个地址重试。reverseProxy(prefix, request, response) 则直接返回 502。

下面的应用把 /p/** 转发到 http://127.0.0.1:19001:

java
package demo;

import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.http.forward.TioHttpProxy;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.common.HttpResponse;

public class ProxyApp {
  public static void main(String[] args) {
    TioApplication.run(ProxyApp.class, () -> {
      TioBootServer.me().getRequestRouter().any("/p/**", request -> {
        HttpResponse response = TioRequestContext.getResponse();
        String path = request.getRequestLine().getPathAndQuery().substring(2);
        try {
          return TioHttpProxy.forward("http://127.0.0.1:19001" + path, request, response);
        } catch (Exception e) {
          return response.setStatus(502);
        }
      });
    }, args);
  }
}

HttpRequestRouter.any(path, handler) 为所有请求方法注册同一个处理器,代理和网关的路由都用它;add(path, handler) 只注册 GET、POST、PUT、DELETE。

运行验证(后端是一个回显请求的服务):

shell
curl -X PATCH -H 'Content-Type: application/json' -d '{"x":1}' localhost:19002/p/echo
{"method": "PATCH", "path": "/echo", "len": 7, "ct": "application/json", "xff": "127.0.0.1", "xfp": "http", "xfh": "localhost:19002"}

head -c 100000 /dev/urandom > bin.dat
curl -X PUT -H 'Content-Type: application/octet-stream' --data-binary @bin.dat localhost:19002/p/bin
# 后端收到的 100000 字节与原文件的 SHA1 一致

curl -N localhost:19002/p/sse
# 后端每秒发一条事件,客户端每秒收到一条

需要更长的读取超时(例如 AI 流式接口)时,传入自己的 OkHttpClient:

java
OkHttpClient client = OkHttpClientPool.getHttpClient().newBuilder()
    .readTimeout(300, TimeUnit.SECONDS).followRedirects(false).build();
TioHttpProxy.forward(client, url, request, response, null);

使用 TioHttpProxy 的应用需要依赖 okhttp 和 okhttp-brotli。

WebSocket:WebSocketProxyHandler ​

WebSocketProxyHandler 是一个 IWebSocketHandler:客户端握手时由解析器选择后端,握手完成后连接后端,并双向转发文本和二进制消息。

  • 后端连接建立前客户端发来的消息先排队,连上后按顺序发出。
  • 后端消息分片会合并后再发给客户端,后端的 ping 自动回复 pong。
  • 任意一端断开,另一端随之关闭。客户端异常断开(没有发送关闭帧、心跳超时)时同样会关闭后端连接。
  • 后端拒绝握手或连接失败时,用关闭码 1011 关闭客户端。
  • 所有后端连接共用一个 TioClient,代理大量连接时不会为每个连接创建线程池。
java
package demo;

import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.boot.websocket.proxy.WebSocketProxyHandler;

public class WsProxyApp {
  public static void main(String[] args) {
    TioApplication.run(WsProxyApp.class, () -> {
      TioBootServer.me().getWebSocketRouter().add("/ws", new WebSocketProxyHandler((request, channelContext) ->
          WebSocketProxyHandler.upstreamOf("ws://127.0.0.1:19003" + request.getRequestLine().getPathAndQuery(),
              request)));
    }, args);
  }
}
  • 解析器返回 null 时拒绝握手,可以在这里校验 token。
  • WebSocketProxyHandler.upstreamOf(url, request) 会带上客户端握手时的请求头(Cookie、token 等)和 X-Forwarded-For;也可以 new WebSocketUpstream(url).header(name, value) 自己组装。
  • 后端连接超时由 server.websocket.proxy.connect.timeout(秒,默认 5)控制。

用 Node.js 验证:

javascript
const ws = new WebSocket('ws://127.0.0.1:19002/ws?room=7', { headers: { token: 'abc123' } });
ws.onopen = () => { ws.send('early-1'); ws.send('early-2'); };
ws.onmessage = (e) => console.log(String(e.data));
hello path=/ws?room=7 xff=127.0.0.1 token=abc123
echo:early-1
echo:early-2

连接关闭回调:ChannelCloseHooks ​

ChannelCloseHooks.add(channelContext, runnable) 给一个连接登记回调,连接因任何原因关闭(关闭帧、网络错误、心跳超时)时执行一次。WebSocket 代理用它在客户端断开时释放后端连接,业务中也可以用它清理与连接绑定的资源:

java
package demo;

import nexus.io.tio.boot.server.ChannelCloseHooks;
import nexus.io.tio.core.ChannelContext;
import nexus.io.tio.core.Tio;
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;

public class RoomWebSocketHandler implements IWebSocketHandler {

  @Override
  public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) {
    return httpResponse;
  }

  @Override
  public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) {
    String room = httpRequest.getParam("room");
    Tio.bindGroup(channelContext, room);
    ChannelCloseHooks.add(channelContext, () -> System.out.println("left room " + room));
  }

  @Override
  public Object onBytes(WebSocketRequest wsRequest, byte[] bytes, ChannelContext channelContext) {
    return null;
  }

  @Override
  public Object onClose(WebSocketRequest wsRequest, byte[] bytes, ChannelContext channelContext) {
    Tio.remove(channelContext, "client closed");
    return null;
  }

  @Override
  public Object onText(WebSocketRequest wsRequest, String text, ChannelContext channelContext) {
    return text;
  }
}

MIT Licensed