八、Netty 教程 – 编写自己的文件服务器
大约 7 分钟Java网络编程
作者:唐亚峰 | 出自:唐亚峰博客
前面已经讲了
Netty
的基本用法(请求/应答
,拆包/粘包
,序列化
),本章以文件下载
为例,编写一个相比传统Tomcat
,Jetty
等容器更加轻量级的文件服务器
案例……
HTTP协议介绍
HTTP
是一个属于应用层面向对象的协议(HTTP1.O
,HTTP1.1
,HTTP2.0
),简洁,快速响应,几乎适用各大行业应用,覆盖广泛,但相比HTTPS
安全性较差(具体区别不做过多概述,有兴趣可以百度百科一下)……
主要特点
- 支持Client/Server模式
- 简单,简洁,客户端只需要根据指定URL,带上规定的参数或者消息体请求即可
- 灵活,允许传输任意对象传输,内容类型由请求头的Content-Type标记
- 无状态,不存在对事务处理记忆功能,若存在后续请求,则需重新传输之前相关信息(容易导致每次连接传输的数据量增大),但在另外一方面,无状态就可以带来快速响应与轻量级负载的优势…
请求方式
GET:获取Request-URI所标识的资源,常见的查询操作
POST:在Request-URI所标识的资源后附加新的提交数据,可以存在消息体中,不一定体现在URL上,用于新增修改等操作
HEAD:请求获取Request-URI所标识的响应消息头
PUT:请求服务器存储的资源,以Request-URI做为标识,一般用作修改操作
DELETE:请求服务器删除Request-URI所标识的记录
TRACE:请求服务器回送收到的消息请求,测试或诊断
CONNECT:保留将来使用
OPTIONS:查询服务器性能,或查询与资源相关的选项和数据
标准路径:http://ip:port/path
HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。HTTP的头信息包括通用头、请求头、响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。
- 通用头标:即可用于请求,也可用于响应,是作为一个整体而不是特定资源与事务相关联。
- 请求头标:允许客户端传递关于自身的信息和希望的响应形式。
- 响应头标:服务器和于传递自身信息的响应。
- 实体头标:定义被传送资源的信息。即可用于请求,也可用于响应。
HTTP响应头和请求头信息对照表:http://tools.jb51.net/table/http_header 有兴趣的可以看下,里面有详细介绍与描述
HTTP响应状态码和描述信息:http://tools.jb51.net/table/http_status_code
编写文件下载服务
简单描述了下HTTP
,现在开始用Netty
给我们提供的HTTP
编写一个入门的服务端程序,含以下功能
- 路径映射
- 递归文件夹操作
- 文件下载
HttpFileServer
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast("http-decoder", new HttpRequestDecoder());
channel.pipeline().addLast("http-aggregator", new HttpObjectAggregator(8 * 1024));
channel.pipeline().addLast("http-encoder", new HttpResponseEncoder());
channel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
channel.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(path));
}
- 初始化添加HTTP相关编码器与解码器,对HTTP响应消息进行编码操作
- 如果把解析这块理解是一个黑盒的话,则输入是ByteBuf,输出是FullHttpRequest,通过该对象便可获取到所有与HTTP协议有关的信息。
- HttpRequestDecoder先通过RequestLine和Header解析成HttpRequest对象,传入到HttpObjectAggregator,然后再通过body解析出HttpContent对象,传入到HttpObjectAggregator,当HttpObjectAggregator发现是LastHttpContent,则代表HTTP协议解析完成,封装FullHttpRequest
- 对于body内容的读取涉及到Content-Length和trunked两种方式,两种方式只是在解析协议时处理的不一致,最终输出是一致的。
- ChunkedWriteHandler是为了支持异步发送过大数据流情况,不占用过多内存,防止JAVA内存溢出的问题…
挑优方案:http://blog.csdn.net/xiangzhihong8/article/details/52029446
HttpFileServerHandler
public class HttpFileServerHandler extends
SimpleChannelInboundHandler<FullHttpRequest> {
private final String path;
HttpFileServerHandler(String path) {
this.path = path;
}
@Override
public void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (!request.decoderResult().isSuccess()) {//判断解码结果,如果失败,回写400错误
sendError(ctx, BAD_REQUEST);
return;
}
if (request.method() != GET) {//判断请求方法,错误回写405
sendError(ctx, METHOD_NOT_ALLOWED);
return;
}
final String uri = request.uri();
final String path = sanitizeUri(uri);//构建映射后的路径
if (path == null) {//构建失败,回写403
sendError(ctx, FORBIDDEN);
return;
}
File file = new File(path);
if (file.isHidden() || !file.exists()) {//如果文件不存在,或者文件为隐藏,回写404
sendError(ctx, NOT_FOUND);
return;
}
if (file.isDirectory()) {//如果为目录,列出新目录下的文件
if (uri.endsWith("/")) {
sendListing(ctx, file);
} else {
sendRedirect(ctx, uri + '/');//否则打开或下载文件
}
return;
}
if (!file.isFile()) {//如果不是一个文件或者文件夹回写403
sendError(ctx, FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile;
try {
randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
} catch (FileNotFoundException e) {
sendError(ctx, NOT_FOUND);//异常情况,回写404
return;
}
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
setContentLength(response, fileLength);
setContentTypeHeader(response, file);
if (isKeepAlive(request)) {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0,
fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future,
long progress, long total) {
if (total < 0) { // 为知长度
System.err.println("进度: " + progress);
} else {
System.err.println("进度: " + progress + " / " + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = ctx
.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, 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("/")) {
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;
}
System.out.println(path + File.separator + uri);
return path + 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(HTTP_1_1, OK);
response.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
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);
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) {
//采用HTTP1.1协议传输
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
response.headers().set(LOCATION, newUri);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx,
HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1,
status, Unpooled.copiedBuffer("Failure: " + status.toString()
+ "\r\n", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,
mimeTypesMap.getContentType(file.getPath()));
}
}
- ChannelProgressiveFutureListener 可以监听当前Channel所关联的任务
实验一把
运行HttpFileServer
,将会看到如下日志输出
HTTP文件目录服务器启动,网址是 : http://127.0.0.1:4040
打开浏览器访问http://127.0.0.1:4040
,如图显示说明服务运行成功,然后就可以下载文件了…
– 说点什么
全文代码:https://git.oschina.net/battcn/battcn-netty/tree/master/Chapter8-1/battcn-netty-8-1-1