netty4.1实现http文件服务器

刺骨的言语ヽ痛彻心扉 2023-07-04 11:28 73阅读 0赞

文章目录

  • 概述
  • 服务器编码
  • 文件服务器业务代码

注:更多netty相关文章请访问博主专栏: netty专栏

概述

netty版本:4.1.45

使用netty搭建一个简单的文件服务器,使用HTTP协议对外提供服务。

如果文件不存在,返回403响应码。如果是文件夹,以超链接展示,如果是文件,支持下载。

服务器编码

  1. /** * netty http 文件下载 服务器 */
  2. public class MyHttpFileBrowserServer {
  3. int port;
  4. public MyHttpFileBrowserServer(int port) {
  5. this.port = port;
  6. }
  7. public void start() throws Exception {
  8. ServerBootstrap bootstrap = new ServerBootstrap();
  9. EventLoopGroup boss = new NioEventLoopGroup();
  10. EventLoopGroup work = new NioEventLoopGroup();
  11. try {
  12. bootstrap.group(boss, work)
  13. .handler(new LoggingHandler(LogLevel.DEBUG))
  14. .channel(NioServerSocketChannel.class)
  15. .childHandler(new HttpBrowserServerInitializer());
  16. ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).sync();
  17. System.out.println("http server started . port : " + port);
  18. f.channel().closeFuture().sync();
  19. }catch (Exception e){
  20. e.printStackTrace();
  21. }finally {
  22. boss.shutdownGracefully();
  23. work.shutdownGracefully();
  24. }
  25. }
  26. public static void main(String[] args) throws Exception {
  27. MyHttpFileBrowserServer server = new MyHttpFileBrowserServer(8080);// 8081为启动端口
  28. server.start();
  29. }
  30. }
  31. class HttpBrowserServerInitializer extends ChannelInitializer<SocketChannel> {
  32. @Override
  33. protected void initChannel(SocketChannel channel) throws Exception {
  34. ChannelPipeline pipeline = channel.pipeline();
  35. pipeline.addLast(new HttpServerCodec())// http 编解器
  36. // http 消息聚合器 512*1024为接收的最大contentlength
  37. .addLast("httpAggregator", new HttpObjectAggregator(512 * 1024))
  38. // 支持异步发送大的码流(大的文件传输),但不占用过多的内存,防止java内存溢出
  39. .addLast("http-chunked", new ChunkedWriteHandler())
  40. .addLast(new HttpBrowserRequestHandler());// 请求处理器
  41. }
  42. }

注意这里channelpipeline中的HttpObjectAggregator和ChunkedWriteHandler。

  • HttpObjectAggregator:用于HTTP消息聚合,将多个消息转换为单一的FullHttpRequest和FullHttpResponse。因为HttpServerCodec解码器在每个HTTP消息中会生产多个对象:HttpRequest、HttpResponse、HttpContent、LastHttpContent。
  • ChunkedWriteHandler:支持异步发送大的码流(大的文件传输),但不占用过多的内存,防止java内存溢出。

文件服务器业务代码

HttpBrowserRequestHandler是业务代码的核心逻辑。用于检验请求的正确性,遍历文件列表,并以只读的方式读取文件,以流的方式发送到客户端。

