依赖Jetty实现流转发功能的实践

本文介绍了从nginx-lua迁移到Java实现文件下载服务的原因,并详细阐述了如何依赖Jetty实现流转发功能,包括开启Servlet的异步支持、确定异步写入API的选择,以及最终选择Jetty内置的HttpClient完成任务。过程中分析了ByteBuffer、InputStream和ReadableByteChannel的限制,最终找到了适合的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在写文件的下载服务,主要功能是将请求URL调用业务接口进行解析,解析到真实的下载地址,然后将下载流透传出去。这一部分之前使用nginx-lua实现,由于维护过于困难(优雅上下线、截流、熔断能力缺乏, 证书相关运维支持缺失),因此改用Java实现。本文也主要论述如何依赖Jetty实现流转发功能。由于对Jetty的依赖非常深,建议使用EmbeddedJettyServer以引入和统一Jetty依赖。

为什么要阅读Jetty高层IO代码

  1. 下载服务必须采用AIO,以避免客户端或存储端带宽对服务表现的影响。
  2. 对于AIO机制,servlet-api中并没有相关标准。
  3. 自建AIO事件循环或自实现Http协议难度大,而且需要较长时间测试以保证代码健壮性。
  4. Jetty作为被依赖Web容器,本身就具有AIO能力。

开启Servlet的异步支持

首先,需要在HttpServletRequest中开启异步支持,如果不开启异步支持,会造成请求提前被返回引起流中断(servlet-api 3.1.0版本以上支持此功能)。

private void startAsync(HttpServletRequest request) {
    AsyncContext asyncContext = request.startAsync();
    // 异步任务设为不会超时
    asyncContext.setTimeout(0);
} 

确定异步写入Api

之后,我们需要了解Jetty是如何写入Http响应体的,我们能不能调用Jetty的方法异步写入Http响应体。

最直接的想法就是查看一下servlet-api中的javax.servlet.ServletOutputStream的实现类,有没有相关异步写入的方法。

通过代码阅读,很容易就可以找到javax.servlet.ServletOutputStream的实现类,也就是我们的主角org.eclipse.jetty.server.HttpOutput。

它身上有以下AIO相关的方法(仅截取三个关心的方法):

 /**
 * Asynchronous send of whole content.
 *
 * @param content  The whole content to send
 * @param callback The callback to use to notify success or failure
 */
public void sendContent(ByteBuffer content, final Callback callback)
{
    if (LOG.isDebugEnabled())
        LOG.debug("sendContent(buffer={},{})", BufferUtil.toDetailString(content), callback);

    _written += content.remaining();
    write(content, true, new Callback.Nested(callback)
    {
        @Override
        public void succeeded()
        {
            closed();
            super.succeeded();
        }

        @Override
        public void failed(Throwable x)
        {
            abort(x);
            super.failed(x);
        }
    });
}

/**
 * Asynchronous send of stream content.
 * The stream will be closed after reading all content.
 *
 * @param in       The stream content to send
 * @param callback The callback to use to notify success or failure
 */
public void sendContent(InputStream in, Callback callback)
{
    if (LOG.isDebugEnabled())
        LOG.debug("sendContent(stream={},{})", in, callback);

    new InputStreamWritingCB(in, callback).iterate();
}

/**
 * Asynchronous send of channel content.
 * The channel will be closed after reading all content.
 *
 * @param in       The channel content to send
 * @param callback The callback to use to notify success or failure
 */
public void sendContent(ReadableByteChannel in, Callback callback)
{
    if (LOG.isDebugEnabled())
        LOG.debug("sendContent(channel={},{})", in, callback);

    new ReadableByteChannelWritingCB(in, callback).iterate();
}

那么问题来了,ByteBuffer、InputStream和ReadableByteChannel各有什么限制呢?毕竟Jetty是一个Web容器,定位是资源的产生方,而不是消费方;产生流是没有任何延迟的,但存储服务的延迟是不可忽略的。那让我们分别来剖析一下。

首先看ByteBuffer的异步实现,实际包装了一下org.eclipse.jetty.server.HttpChannel的write方法,方法描述如下:

