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