使用Netty完成一个轻量级Http文件下载器
引言
在网络开发中,不可避免的会与许多标准通讯协议打交道。在这方面,Netty为开发者提供了很大的便利。Netty自身除了提供编解码机制外,还提供了大量现成协议的编解码支持,这部分支持的内容可以在包netty-codec中找到。当然也包括我们今天要讲述,对Http的支持。
有了对Http编解码协议的支持,我们完全可以使用Netty来开发一款web容器或者符合需求的Http容器。今天,我们就以文件的下载作为场景,介绍下Netty对Http的支持。本文中使用的相关代码存储于:https://gitee.com/eric_ds/learnNetty/tree/master/http 目录下。
Http协议
在使用Netty完成Http服务器功能之前,首先需要对Http协议有个相对全面的了解。Http协议是工作在应用层的一个文本协议,其工作模型是请求-响应模型。客户端发出Http请求,服务端回复Http响应,一来一回,一次完整的Http交互才结束。这里我们将基本介绍下Http协议中对传输内容的格式规范要求,方便于后文对协议解析部分的代码理解。完整的Http协议中包含的内容十分的多,超出了本文的范畴,这里不再一一阐述。感兴趣的读者可以在RFC2616文档进行查阅。
Http协议对传输的报文格式定义如下:

主要是分为四个部分:起始行,零个或者多个头域,标记头域结束的回车换行,以及可能存在的内容体构成。其中起始行有两种情况,分别是:请求行和状态行。
起始行
请求行
请求行出现在客户端发出的Http请求中,表明这是一个Http请求。请求行的格式如下:

Http方法就是我们常提到的Http动词,早期常见的有GET,POST两种,RFC文档中也要求实现HEAD等动词,至于PUT,DELETE,PATCH服务器可以根据需要提供实现或者不实现。
请求URL就是我们常提到的URL地址。通过Http协议,Http方案的URL格式定义如下
http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
当访问的端口是80时,:port可以直接省略。abs_path用于对所需资源进行定位。"?" query 不是必须的。其目的在于传递查询参数。
Http版本号用于标识当前客户端要求本次通讯采用的Http协议,目前常见的有HTTP/1.0或HTTP/1.1。1.1和1.0的主要区别在于1.1允许一个tcp连接上发送和接收多个Http请求响应,而1.0当中一次Http交互完成后就会断开tcp连接,对于如今比较复杂和内容丰富的页面,显然1.0的这种处理方式是比较低效的,因此大部分的客户端和服务端对1.1版本早就提供了支持。
请求行的最后以连续的换行和回车作为结束标记。
状态行
状态行出现在服务端发出的Http响应中,用于表明这是一个Http响应,状态行的格式如下:

Http版本号用于指示该响应内容使用的版本号标识。
状态码用于指示该响应的结果情况。状态码为三位整数,第一位用于表明响应的类型,后两位无定义。第一位数字有5种定义:
- 1XX:用于报告,表示接受到请求,继续处理流程。
- 2XX:用于成功响应。表示接受到请求,并且被正确的处理。
- 3XX:重发,为了能够正确处理,需要重新发送请求。
- 4XX:客户端出错,请求内容包含错误讯息。
- 5XX:服务端出错,服务端无法理解或者处理客户端的请求。
在协议文档中定义了一些常见的状态码,比如200代表成功,404代表不存在等等。状态码可以自行定义,只需要避已经公共存在的状态码定义即可。原因短语是对状态码的文本解释,通常情况下无需关注。因为对状态码的识别已经足够进行条件控制了。
响应吗的最后也是通过连续的回车和换行结束。
头域
Http协议中定义的头域内容非常多,大体而言头域可以分为三种:通用头域,请求/响应头域,实体头域。
请求头域用于客户端向服务端发送本次请求的一些附加信息。虽然说是附加信息,但是这些信息中往往包含了很重要的数据内容。比如有:
- host:请求的主机位置
- Accept-Charset:客户端能够接受的字符集
等等。
响应头域用于服务端向客户端发送本次响应的一些附加信息,一般是对响应的内容起到元数据说明的作用。
通用头域则是一些通用的附加元数据信息描述,实体头域则是定义了所传输实体的一些附加信息。比较常见的有:
- Content-Length:传输内容体长度
- Last-Modified:资源的上一次修改时间等。
内容体
内容体则负责承载请求或者响应中需要传输的实体信息。常见的比如向服务端提交一个表单,内容体则承载了表单信息;如果是点开一个网址,服务端响应信息中,内容体就是一段html文本。其具体的信息较多,这里不展开,感兴趣的读者可以查阅RFC2616文档。
需求澄清
用Netty实现Http文件下载,大体上分为两个步骤:
- 浏览器访问特定资源URL,获取当前可下载资源列表。
- 浏览器访问特定资源URL,下载资源文件。
对于需求1而言,我们将磁盘上的某个文件夹映射为网络资源地址,只需要将对应的资源操作,转化为对磁盘文件夹的展示即可。
对于需求2而言,资源URL已经直接定位到了具体的文件,则通过读取磁盘上的数据内容,使用http协议将数据通过连接发送回客户端。
第一个版本
Netty内建提供了对Http协议的支持,其具体的支持包路径在io.netty.handler.codec.http.*下。Netty将Http协议中的各个部分抽象为HttpObject接口,来看下类图,如下

