前言
由於我們的api-gateway使用vertx web開發,而大文件的下載在通過網關處理時,會超時並且嚴重拖慢網關的處理。所以需要單獨優化處理了下載接口,讓網關可以將後端傳過來的buffer直接發送給客戶端,而不需要等待網關下載到網關的服務器上,再轉發給客戶端(這也是大文件下載客戶端會超時的一部分原因)
問題所在
webclient對body的處理:
vertx webclient默認下載文件時,是先下載到同級目錄下的file-uploads文件夾下,下載全部完成後纔會觸發handler回調,而大文件時,可能會持續幾分鐘甚至更長,而這個時候持有的客戶端請求八成已經超時斷開了。
解決方案
webclient其實對body的處理是可以配置的,可以配置成pipe模式,並傳入自定義的writeStream來實現自定義的流處理方案,而在routingContext中也是可以拿到前端的netSocket的,也就可以拿到客戶端的writeStream。所以我們的方案就是,自定義一個GatewayWriteStream,然後把客戶端的netsocket交給這個GatewayWriteStream。具體的代碼如下:
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.NetSocket;
import io.vertx.core.streams.WriteStream;
import java.util.Objects;
/**
* @author liwei
* @title: GatewayWriteStream
* @projectName moho
* @description: 下載文件的流處理
* @date 2019-11-22 13:30
*/
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就是文件的大小(這是我目前的處理方式,但這樣會造成每次需要請求兩次後端,如果有更好的解決方式我會更新,如果你發現了也請留言告訴我)
擴展
與其對應的應該還有網關上傳功能,我目前沒有做,不過可以想象的到方式大概如下:
- 配置router的bodyhandler對body的處理方式爲pipe,然後傳入自定義的writestream,這樣我們就拿到了客戶端上傳的流
- 自定義實現readstream接口,將1中的寫流傳入進來,然後調用webclient.sendStream,將自定義的readstream傳入應該就可以了。
- 如果有誰做了,可以貼個鏈接分享給我
2019-12-11補充:
上傳大文件的轉發:
實現思路和之前想的差不多,只不過不能使用webclient,因爲它封裝時候已經把buffer的handler隱藏掉了,只能通過httpclient來處理。不多說看代碼:
HttpClient client = vertx.createHttpClient(new HttpClientOptions());
HttpClientRequest c_req=client.request(VertHttpRequestWrapper.transMethod(request.getMethod()), SocketAddress.inetSocketAddress(targetInfo.getPort(),targetInfo.getHost())
,targetInfo.getPort(),targetInfo.getHost(),targetInfo.getRemoteUri(), res->{
context.response().setChunked(true);
context.response().setStatusCode(res.statusCode());
context.response().headers().setAll(res.headers());
res.handler(data->{
req.response().write(data);
});
res.endHandler((v)->req.response().end());
});
c_req.setChunked(true);
c_req.headers().setAll(context.request().headers());
req.handler(c_req::write);
req.endHandler((v) -> c_req.end());
這裏的處理就是把客戶端發送的buffer流直接轉發給了後端服務器,這樣做存在的問題是,不能對Body進行任何校驗,網關就起到了反向代理的作用,並且如果不在前邊對請求增加校驗,很容易因爲配置問題導致網關變成一個簡單的反向代理服務。
如何校驗是上傳:
String contentType = context.request().getHeader(HttpHeaders.CONTENT_TYPE);
if(contentType==null){
context.fail(RouterException.e(RouterCode.REQUEST_ERROR.getCode(),
RouterCode.REQUEST_ERROR.getMessage() + ": "+ "該請求並不是上傳請求,請覈實請求頭",
RouterCode.REQUEST_ERROR.getStatus()));
return;
}
String lowerCaseContentType = contentType.toLowerCase();
boolean isMultipart = lowerCaseContentType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString());
boolean isUrlEncoded = lowerCaseContentType.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString());
if(!isMultipart&&!isUrlEncoded){
context.fail(RouterException.e(RouterCode.REQUEST_ERROR.getCode(),
RouterCode.REQUEST_ERROR.getMessage() + ": "+ "該請求並不是上傳請求,請覈實請求頭",
RouterCode.REQUEST_ERROR.getStatus()));
return;
}