/**
 * <p>Non-Blocking write, committing the response if needed.</p>
 * Called as last link in HttpOutput.Filter chain
 * @param content  the content buffer to write
 * @param complete whether the content is complete for the response
 * @param callback Callback when complete or failed
 */
@Override
public void write(ByteBuffer content, boolean complete, Callback callback)

可以看到,使用ByteBuffer异步写方法是不合适的,因为此时complete参数是true,也就是说必须要把所有内容全部放在ByteBuffer中,否则会引起EofException或WritePendingException(其后的调用)。

那么InputStream的异步实现能不能用呢?我们来看一下相关实现:

@Override
protected Action process() throws Exception
{
    // Only return if EOF has previously been read and thus
    // a write done with EOF=true
    if (_eof)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("EOF of {}", this);
        // Handle EOF
        _in.close();
        closed();
        _channel.getByteBufferPool().release(_buffer);
        return Action.SUCCEEDED;
    }

    // Read until buffer full or EOF
    int len = 0;
    while (len < _buffer.capacity() && !_eof)
    {
        int r = _in.read(_buffer.array(), _buffer.arrayOffset() + len, _buffer.capacity() - len);
        if (r < 0)
            _eof = true;
        else
            len += r;
    }

    // write what we have
    _buffer.position(0);
    _buffer.limit(len);
    _written += len;
    write(_buffer, _eof, this);
    return Action.SCHEDULED;
}

可以看到InputStream的实现是同步的读取一个buffer,然后异步发送。想一下,Jetty的作为资源的生产方,读取的延时可以忽略不记,这样的实现是合适的。但是存储方的延时不能忽略,仍然对业务不适用。

现在只剩下ReadableByteChannel的实现了,我们来看一下相关实现:

protected Action process() throws Exception
{
    // Only return if EOF has previously been read and thus
    // a write done with EOF=true
    if (_eof)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("EOF of {}", this);
        _in.close();
        closed();
        _channel.getByteBufferPool().release(_buffer);
        return Action.SUCCEEDED;
    }

    // Read from stream until buffer full or EOF
    BufferUtil.clearToFill(_buffer);
    while (_buffer.hasRemaining() && !_eof)
        _eof = (_in.read(_buffer)) < 0;

    // write what we have
    BufferUtil.flipToFlush(_buffer, 0);
    _written += _buffer.remaining();
    write(_buffer, _eof, this);

    return Action.SCHEDULED;
}

总算有符合要求的了!那有没有planB呢?毕竟提供Channel的异步HttpClient感觉不多啊…那换个思路,我能不能拿到Jetty的Channel呢?毕竟这样我就可以用所有Channel的Api了,看了一下,HttpOutput里还真有,666

public HttpChannel getHttpChannel()
{
    return _channel;
}

4.确定HttpClient

HttpClient,第一个想到的是apache的HttpAsyncClient,看了一下,还真的提供了ReadableByteChannel(用法可以参考org.apache.http.nio.client.methods.ZeroCopyConsumer)。但是可能公司内jar包被人篡改了,也可能是HttpAsyncClient实现的channel并不标准,总之透传的内容是损坏的….巨扎心….

那我还能用什么Client呢?偶然间,居然发现Jetty本身就有HttpClient,还有这种操作?那就试一把

import com.meituan.xm.mbox.utils.response.RequestContext;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.util.Callback;

import java.io.IOException;
import java.nio.ByteBuffer;

public class StreamReadListener extends Response.Listener.Adapter {
    private HttpOutput httpOutput;

    public StreamReadListener(RequestContext requestContext) throws IOException {
        httpOutput = (HttpOutput) requestContext.getResponse().getOutputStream();
    }

    @Override
    public void onContent(Response response, ByteBuffer content, Callback callback) {
        httpOutput.getHttpChannel().write(content, false, callback);
    }

}

不愧是原装的HttpClient,自己的Server调用自己Client的回调,自己解决调用异常,连透传都不用适配, 6的飞起。至此,所有关键问题已全部解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值