Netty 实现HTTP文件服务器
一,需求
文件服务器使用HTTP协议对外提供服务。用户通过浏览器访问文件服务器,首先对URL进行检查,若失败返回403错误;若通过校验,以链接的方式打开当前目录,每个目录或文件都以超链接的形式展现,可递归访问,并下载文件。
二,关键实现代码
①文件服务器启动类
需要添加的通道处理器如下:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
}
1) HttpRequestDecoder
Decodes {@link ByteBuf}s into {@link HttpRequest}s and {@link HttpContent}s.
它负责把字节解码成Http请求。
2) HttpObjectAggregator
A {@link ChannelHandler} that aggregates an {@link HttpMessage} and its following {@link HttpContent}s into a single {@link FullHttpRequest} or {@link FullHttpResponse} (depending on if it used to handle requests or responses)
它负责把多个HttpMessage组装成一个完整的Http请求或者响应。到底是组装成请求还是响应,则取决于它所处理的内容是请求的内容,还是响应的内容。这其实可以通过Inbound和Outbound来判断,对于Server端而言,在Inbound 端接收请求,在Outbound端返回响应。
It is useful when you don't want to take care of HTTP messages whose transfer encoding is 'chunked'.
如果Server向Client返回的数据指定的传输编码是 chunked。则,Server不需要知道发送给Client的数据总长度是多少,它是通过分块发送的,参考分块传输编码
Be aware that you need to have the {@link HttpResponseEncoder} or {@link HttpRequestEncoder} before the {@link HttpObjectAggregator} in the {@link ChannelPipeline}.
注意,HttpObjectAggregator通道处理器必须放到HttpRequestDecoder或者HttpRequestEncoder后面。
3) HttpResponseEncoder
当Server处理完消息后,需要向Client发送响应。那么需要把响应编码成字节,再发送出去。故添加HttpResponseEncoder处理器。
4)ChunkedWriteHandler
A {@link ChannelHandler} that adds support for writing a large data stream asynchronously neither spending a lot of memory nor getting {@link OutOfMemoryError}.
该通道处理器主要是为了处理大文件传输的情形。大文件传输时,需要复杂的状态管理,而ChunkedWriteHandler实现这个功能。
5) HttpFileServerHandler
自定义的通道处理器,其目的是实现文件服务器的业务逻辑。
通道处理器添加完毕之后,需要启动服务器。代码如下:
ChannelFuture f = b.bind("localhost", port).sync();
f.channel().closeFuture().sync();
因为在Netty中所有的事件都是异步的,因此bind操作是一个异步操作,通道的关闭也是一个异步操作。因此使用ChannelFuture来作为一个 palceholder,代表操作执行之后的结果。
最后关闭事件线程,代码如下:
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
②文件处理器类
HttpFileServerHandler.java是自定义的通道处理器,用来实现HTTP文件服务器的业务逻辑。从上面添加的Handler可以看出,在HTTP文件服务器的实现过程中,Netty已经为我们解决了很多工作,如:HttpRequestDecoder自动帮我们解析HTTP请求(解析byte);再比如:HttpObjectAggregator把多个HTTP请求中的数据组装成一个,当服务器发送的response事先不知道响应的长度时就很有用。
文件处理器通过继承SimpleChannelInboundHandler来实现,代码如下:
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private final String url;
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
protected void messageReceived(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
if(!request.decoderResult().isSuccess())
{
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
if(request.method() != HttpMethod.GET)
{
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
当服务器接收到消息时,会自动触发 messageReceived方法。该方法首先对URL进行判断,并只接受GET请求。
相关的验证通过后,通过RandomAccessFile类打开文件,并构造响应。
RandomAccessFile randomAccessFile = null;
try{
randomAccessFile = new RandomAccessFile(file, "r");
}catch(FileNotFoundException fnfd){
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
} long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
如果请求中带有“KEEP-ALIVE”,则不关闭连接。
if(HttpHeaderUtil.isKeepAlive(request)){
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
进行数据的发送
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete."); }
ctx.write(response);
当发送完数据之后,由于采用的是Transfer-Encoding:chunk模式来传输数据,因此需要在发送一个长度为0的chunk用来标记数据传输完成。
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if(!HttpHeaderUtil.isKeepAlive(request))
lastContentFuture.addListener(ChannelFutureListener.CLOSE); }
使用Keep-Alive,可以减少HTTP连接建立的次数,在HTTP1.1中该选项是默认开启的。
Connection: Keep-Alive When the server processes the request and generates a response, it also adds a header to the response: Connection: Keep-Alive When this is done, the socket connection is not closed as before, but kept open after sending the response. When the client sends another request, it reuses the same connection. The connection will continue to be reused until either the client or the server decides that the conversation is over, and one of them drops the connection.
在使用Keep-Alive的情况下,当Server处理了Client的请求且生成一个response后,在response的头部添加Connection: Keep-Alive选项,把response返回给client,此时Socket连接并不会关闭。
【若没有Keep-Alive,一次HTTP请求响应之后,本次Socket连接就关闭了】
由于连接还没有关闭,当client再发送另一个请求时,就会重用这个Socket连接,直至其中一方drops the connection.
关于Keep-Alive的讨论,参考:
整个源码参考:
package httpFileServer; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler; public class HttpFileServer {
private static final String DEFAULT_URL = "/src/"; public void run(final int port, final String url)throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(); try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
}
}); ChannelFuture f = b.bind("localhost", port).sync();
System.out.println("HTTP 文件服务器启动, 地址是: " + "http://localhost:" + port + url);
f.channel().closeFuture().sync(); }finally{
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
} public static void main(String[] args)throws Exception {
int port = 8888;
if(args.length > 0)
{
try{
port = Integer.parseInt(args[0]);
}catch(NumberFormatException e){
port = 8080;
}
} String url = DEFAULT_URL;
if(args.length > 1)
url = args[1];
new HttpFileServer().run(port, url);
}
} package httpFileServer; import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern; import javax.activation.MimetypesFileTypeMap;
import javax.swing.text.html.MinimalHTMLWriter; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener; public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{ private final String url; public HttpFileServerHandler(String url) {
this.url = url;
} @Override
protected void messageReceived(ChannelHandlerContext ctx,
FullHttpRequest request) throws Exception {
if(!request.decoderResult().isSuccess())
{
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
if(request.method() != HttpMethod.GET)
{
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
} final String uri = request.uri();
final String path = sanitizeUri(uri);
if(path == null)
{
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
} File file = new File(path);
if(file.isHidden() || !file.exists())
{
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
if(file.isDirectory())
{
if(uri.endsWith("/"))
{
sendListing(ctx, file);
}else{
sendRedirect(ctx, uri + "/");
}
return;
}
if(!file.isFile())
{
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
} RandomAccessFile randomAccessFile = null;
try{
randomAccessFile = new RandomAccessFile(file, "r");
}catch(FileNotFoundException fnfd){
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
} long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpHeaderUtil.setContentLength(response, fileLength);
// setContentLength(response, fileLength);
setContentTypeHeader(response, file); if(HttpHeaderUtil.isKeepAlive(request)){
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
} ctx.write(response);
ChannelFuture sendFileFuture = null;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() { @Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete."); } @Override
public void operationProgressed(ChannelProgressiveFuture future,
long progress, long total) throws Exception {
if(total < 0)
System.err.println("Transfer progress: " + progress);
else
System.err.println("Transfer progress: " + progress + "/" + total);
}
}); ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if(!HttpHeaderUtil.isKeepAlive(request))
lastContentFuture.addListener(ChannelFutureListener.CLOSE); } @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if(ctx.channel().isActive())
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
} private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private String sanitizeUri(String uri){
try{
uri = URLDecoder.decode(uri, "UTF-8");
}catch(UnsupportedEncodingException e){
try{
uri = URLDecoder.decode(uri, "ISO-8859-1");
}catch(UnsupportedEncodingException e1){
throw new Error();
}
} if(!uri.startsWith(url))
return null;
if(!uri.startsWith("/"))
return null; uri = uri.replace('/', File.separatorChar);
if(uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".")
|| INSECURE_URI.matcher(uri).matches()){
return null;
}
return System.getProperty("user.dir") + File.separator + uri;
} private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*"); private static void sendListing(ChannelHandlerContext ctx, File dir){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
// response.headers().set("CONNECT_TYPE", "text/html;charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); String dirPath = dir.getPath();
StringBuilder buf = new StringBuilder(); buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("目录:");
buf.append("</title></head><body>\r\n"); buf.append("<h3>");
buf.append(dirPath).append(" 目录:");
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li>链接:<a href=\" ../\")..</a></li>\r\n");
for (File f : dir.listFiles()) {
if(f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
} buf.append("<li>链接:<a href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></li>\r\n");
} buf.append("</ul></body></html>\r\n"); ByteBuf buffer = Unpooled.copiedBuffer(buf,CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} private static void sendRedirect(ChannelHandlerContext ctx, String newUri){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
// response.headers().set("LOCATIN", newUri);
response.headers().set(HttpHeaderNames.LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file){
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath()));
}
}
Netty 实现HTTP文件服务器的更多相关文章
- netty系列之:搭建自己的下载文件服务器
目录 简介 文件的content-type 客户端缓存文件 其他HTTP中常用的处理 文件内容展示处理 文件传输进度 总结 简介 上一篇文章我们学习了如何在netty中搭建一个HTTP服务器,讨论了如 ...
- Netty实现简单HTTP服务器
netty package com.dxz.nettydemo.http; import java.io.UnsupportedEncodingException; import io.netty.b ...
- YII Framework学习教程-YII的日志
日志的作用(此处省略1000字) YII中的日志很好很强大,允许你把日志信息存放到数据库,发送到制定email,存放咋文件中,意见显示页面是,甚至可以用来做性能分析. YII中日志的基本配置:/yii ...
- netty系列之:搭建HTTP上传文件服务器
目录 简介 GET方法上传数据 POST方法上传数据 POST方法上传文件 总结 简介 上一篇的文章中,我们讲到了如何从HTTP服务器中下载文件,和搭建下载文件服务器应该注意的问题,使用的GET方法. ...
- Netty+SpringBoot写一个基于Http协议的文件服务器
本文参考<Netty权威指南> NettyApplication package com.xh.netty; import org.springframework.boot.SpringA ...
- Netty权威指南
Netty权威指南(异步非阻塞通信领域的经典之作,国内首本深入剖析Netty的著作,全面系统讲解原理.实战和源码,带你完美进阶Netty工程师.) 李林锋 著 ISBN 978-7-121-233 ...
- HTTP协议开发应用-文件服务器
HTTP(超文本传输协议)协议是建立在TCP传输协议之上的应用层协议.HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统. 本文将重点介绍如何基于Netty的 ...
- netty高级篇(3)-HTTP协议开发
一.HTTP协议简介 应用层协议http,发展至今已经是http2.0了,拥有以下特点: (1) CS模式的协议 (2) 简单 - 只需要服务URL,携带必要的请求参数或者消息体 (3) 灵活 - 任 ...
- Netty笔记——技术点汇总
目录 · Linux网络IO模型 · 文件描述符 · 阻塞IO模型 · 非阻塞IO模型 · IO复用模型 · 信号驱动IO模型 · 异步IO模型 · BIO编程 · 伪异步IO编程 · NIO编程 · ...
随机推荐
- vue中eventbus被多次触发(vue中使用eventbus踩过的坑)【bus.$on事件被多次绑定】
问题描述:只要页面没有强制刷新,存在组件切换,bus.$on方法会被多次绑定,造成事件多次触发 触发bus.$on中绑定的方法.png bus.$on多次绑定.png 解决办法:在每次调用方法 ...
- [转帖].NET Core 2.0 是您的最好选择吗?
.NET Core 2.0 是您的最好选择吗? https://www.cnblogs.com/vipyoumay/p/7388371.html 1. NET Core 2.0 是您的最好选择吗? 1 ...
- Docker镜像加速设置
地址:https://www.daocloud.io/mirror#accelerator-doc 配置 Docker 加速器 Linux MacOS Windows curl -sSL https: ...
- php开发APP接口(总结一)
一.什么是app接口:服务端与客户端的数据交互. 大部分APP接口是通过http协议通信的. http通信的三要素: URL: 通信的地址 Method:通信的方式(get | post | pu ...
- ComboBox中如何嵌套TreeView控件
在ComboBox中嵌套TreeView控件,有时候我们在设计界面的时候,由于界面设计的需要,我们需要将TreeView控件嵌套在ComboBox中,因为TreeView控件实在是太占用地方了,要 ...
- 通过反射来读取XML格式的ControlTemplate
在之前的一个WPF项目中,由于设置控件模板在前台xaml中读取失败,由此想到了通过反射的形式来读取该模板,首先将该模板写入一个xml文件中,然后再读取该xml文件,在这里首先介绍一下:资源和嵌入式资源 ...
- sort和uniq的应用实例
sort 排序 uniq 1.语法:sort [option]... [file]... 2.选项:-k key,关键子,指定以那个列来排序.如果不指定,默认将正行作为关键字排序-n 对数值排序.默认 ...
- 国产首款5G手机抢先亮相:如此给力的说!
5G网络是接下来移动互联网发展的主旋律,各家都在努力跟进,目前最积极的当属手机厂商,而2019年我们就能看到多款5G手机降临了. 在11月27日的未来信息通信技术国际研讨会上,vivo展示了他们正在研 ...
- std::shared_ptr 和普通指针的转换
相互转化见示例 struct test { int num; string name; }; test* pTest = new test(); std::shared_ptr<test> ...
- echarts之简单的入门——【二】再增加一个柱状图和图例组件
echarts之简单的入门——[一]做个带时间轴的柱状统计图 现在需求说,我需要知道日答题总次数和活跃人数,那么我们如何在上面的图表中增加一个柱状图呢? 如果你看过简单入门中的配置项手册中series ...