需求:
基于netty框架完成一个文件服务器,通过浏览器可以浏览相关的文件及下载文件。
目录结构
在跟src同级别目录下,放一个sources 文件夹,并向文件夹内放入一些文件,用于测试下载。
代码:
代码分为两个,一个是 HttpFileServer 用于做通道连接。另外一个是 HttpFileServerHandler 用于具体的业务处理。
1 import io.netty.bootstrap.ServerBootstrap; 2 import io.netty.channel.ChannelFuture; 3 import io.netty.channel.ChannelInitializer; 4 import io.netty.channel.EventLoopGroup; 5 import io.netty.channel.nio.NioEventLoopGroup; 6 import io.netty.channel.socket.SocketChannel; 7 import io.netty.channel.socket.nio.NioServerSocketChannel; 8 import io.netty.handler.codec.http.HttpObjectAggregator; 9 import io.netty.handler.codec.http.HttpRequestDecoder; 10 import io.netty.handler.codec.http.HttpResponseEncoder; 11 import io.netty.handler.stream.ChunkedWriteHandler; 12 13 public class HttpFileServer { 14 15 private static final String DEFAULT_URL="/sources/"; 16 17 public void run (final int port,final String url) throws Exception { 18 EventLoopGroup bossGroup = new NioEventLoopGroup(); 19 EventLoopGroup workerGroup = new NioEventLoopGroup(); 20 21 try { 22 ServerBootstrap b =new ServerBootstrap(); 23 b.group(bossGroup,workerGroup) 24 .channel(NioServerSocketChannel.class) 25 .childHandler(new ChannelInitializer<SocketChannel>() { 26 @Override 27 protected void initChannel(SocketChannel ch) throws Exception { 28 ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());// 解码器 29 ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536)); 30 ch.pipeline().addLast("http-encoder",new HttpResponseEncoder());// 编码器 31 ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());// 压缩解压 32 ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));// 实际的文件下载处理handler 33 } 34 }); 35 ChannelFuture future = b.bind(port).sync(); 36 System.out.println("运行在端口号 port = " + port); 37 future.channel().closeFuture().sync(); 38 }finally { 39 bossGroup.shutdownGracefully(); 40 workerGroup.shutdownGracefully(); 41 } 42 43 } 44 45 public static void main(String[] args) throws Exception{ 46 int port = 8765; 47 String url = DEFAULT_URL; 48 new HttpFileServer().run(port,url); 49 } 50 51 }
1 import java.io.File; 2 import java.io.FileNotFoundException; 3 import java.io.RandomAccessFile; 4 import java.io.UnsupportedEncodingException; 5 import java.net.URLDecoder; 6 import java.util.concurrent.ExecutionException; 7 import java.util.concurrent.TimeUnit; 8 import java.util.concurrent.TimeoutException; 9 import java.util.regex.Pattern; 10 11 import javax.activation.MimetypesFileTypeMap; 12 import javax.swing.text.html.MinimalHTMLWriter; 13 14 import io.netty.buffer.ByteBuf; 15 import io.netty.buffer.Unpooled; 16 import io.netty.channel.Channel; 17 import io.netty.channel.ChannelFuture; 18 import io.netty.channel.ChannelFutureListener; 19 import io.netty.channel.ChannelHandlerContext; 20 import io.netty.channel.ChannelProgressiveFuture; 21 import io.netty.channel.ChannelProgressiveFutureListener; 22 import io.netty.channel.SimpleChannelInboundHandler; 23 import io.netty.handler.codec.http.DefaultFullHttpResponse; 24 import io.netty.handler.codec.http.DefaultHttpResponse; 25 import io.netty.handler.codec.http.FullHttpRequest; 26 import io.netty.handler.codec.http.FullHttpResponse; 27 import io.netty.handler.codec.http.HttpHeaderNames; 28 import io.netty.handler.codec.http.HttpHeaderUtil; 29 import io.netty.handler.codec.http.HttpHeaderValues; 30 import io.netty.handler.codec.http.HttpHeaders; 31 import io.netty.handler.codec.http.HttpMessage; 32 import io.netty.handler.codec.http.HttpMethod; 33 import io.netty.handler.codec.http.HttpResponse; 34 import io.netty.handler.codec.http.HttpResponseStatus; 35 import io.netty.handler.codec.http.HttpVersion; 36 import io.netty.handler.codec.http.LastHttpContent; 37 import io.netty.handler.codec.http2.Http2Headers; 38 import io.netty.handler.stream.ChunkedFile; 39 import io.netty.util.CharsetUtil; 40 import io.netty.util.concurrent.Future; 41 import io.netty.util.concurrent.GenericFutureListener; 42 43 public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{ 44 45 private final String url; 46 47 public HttpFileServerHandler(String url) { 48 this.url = url; 49 } 50 51 @Override 52 protected void messageReceived(ChannelHandlerContext ctx, 53 FullHttpRequest request) throws Exception { 54 // 对请求的解码结果进行判断 55 if(!request.decoderResult().isSuccess()) 56 { 57 // 400 58 sendError(ctx, HttpResponseStatus.BAD_REQUEST); 59 return; 60 } 61 // 对请求方式进行判断,如果不是get方式(如post方式)则返回异常 62 if(request.method() != HttpMethod.GET) 63 { 64 // 405 65 sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); 66 return; 67 } 68 69 final String uri = request.uri(); 70 final String path = sanitizeUri(uri); 71 if(path == null) 72 { 73 // 403 74 sendError(ctx, HttpResponseStatus.FORBIDDEN); 75 return; 76 } 77 78 File file = new File(path); 79 if(file.isHidden() || !file.exists()) 80 { 81 // 404 82 sendError(ctx, HttpResponseStatus.NOT_FOUND); 83 return; 84 } 85 if(file.isDirectory()) 86 { 87 if(uri.endsWith("/")) 88 { 89 sendListing(ctx, file); 90 }else{ 91 sendRedirect(ctx, uri + "/"); 92 } 93 return; 94 } 95 if(!file.isFile()) 96 { 97 // 403 98 sendError(ctx, HttpResponseStatus.FORBIDDEN); 99 return; 100 } 101 102 RandomAccessFile randomAccessFile = null; 103 try{ 104 randomAccessFile = new RandomAccessFile(file, "r"); 105 }catch(FileNotFoundException fnfd){ 106 sendError(ctx, HttpResponseStatus.NOT_FOUND); 107 return; 108 } 109 110 long fileLength = randomAccessFile.length(); 111 HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 112 HttpHeaderUtil.setContentLength(response, fileLength); 113 // setContentLength(response, fileLength); 114 setContentTypeHeader(response, file); 115 116 117 118 if(HttpHeaderUtil.isKeepAlive(request)){ 119 response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 120 } 121 122 ctx.write(response); 123 ChannelFuture sendFileFuture = null; 124 sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise()); 125 sendFileFuture.addListener(new ChannelProgressiveFutureListener() { 126 127 @Override 128 public void operationComplete(ChannelProgressiveFuture future) 129 throws Exception { 130 System.out.println("Transfer complete."); 131 132 } 133 134 @Override 135 public void operationProgressed(ChannelProgressiveFuture future, 136 long progress, long total) throws Exception { 137 if(total < 0) 138 System.err.println("Transfer progress: " + progress); 139 else 140 System.err.println("Transfer progress: " + progress + "/" + total); 141 } 142 }); 143 144 ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 145 if(!HttpHeaderUtil.isKeepAlive(request)) 146 lastContentFuture.addListener(ChannelFutureListener.CLOSE); 147 148 } 149 150 @Override 151 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) 152 throws Exception { 153 cause.printStackTrace(); 154 if(ctx.channel().isActive()) 155 sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR); 156 } 157 158 private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&"].*"); 159 private String sanitizeUri(String uri){ 160 try{ 161 uri = URLDecoder.decode(uri, "UTF-8"); 162 }catch(UnsupportedEncodingException e){ 163 try{ 164 uri = URLDecoder.decode(uri, "ISO-8859-1"); 165 }catch(UnsupportedEncodingException e1){ 166 throw new Error(); 167 } 168 } 169 170 if(!uri.startsWith(url)) 171 return null; 172 if(!uri.startsWith("/")) 173 return null; 174 175 uri = uri.replace('/', File.separatorChar); 176 if(uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".") || uri.endsWith(".") 177 || INSECURE_URI.matcher(uri).matches()){ 178 return null; 179 } 180 return System.getProperty("user.dir") + File.separator + uri; 181 } 182 183 private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\.]*"); 184 185 private static void sendListing(ChannelHandlerContext ctx, File dir){ 186 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 187 // response.headers().set("CONNECT_TYPE", "text/html;charset=UTF-8"); 188 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); 189 190 String dirPath = dir.getPath(); 191 StringBuilder buf = new StringBuilder(); 192 193 buf.append("<!DOCTYPE html> "); 194 buf.append("<html><head><title>"); 195 buf.append(dirPath); 196 buf.append("目录:"); 197 buf.append("</title></head><body> "); 198 199 buf.append("<h3>"); 200 buf.append(dirPath).append(" 目录:"); 201 buf.append("</h3> "); 202 buf.append("<ul>"); 203 buf.append("<li>链接:<a href=" ../")..</a></li> "); 204 for (File f : dir.listFiles()) { 205 if(f.isHidden() || !f.canRead()) { 206 continue; 207 } 208 String name = f.getName(); 209 if (!ALLOWED_FILE_NAME.matcher(name).matches()) { 210 continue; 211 } 212 213 buf.append("<li>链接:<a href=""); 214 buf.append(name); 215 buf.append("">"); 216 buf.append(name); 217 buf.append("</a></li> "); 218 } 219 220 buf.append("</ul></body></html> "); 221 222 ByteBuf buffer = Unpooled.copiedBuffer(buf,CharsetUtil.UTF_8); 223 response.content().writeBytes(buffer); 224 buffer.release(); 225 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 226 } 227 228 229 private static void sendRedirect(ChannelHandlerContext ctx, String newUri){ 230 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND); 231 // response.headers().set("LOCATIN", newUri); 232 response.headers().set(HttpHeaderNames.LOCATION, newUri); 233 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 234 } 235 private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){ 236 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, 237 Unpooled.copiedBuffer("Failure: " + status.toString() + " ", CharsetUtil.UTF_8)); 238 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); 239 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 240 } 241 private static void setContentTypeHeader(HttpResponse response, File file){ 242 MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap(); 243 response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath())); 244 } 245 }
页面效果: