前言
上篇分享中已经提到了使用vertx web是可以开发网关的,而大文件的下载在通过网关处理时,如果和其他默认请求处理方式一样,则会导致超时并且严重拖慢网关的处理。所以需要单独处理下载接口,但是不能使用之前讲的那种简单的反向代理,否则网关并不能对该请求做任何处理,所以为了让网关可以将后端传过来的buffer直接发送给客户端,而不需要等待网关下载到网关的服务器上,再转发给客户端(这也是大文件下载客户端会超时的一部分原因),就有了今天这篇分享
难点分析
作为一个网关来说,当转发一般请求时,必然需要使用vertx的webclient来访问实际后端,然后把返回的结果经过一系列自定义处理后返回给前端。而当请求为下载文件时 webclient对body的处理方式为:
先下载到同级目录下的file-uploads文件夹下,下载全部完成后才会触发handler回调,而大文件时,可能会持续几分钟甚至更长,而这个时候持有的客户端请求八成已经超时断开了。
解决方案
webclient其实对body的处理是可以配置的,可以配置成pipe模式,并传入自定义的writeStream来实现自定义的流处理方案,而在routingContext中也是可以拿到前端的netSocket的,也就可以拿到客户端的writeStream。所以我们的方案就是,自定义一个GatewayWriteStream,然后把客户端的netsocket交给这个GatewayWriteStream。具体的代码如下:
public class GatewayWriteStream implements WriteStream<Buffer> {
private NetSocket netSocket;
private Handler<Throwable> exceptionHandler;
private int maxWrites = 128 * 1024; // TODO - we should tune this for best pernformace
private long writesOutstanding;
private Runnable closedDeferred;
private boolean closed;
private final Vertx vertx;
private final Context context;
private int lwm = maxWrites / 2;
private Handler<Void> drainHandler;
private Logger log;
public GatewayWriteStream(NetSocket netSocket, Vertx vertx,String length) {
Objects.requireNonNull(netSocket, "NetSocket");
this.netSocket = netSocket;
//返回响应头,来通知客户端文件大小
String header="HTTP/1.0 200 \n"+
HttpHeaders.CONTENT_LENGTH.toString()+": "+length+" \n\n";
Buffer buffer=Buffer.buffer(header);
this.netSocket.write(buffer);
this.vertx = vertx;
this.context=vertx.getOrCreateContext();
this.log= LoggerFactory.getLogger(this.getClass());
}
@Override
public WriteStream<Buffer> exceptionHandler(Handler<Throwable> handler) {
check();
this.exceptionHandler = handler;;
return this;
}
@Override
public WriteStream<Buffer> write(Buffer buffer) {
return write(buffer, null);
}
@Override
public synchronized WriteStream<Buffer> write(Buffer buffer, Handler<AsyncResult<Void>> handler) {
doWrite(buffer, handler);
return this;
}
@Override
public void end() {
netSocket.end();
}
@Override
public void end(Handler<AsyncResult<Void>> handler) {
netSocket.end(handler);
}
@Override
public WriteStream<Buffer> setWriteQueueMaxSize(int maxSize) {
netSocket.setWriteQueueMaxSize(maxSize);
return this;
}
@Override
public boolean writeQueueFull() {
return netSocket.writeQueueFull();
}
@Override
public WriteStream<Buffer> drainHandler(Handler<Void> handler) {
check();
this.drainHandler = handler;
checkDrained();
return this;
}
private synchronized WriteStream<Buffer> doWrite(Buffer buffer, Handler<AsyncResult<Void>> handler) {
Objects.requireNonNull(buffer, "buffer");
check();
Handler<AsyncResult<Void>> wrapped = ar -> {
if (ar.succeeded()) {
checkContext();
Runnable action;
synchronized (GatewayWriteStream.this) {
if (writesOutstanding == 0 && closedDeferred != null) {
action = closedDeferred;
} else {
action = this::checkDrained;
}
}
action.run();
if (handler != null) {
handler.handle(ar);
}
} else {
if (handler != null) {
handler.handle(ar);
} else {
handleException(ar.cause());
}
}
};
doWriteBuffer(buffer,buffer.length(),wrapped);
return this;
}
private void doWriteBuffer(Buffer buff, long toWrite, Handler<AsyncResult<Void>> handler) {
if (toWrite > 0) {
synchronized (this) {
writesOutstanding += toWrite;
}
writeInternal(buff, handler);
} else {
handler.handle(Future.succeededFuture());
}
}
private void writeInternal(Buffer buff, Handler<AsyncResult<Void>> handler) {
netSocket.write(buff,as->{
if(as.succeeded()){
synchronized (GatewayWriteStream.this) {
writesOutstanding -= buff.getByteBuf().nioBuffer().limit();
}
handler.handle(Future.succeededFuture());
}
});
}
private synchronized void closeInternal(Handler<AsyncResult<Void>> handler) {
check();
closed = true;
if (writesOutstanding == 0) {
doClose(handler);
} else {
closedDeferred = () -> doClose(handler);
}
}
private void check() {
checkClosed();
}
private void checkClosed() {
if (closed) {
throw new IllegalStateException("File handle is closed");
}
}
private void doClose(Handler<AsyncResult<Void>> handler) {
Context handlerContext = vertx.getOrCreateContext();
handlerContext.executeBlocking(res -> {
netSocket.end();
res.complete(null);
}, handler);
}
private void checkContext() {
if (!vertx.getOrCreateContext().equals(context)) {
throw new IllegalStateException("AsyncFile must only be used in the context that created it, expected: "
+ context + " actual " + vertx.getOrCreateContext());
}
}
private synchronized void checkDrained() {
if (drainHandler != null && writesOutstanding <= lwm) {
Handler<Void> handler = drainHandler;
drainHandler = null;
handler.handle(null);
}
}
private void handleException(Throwable t) {
if (exceptionHandler != null && t instanceof Exception) {
exceptionHandler.handle(t);
} else {
log.error("Unhandled exception", t);
}
}
}
注:这里的代码借鉴了vertx官方的AsyncFileImpl,其中很多buffer长度和位置的记录是可以省略的,只不过我这里为了后续其他功能保留了下来,不需要可以自行更改。
router的handler中的代码,类似如下:
private void downLoad(TargetInfo targetInfo, RoutingContext context, String length) {
SocketAddress socketAddress = SocketAddress.inetSocketAddress(targetInfo.getPort(), targetInfo.getHost());
GatewayWriteStream writeStream = new GatewayWriteStream(context.request().netSocket(), context.vertx(), length);
webClient.request(HttpMethod.GET, socketAddress, targetInfo.getRemoteUri()).
as(BodyCodec.pipe(writeStream)).send(v -> {
});
}
而传入的length是文件的大小,他的获取方式是在真正请求之前,先向后端发起一次head请求,返回的headers中的CONTENT_LENGTH就是文件的大小(这是我目前的处理方式,但这样会造成每次需要请求两次后端,且有些后端服务不支持header,如果有更好的解决方式我会更新,如果你发现了也请留言告诉我)
Feature
后面有时间我会全面的写一个如何使用vert.x开发一个高性能网关的系列分享。