HttpObject是一个标识接口,本身没有提供方法,只是表明了其子接口的类别归属。所有的Http对象接口都继承了HttpObject。HttpRequest用于存储Http协议中,请求行和头域的内容。如果客户端的请求中带有内容体,则这部分数据存储在HttpContent对象中。与之同理,HttpResponse存储着状态行和头域的相关内容,响应的内容体存储在HttpContent之中。
对于Http请求的解析,Netty提供了解码类HttpRequestDecoder。该解码器可以将读取到的ByteBuf转换为HttpRequest对象和HttpContent对象。解码器是边解码,边向后传递解码到的内容。这种设计模式有助于减少可能的堆积内存数量。比如如果大文件上传的场景,在一个解码流程中完整解码内容体,势必要在内存中存储所有收到的数据(Netty内建的ByteToMessageDecoder采用此种实现),这可能导致程序消耗巨大的内存。边解码,边传递解码对象,则可以让开发者更容易处理此类场景,避免在完整解码前的巨大内存堆积。
如果请求消息带有消息体,HttpRequestDecoder会将整个请求解码为三种对象:
- 包含请求行和头域内容的
HttpRequest。 - 部分解码中的部分内容体的
HttpContent。 - 代表内容体解码完毕的
LastHttpContent。
表单提交这种场景不消耗多少内存,但是也会产生HttpContent。如果开发者不想处理HttpRequest和HttpContent分隔的这种情况,可以使用聚合处理器HttpObjectAggregator。该处理器可以将管道中前向的解码器产生的HttpObject聚合在一起,并且在收到LastHttpContent对象后,将已经聚合的HttpObject聚合成为一个完整的FullHttpRequest对象,并且向管道后面的处理器进行传递。那么此时开发者就只需要处理一个已经完全解码完毕的Http请求。
开发者要针对http请求发出响应,需要构建出HttpResponse和HttpContent分别用于存储状态行、头域和内容体相关数据,并且最终编码为Http协议数据。Netty针对Http编码,也为开发者提供了现成的编码器即HttpResponseEncoder。有了这些,就可以完成第一个版本,用于实现“返回资源列表”这个功能。主代码可以撰写如下
public class HttpServer
{
public void start() throws InterruptedException
{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup());
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.childHandler(new ChannelInitializer<SocketChannel>()
{
@Override
protected void initChannel(SocketChannel ch) throws Exception
{
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpRequestDecoder());
pipeline.addLast(new HttpResponseEncoder());
pipeline.addLast(new HttpObjectAggregator(1024));
pipeline.addLast(new ListFileHandler(new File("z:/")));
}
});
ChannelFuture bind = bootstrap.bind(80);
bind.sync();
bind.channel().closeFuture().sync();
}
public static void main(String[] args) throws InterruptedException
{
new HttpServer().start();
}
}
这是服务端程序,所以对于连接管道上的处理器,首先是Http请求解码器,而后是Http响应编码器,再次是用于聚合Http对象的聚合处理器,最后则是解析请求内容,处理业务数据,返回影响的资源展示处理器,即ListFileHandler。其代码如下
public class ListFileHandler extends ChannelInboundHandlerAdapter
{
private File dir;
private ObjectMapper mapper = new ObjectMapper();
private static final Charset CHARSET = Charset.forName("utf8");
public ListFileHandler(File dir) //使用了一个外部定义的文件夹对象,该文件夹下存储着所有可以展示的资源文件
{
this.dir = dir;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
String
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>