这里只是实例代码,功能不是很完善。

  1. class HttpBrowserRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
  2. @Override
  3. public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
  4. // 解码失败 400
  5. if (!request.getDecoderResult().isSuccess()) {
  6. sendError(ctx, HttpResponseStatus.BAD_REQUEST);
  7. return;
  8. }
  9. // 只支持get方法
  10. if (request.getMethod() != HttpMethod.GET) {
  11. sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
  12. return;
  13. }
  14. //
  15. final String uri = request.getUri();
  16. // 处理Uri地址
  17. final String path = sanitizeUri(uri);
  18. if (path == null) {
  19. sendError(ctx, HttpResponseStatus.FORBIDDEN);
  20. return;
  21. }
  22. File file = new File(path);
  23. // 如果文件不存在,或不可访问
  24. if (file.isHidden() || !file.exists()) {
  25. sendError(ctx, HttpResponseStatus.NOT_FOUND);
  26. return;
  27. }
  28. // 如果请求是文件夹
  29. if (file.isDirectory()) {
  30. // 请求以 '/'结尾,列出该文件夹下的所有内容
  31. if (uri.endsWith("/")) {
  32. sendListing(ctx, file);
  33. } else {
  34. // 否则自动补全'/' 然后再重定向访问
  35. sendRedirect(ctx, uri + '/');
  36. }
  37. return;
  38. }
  39. if (!file.isFile()) {
  40. sendError(ctx, HttpResponseStatus.FORBIDDEN);
  41. return;
  42. }
  43. RandomAccessFile randomAccessFile = null;
  44. try {
  45. randomAccessFile = new RandomAccessFile(file, "r");// 以只读的方式打开文件
  46. } catch (FileNotFoundException fnfe) {
  47. sendError(ctx, HttpResponseStatus.NOT_FOUND);
  48. return;
  49. }
  50. long fileLength = randomAccessFile.length();
  51. // 创建一个默认的Http响应
  52. HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
  53. // 设置响应文件大小
  54. HttpUtil.setContentLength(response, fileLength);
  55. // 设置 content Type
  56. setContentTypeHeader(response, file);
  57. // 设置 keep alive
  58. if (HttpUtil.isKeepAlive(request)) {
  59. response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
  60. }
  61. ctx.write(response);
  62. //通过Netty的ChunkedFile对象直接将文件写入发送到缓冲区中
  63. ChannelFuture sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
  64. sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
  65. @Override
  66. public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) {
  67. if (total < 0) { // total unknown
  68. System.err.println("Transfer progress: " + progress);
  69. } else {
  70. System.err.println("Transfer progress: " + progress + " / " + total);
  71. }
  72. }
  73. @Override
  74. public void operationComplete(ChannelProgressiveFuture future) throws Exception {
  75. System.out.println("Transfer complete.");
  76. }
  77. });
  78. ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
  79. //如果不支持keep-Alive,服务器端主动关闭请求
  80. if (!HttpUtil.isKeepAlive(request)) {
  81. lastContentFuture.addListener(ChannelFutureListener.CLOSE);
  82. }
  83. }
  84. @Override
  85. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  86. cause.printStackTrace();
  87. if (ctx.channel().isActive()) {
  88. sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
  89. }
  90. }
  91. private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
  92. /** * 格式化uri并且获取路径 * * @param uri * @return */
  93. private String sanitizeUri(String uri) {
  94. try {
  95. uri = URLDecoder.decode(uri, "UTF-8");
  96. } catch (UnsupportedEncodingException e) {
  97. try {
  98. uri = URLDecoder.decode(uri, "ISO-8859-1");
  99. } catch (UnsupportedEncodingException e1) {
  100. throw new Error();
  101. }
  102. }
  103. if (!uri.startsWith("/")) {
  104. return null;
  105. }
  106. uri = uri.replace('/', File.separatorChar);
  107. if (uri.contains(File.separator + '.') || uri.contains('.' + File.separator) || uri.startsWith(".")
  108. || uri.endsWith(".") || INSECURE_URI.matcher(uri).matches()) {
  109. return null;
  110. }
  111. return System.getProperty("user.dir") + File.separator + uri;
  112. }
  113. private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
  114. private static void sendListing(ChannelHandlerContext ctx, File dir) {
  115. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
  116. response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
  117. StringBuilder buf = new StringBuilder();
  118. String dirPath = dir.getPath();
  119. buf.append("<!DOCTYPE html>\r\n");
  120. buf.append("<html><head><title>");
  121. buf.append(dirPath);
  122. buf.append(" 目录:");
  123. buf.append("</title></head><body>\r\n");
  124. buf.append("<h3>");
  125. buf.append(dirPath).append(" 目录:");
  126. buf.append("</h3>\r\n");
  127. buf.append("<ul>");
  128. buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n");
  129. for (File f : dir.listFiles()) {
  130. if (f.isHidden() || !f.canRead()) {
  131. continue;
  132. }
  133. String name = f.getName();
  134. if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
  135. continue;
  136. }
  137. buf.append("<li>链接:<a href=\"");
  138. buf.append(name);
  139. buf.append("\">");
  140. buf.append(name);
  141. buf.append("</a></li>\r\n");
  142. }
  143. buf.append("</ul></body></html>\r\n");
  144. ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
  145. response.content().writeBytes(buffer);
  146. buffer.release();
  147. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  148. }
  149. private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
  150. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
  151. response.headers().set(HttpHeaderNames.LOCATION, newUri);
  152. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  153. }
  154. private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
  155. FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
  156. Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
  157. response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
  158. ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  159. }
  160. private static void setContentTypeHeader(HttpResponse response, File file) {
  161. MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
  162. response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
  163. }
  164. }

运行HTTPserver服务器。

在浏览器访问 http://localhost:8080/
在这里插入图片描述

访问:http://localhost:8080/reactor3-helloworld/ 。 对于文件,可以直接下载到本地。
在这里插入图片描述
输入一个不合法的URL,返回403错误。
在这里插入图片描述

注:如果是java11版本,需要添加activation依赖。详见:java11:NoClassDefFoundError: javax/activation/MimetypesFileTypeMap

注:更多netty相关文章请访问博主专栏: netty专栏

发表评论

表情:
评论列表 (有 0 条评论,73人围观)

还没有评论,来说两句吧...

相关阅读