如何使用vertx反向代理處理大文件的下載

前言

由於我們的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就是文件的大小(這是我目前的處理方式,但這樣會造成每次需要請求兩次後端,如果有更好的解決方式我會更新,如果你發現了也請留言告訴我)

擴展

與其對應的應該還有網關上傳功能,我目前沒有做,不過可以想象的到方式大概如下:

  1. 配置router的bodyhandler對body的處理方式爲pipe,然後傳入自定義的writestream,這樣我們就拿到了客戶端上傳的流
  2. 自定義實現readstream接口,將1中的寫流傳入進來,然後調用webclient.sendStream,將自定義的readstream傳入應該就可以了。
  3. 如果有誰做了,可以貼個鏈接分享給我

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;
            }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章