依赖Jetty实现流转发功能的实践
来源:互联网 发布:郭襄 金轮法王 知乎 编辑:程序博客网 时间:2024/06/06 11:01
最近在写文件的下载服务,主要功能是将请求URL调用业务接口进行解析,解析到真实的下载地址,然后将下载流透传出去。这一部分之前使用nginx-lua实现,由于维护过于困难(优雅上下线、截流、熔断能力缺乏, 证书相关运维支持缺失),因此改用Java实现。本文也主要论述如何依赖Jetty实现流转发功能。由于对Jetty的依赖非常深,建议使用EmbeddedJettyServer以引入和统一Jetty依赖。
为什么要阅读Jetty高层IO代码
- 下载服务必须采用AIO,以避免客户端或存储端带宽对服务表现的影响。
- 对于AIO机制,servlet-api中并没有相关标准。
- 自建AIO事件循环或自实现Http协议难度大,而且需要较长时间测试以保证代码健壮性。
- 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 */@Overridepublic void write(ByteBuffer content, boolean complete, Callback callback)
可以看到,使用ByteBuffer异步写方法是不合适的,因为此时complete参数是true,也就是说必须要把所有内容全部放在ByteBuffer中,否则会引起EofException或WritePendingException(其后的调用)。
那么InputStream的异步实现能不能用呢?我们来看一下相关实现:
@Overrideprotected 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的飞起。至此,所有关键问题已全部解决。
- 依赖Jetty实现流转发功能的实践
- FFMPEG对RTP直播流转发的实现
- FFMPEG对RTP直播流转发的实现
- 实现邮箱发邮件的功能
- 基于java实现发短信的功能
- EasyRTMP实现将RTSP流转换成RTMP流实现RTSP直播转RTMP直播的功能
- 将EasyRTMP_RTSP移植到Android平台实现的RTSP拉流转推RTMP直播流功能
- 实现Spring依赖注入功能的想法
- OA项目的审批流转模块实现
- 发邮件的功能
- 【Practical Java】实践10:不要依赖equals()的缺省实现
- 发短信功能代码实现
- Java发邮件功能实现
- java实现发邮件功能
- Android 发短信功能实现
- Java实现发邮件功能
- Java实现发短信功能
- javaMail实现发邮件功能
- 可在单片机上运行的机器学习算法KNN(C语言实现)
- 如何将自己的jar包加载到本地库
- Visual Studio 2013 + OpenCV2.4.13 从x86架构切换至x64架构
- windows下mysql数据库的基本使用
- 1640: [Usaco2007 Nov]Best Cow Line 队列变换
- 依赖Jetty实现流转发功能的实践
- java自己实现单链表的基本操作
- 基础控件——TextView
- UVa 1587 Box
- LeetCode-[双指针法]Container With Most Water
- hdu 1071 ACM steps The area
- Java 8的新特性
- react-native ListView的简单用法
- RTC时间计算方法