一起来学Netty - 编写自己的文件服务器

文章目录
  1. 1. HTTP协议介绍
    1. 1.1. 主要特点
    2. 1.2. 请求方式
  2. 2. 编写文件下载服务
    1. 2.1. HttpFileServer
    2. 2.2. HttpFileServerHandler
    3. 2.3. 实验一把
  3. 3. - 说点什么

前面已经讲了Netty的基本用法(请求/应答拆包/粘包序列化),本章以文件下载为例,编写一个相比传统TomcatJetty等容器更加轻量级的文件服务器案例……

HTTP协议介绍

HTTP是一个属于应用层面向对象的协议(HTTP1.OHTTP1.1HTTP2.0),简洁,快速响应,几乎适用各大行业应用,覆盖广泛,但相比HTTPS安全性较差(具体区别不做过多概述,有兴趣可以百度百科一下)……

主要特点

  • 支持Client/Server模式
  • 简单,简洁,客户端只需要根据指定URL,带上规定的参数或者消息体请求即可
  • 灵活,允许传输任意对象传输,内容类型由请求头的Content-Type标记
  • 无状态,不存在对事务处理记忆功能,若存在后续请求,则需重新传输之前相关信息(容易导致每次连接传输的数据量增大),但在另外一方面,无状态就可以带来快速响应与轻量级负载的优势…

请求方式

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
@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先通过RequestLineHeader解析成HttpRequest对象,传入到HttpObjectAggregator,然后再通过body解析出HttpContent对象,传入到HttpObjectAggregator,当HttpObjectAggregator发现是LastHttpContent,则代表HTTP协议解析完成,封装FullHttpRequest
  • 对于body内容的读取涉及到Content-Lengthtrunked两种方式,两种方式只是在解析协议时处理的不一致,最终输出是一致的。
  • ChunkedWriteHandler是为了支持异步发送过大数据流情况,不占用过多内存,防止JAVA内存溢出的问题…

挑优方案http://blog.csdn.net/xiangzhihong8/article/details/52029446

HttpFileServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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,将会看到如下日志输出

1
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

  • 个人QQ:1837307557
  • battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)