Netty HTTP协议--文件服务系统开发
- 介绍
- HTTP-文件系统场景
- 描述
- 流程图
- 代码展示
- netty依赖
- 服务端启动类 HttpFileServer
- 服务端业务逻辑处理类 HttpFileServerHandler
- 结果展示
- 错误路径
- 文件夹路径
- 文件路径
- 遗留bug
- bug版本
- 总结
介绍
由于Netty天生是异步事件驱动的架构,因此基于NIO TCP 协议栈开发的HTTP协议栈也是异步非阻塞的。Netty的HTTP协议栈无论在性能还是可靠性上,都表现优异,非常适合在非Web容器的场景下应用,相比于传统的Tomcat,Jetty等Web容器,它更加轻量和小巧,灵活性和定制性也更好。
HTTP-文件系统场景
描述
文件服务器使用HTTP协议对外提供服务,当客户通过浏览器访问文件服务器时,对访问路径进行检查,检查失败时返回HTTP403错误,如果校验通过,以链接的方式打开当前文件目录,每个目录或者文件都是个超链接,可以递归访问。
如果是目录,可以继续递归访问它下面的子目录或者文件,如果是文件且可读,则可以在浏览器直接打开。或者通过【目标另存为】下载该文件。
流程图
代码展示
netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId> <!-- Use 'netty5-all' for 5.0-->
<version>5.0.0.Alpha1</version>
<scope>compile</scope>
</dependency>
服务端启动类 HttpFileServer
public class HttpFileServer {
//默认路径,我们可以自定义自己的
private static final String DEFAULT_URL="/Netty-protobuf/";
public void run(final int port,final String url){
EventLoopGroup boosGroup=new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap=new ServerBootstrap();
bootstrap.group(boosGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//HTTP请求消息解码器
socketChannel.pipeline()
.addLast("http-decoder",new HttpRequestDecoder());
//HttpObjectAggregator解码器,它的作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
//原因是Http解码器在每个HTTP消息中会生成多个消息对象
socketChannel.pipeline()
.addLast("http-aggregator",new HttpObjectAggregator(65536));
socketChannel.pipeline()
.addLast("http-encoder",new HttpResponseEncoder());
//ChunkedWriteHandler 支持异步发送大的码流,但不占用过多的内存,防止发生Java内存溢出错误
socketChannel.pipeline()
.addLast("http-chunked",new ChunkedWriteHandler());
//HttpFileServerHandler文件服务器业务逻辑处理
socketChannel.pipeline()
.addLast(new HttpFileServerHandler(url));
}
});
ChannelFuture future=bootstrap.bind("127.0.0.1",port).sync();
System.out.println("Http 文件目录服务器启动,网址是: http://127.0.0.1:"+port+url);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//优雅退出
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new HttpFileServer().run(8080,DEFAULT_URL);
}
}
服务端业务逻辑处理类 HttpFileServerHandler
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String url;
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
protected void messageReceived(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception {
//如果解码失败,直接400返回
if (!fullHttpRequest.getDecoderResult().isSuccess()) {
sendError(channelHandlerContext, BAD_REQUEST);
return;
}
//如果不是get请求,则 405 返回
if (fullHttpRequest.getMethod() != HttpMethod.GET) {
sendError(channelHandlerContext, METHOD_NOT_ALLOWED);
return;
}
final String uri = fullHttpRequest.getUri();
final String path = sanitizeUri(uri);
System.out.println("请求的uri is = "+uri);
//路径不合法,path==null.返回403
if (path == null) {
sendError(channelHandlerContext, FORBIDDEN);
return;
}
File file = new File(path);
//如果文件不存在获知是系统隐藏文件,则 404 返回
if (file.isHidden() || !file.exists()) {
sendError(channelHandlerContext, NOT_FOUND);
return;
}
//如果是目录
if (file.isDirectory()) {
if (uri.endsWith("/")) {
//返回给客户端
sendListing(channelHandlerContext, file);
} else {
//进行重定向
sendRedirect(channelHandlerContext, uri + "/");
}
return;
}
//如果不是合法文件,则 403
if (!file.isFile()) {
sendError(channelHandlerContext, FORBIDDEN);
}
RandomAccessFile randomAccessFile = null;
try {
//以只读的方式打开文件
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException exception) {
//打开失败,返回 404
sendError(channelHandlerContext, NOT_FOUND);
return;
}
//获取文件的长度
long fileLength = randomAccessFile.length();
//构造响应体
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK);
//设置返回字节数,就是文件的字节数
//setContentLength(response, fileLength);
//设置content type
setContentTypeHeader(response, file);
//设置 connection
if (isKeepAlive(fullHttpRequest)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
//直接将 randomAccessFile 转换为 ByteBuf对象然后 flush。
//没有采用 ChannelFuture方式。这个方式 一直有个Bug无法解决。
byte[] b=new byte[(int)randomAccessFile.length()];
randomAccessFile.read(b);
ByteBuf byteBuf=Unpooled.copiedBuffer( b);
response.content().writeBytes(byteBuf);
byteBuf.release();
channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
/* channelHandlerContext.write(response);
//通过ChunkedFile 对象之间将文件写入到缓冲区中
ChannelFuture sendFileFuture = channelHandlerContext.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192),
channelHandlerContext.newProgressivePromise());
//增加 ChannelProgressiveFutureListener ,如果发送完成,打印 Transfer complete
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
//打印进度条
if (total < 0) {
System.err.println("Transfer progress : " + progress);
} else {
System.err.println("Transfer progress : " + progress + "/" + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);*/
System.out.println("do writeAndFlush ");
//if (isKeepAlive(fullHttpRequest)) {
//lastContentFuture.addListener(ChannelFutureListener.CLOSE);
// }
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
cause.printStackTrace();
if (context.channel().isActive()) {
sendError(context, INTERNAL_SERVER_ERROR);
}
}
private String sanitizeUri(String uri) {
try {
//使用JDK 的 java.net.URLDecoder 对uri 进行解码,使用UTF-8字符集
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
//出现异常 再用字符集 ISO-8859-1 解码
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException ex) {
throw new Error();
}
}
//解码成功后 对uri 进行校验
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;
}
//对文件进行拼接,使用当前运行程序所在的工程目录+URL 构造绝对路径
return System.getProperty("user.dir") + File.separator + uri;
}
private static void sendListing(ChannelHandlerContext context,File dir){
//构造响应消息
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,OK);
//设置消息头类型
response.headers().set(CONTENT_TYPE,"text/html;charset=UTF-8");
String dirPath=dir.getPath();
StringBuilder builder=new StringBuilder();
//编辑html 格式
builder.append("<!DOCTYPE html>\r\n");
builder.append("<html><head><title>");
builder.append(dirPath);
builder.append(" 目录: ");
builder.append("</title></head><body>\r\n");
builder.append("<h3>");
builder.append(dirPath).append(" 目录: ");
builder.append("</h3>\r\n");
builder.append("<ul>");
//..链接
builder.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;
}
//超链接
builder.append("<li> 链接: <a href=\"");
builder.append(name);
builder.append("\">");
builder.append(name);
builder.append("</a></li>\r\n");
}
builder.append("</ul></body></html>\r\n");
//构造缓冲对象
ByteBuf byteBuf= Unpooled.copiedBuffer(builder, CharsetUtil.UTF_8);
response.content().writeBytes(byteBuf);
byteBuf.release();
//将response 信息刷新到SocketChannel中
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext context,String newUri){
// 构造 响应体,302 重定向
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,FOUND);
response.headers().set(LOCATION,newUri);
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext context,HttpResponseStatus status){
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
status,Unpooled.copiedBuffer("Failure : "+status+"\r\n",CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE,"text/plain; charset=UTF-8");
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response,File file){
MimetypesFileTypeMap mimetypesFileTypeMap=new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getPath()));
}
}
结果展示
错误路径
文件夹路径
文件路径
遗留bug
其实最开始实现文件打开的逻辑,并不是 上面代码所展示的,直接将 RandomAccessFile 转换为ByteBuf,然后 writeFlush 到channel中去。而是采用的ChannelFuture方式。可以添加事件,打印下载打开文件的进度。比较优雅。但是一直无法解决Bug。网上也没有比较好的方案。所以也是一个遗憾。希望有大神可以解决这个问题。
bug版本
HttpFileServerHandler
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String url;
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
public HttpFileServerHandler(String url) {
this.url = url;
}
@Override
protected void messageReceived(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception {
//如果解码失败,直接400返回
if (!fullHttpRequest.getDecoderResult().isSuccess()) {
sendError(channelHandlerContext, BAD_REQUEST);
return;
}
//如果不是get请求,则 405 返回
if (fullHttpRequest.getMethod() != HttpMethod.GET) {
sendError(channelHandlerContext, METHOD_NOT_ALLOWED);
return;
}
final String uri = fullHttpRequest.getUri();
final String path = sanitizeUri(uri);
System.out.println("请求的uri is = "+uri);
//路径不合法,path==null.返回403
if (path == null) {
sendError(channelHandlerContext, FORBIDDEN);
return;
}
File file = new File(path);
//如果文件不存在获知是系统隐藏文件,则 404 返回
if (file.isHidden() || !file.exists()) {
sendError(channelHandlerContext, NOT_FOUND);
return;
}
//如果是目录
if (file.isDirectory()) {
if (uri.endsWith("/")) {
//返回给客户端
sendListing(channelHandlerContext, file);
} else {
//进行重定向
sendRedirect(channelHandlerContext, uri + "/");
}
return;
}
//如果不是合法文件,则 403
if (!file.isFile()) {
sendError(channelHandlerContext, FORBIDDEN);
}
RandomAccessFile randomAccessFile = null;
try {
//以只读的方式打开文件
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException exception) {
//打开失败,返回 404
sendError(channelHandlerContext, NOT_FOUND);
return;
}
//获取文件的长度
long fileLength = randomAccessFile.length();
//构造响应体
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, OK);
//设置返回字节数,就是文件的字节数
//setContentLength(response, fileLength);
//设置content type
setContentTypeHeader(response, file);
//设置 connection
if (isKeepAlive(fullHttpRequest)) {
response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
/* byte[] b=new byte[(int)randomAccessFile.length()];
randomAccessFile.read(b);
ByteBuf byteBuf=Unpooled.copiedBuffer( b);
response.content().writeBytes(byteBuf);
byteBuf.release();
channelHandlerContext.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);*/
channelHandlerContext.write(response);
//通过ChunkedFile 对象之间将文件写入到缓冲区中
ChannelFuture sendFileFuture = channelHandlerContext.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192),
channelHandlerContext.newProgressivePromise());
//增加 ChannelProgressiveFutureListener ,如果发送完成,打印 Transfer complete
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception {
//打印进度条
if (total < 0) {
System.err.println("Transfer progress : " + progress);
} else {
System.err.println("Transfer progress : " + progress + "/" + total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
System.out.println("do writeAndFlush ");
if (!isKeepAlive(fullHttpRequest)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable cause) {
cause.printStackTrace();
if (context.channel().isActive()) {
sendError(context, INTERNAL_SERVER_ERROR);
}
}
private String sanitizeUri(String uri) {
try {
//使用JDK 的 java.net.URLDecoder 对uri 进行解码,使用UTF-8字符集
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
try {
//出现异常 再用字符集 ISO-8859-1 解码
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (UnsupportedEncodingException ex) {
throw new Error();
}
}
//解码成功后 对uri 进行校验
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;
}
//对文件进行拼接,使用当前运行程序所在的工程目录+URL 构造绝对路径
return System.getProperty("user.dir") + File.separator + uri;
}
private static void sendListing(ChannelHandlerContext context,File dir){
//构造响应消息
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,OK);
//设置消息头类型
response.headers().set(CONTENT_TYPE,"text/html;charset=UTF-8");
String dirPath=dir.getPath();
StringBuilder builder=new StringBuilder();
//编辑html 格式
builder.append("<!DOCTYPE html>\r\n");
builder.append("<html><head><title>");
builder.append(dirPath);
builder.append(" 目录: ");
builder.append("</title></head><body>\r\n");
builder.append("<h3>");
builder.append(dirPath).append(" 目录: ");
builder.append("</h3>\r\n");
builder.append("<ul>");
//..链接
builder.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;
}
//超链接
builder.append("<li> 链接: <a href=\"");
builder.append(name);
builder.append("\">");
builder.append(name);
builder.append("</a></li>\r\n");
}
builder.append("</ul></body></html>\r\n");
//构造缓冲对象
ByteBuf byteBuf= Unpooled.copiedBuffer(builder, CharsetUtil.UTF_8);
response.content().writeBytes(byteBuf);
byteBuf.release();
//将response 信息刷新到SocketChannel中
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext context,String newUri){
// 构造 响应体,302 重定向
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,FOUND);
response.headers().set(LOCATION,newUri);
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext context,HttpResponseStatus status){
FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
status,Unpooled.copiedBuffer("Failure : "+status+"\r\n",CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE,"text/plain; charset=UTF-8");
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response,File file){
MimetypesFileTypeMap mimetypesFileTypeMap=new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getPath()));
}
}
总结
好了,代码比较简单,但是实现的功能比较cool。这个功能和nginx 的文件功能比较像。nginx 只需要配置参数就可以实现对应的功能。大家可以 敲敲代码,实现了功能,是不是发现对HTTP和Netty的理解又进了一步。