【Netty系列】实现HTTP文件服务器

目录

一、完整代码实现

[1. Maven依赖 (pom.xml)](#1. Maven依赖 (pom.xml))

[2. 主启动类 (FileServer.java)](#2. 主启动类 (FileServer.java))

[3. 通道初始化类 (FileServerInitializer.java)](#3. 通道初始化类 (FileServerInitializer.java))

[4. 核心业务处理器 (FileServerHandler.java)](#4. 核心业务处理器 (FileServerHandler.java))

二、代码关键解释

[1. 架构分层](#1. 架构分层)

[2. 安全防护机制](#2. 安全防护机制)

[3. 文件传输优化](#3. 文件传输优化)

[4. 目录列表生成](#4. 目录列表生成)

三、运行与测试

[1. 准备测试环境](#1. 准备测试环境)

[2. 启动服务器](#2. 启动服务器)

[3. 测试用例](#3. 测试用例)

四、扩展建议

[1. 添加HTTPS支持](#1. 添加HTTPS支持)

[2. 实现断点续传](#2. 实现断点续传)

[3. 添加缓存控制](#3. 添加缓存控制)


以下是基于Netty 4.1的完整HTTP文件服务器实现,包含详细注释和关键功能说明:

一、完整代码实现

1. Maven依赖 (pom.xml)
复制代码
<dependencies>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.86.Final</version>
    </dependency>
</dependencies>
2. 主启动类 (FileServer.java)
复制代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class FileServer {
    private static final int PORT = 8080;
    private static final String BASE_DIR = "server_files";

    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new FileServerInitializer(BASE_DIR))
             .option(ChannelOption.SO_BACKLOG, 128)
             .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(PORT).sync();
            System.out.println("File server started at port: " + PORT);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
3. 通道初始化类 (FileServerInitializer.java)
复制代码
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.stream.ChunkedWriteHandler;

public class FileServerInitializer extends ChannelInitializer<SocketChannel> {
    private final String baseDir;

    public FileServerInitializer(String baseDir) {
        this.baseDir = baseDir;
    }

    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
      
        // HTTP协议编解码器
        pipeline.addLast(new HttpServerCodec());
      
        // 聚合HTTP完整请求(最大支持64KB)
        pipeline.addLast(new HttpObjectAggregator(65536));
      
        // 支持分块大文件传输
        pipeline.addLast(new ChunkedWriteHandler());
      
        // 自定义业务处理器
        pipeline.addLast(new FileServerHandler(baseDir));
    }
}
4. 核心业务处理器 (FileServerHandler.java)
复制代码
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final String baseDir;

    public FileServerHandler(String baseDir) {
        this.baseDir = baseDir;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
        try {
            // 1. 验证HTTP方法
            if (!HttpMethod.GET.equals(request.method())) {
                sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
                return;
            }

            // 2. 路径安全处理
            String uri = request.uri();
            if (uri.contains("..") || uri.contains("/.")) {
                sendError(ctx, HttpResponseStatus.FORBIDDEN);
                return;
            }

            // 3. 获取物理文件路径
            Path filePath = Paths.get(baseDir, uri).normalize().toAbsolutePath();
          
            // 4. 验证文件是否存在
            if (!Files.exists(filePath)) {
                sendError(ctx, HttpResponseStatus.NOT_FOUND);
                return;
            }

            // 5. 处理目录请求
            if (Files.isDirectory(filePath)) {
                sendDirectoryListing(ctx, filePath);
                return;
            }

            // 6. 发送文件内容
            sendFile(ctx, filePath.toFile());
        } catch (Exception e) {
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private void sendFile(ChannelHandlerContext ctx, File file) throws Exception {
        RandomAccessFile raf = new RandomAccessFile(file, "r");
        long fileLength = raf.length();

        // 构建HTTP响应头
        HttpResponse response = new DefaultHttpResponse(
            HttpVersion.HTTP_1_1, 
            HttpResponseStatus.OK
        );
        HttpUtil.setContentLength(response, fileLength);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, 
            Files.probeContentType(file.toPath()));
      
        // 发送响应头
        ctx.write(response);

        // 零拷贝传输文件内容(SSL和非SSL场景处理不同)
        if (ctx.pipeline().get(SslHandler.class) == null) {
            ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength));
        } else {
            ctx.write(new ChunkedFile(raf));
        }

        // 结束传输
        ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        future.addListener(f -> raf.close());
    }

    private void sendDirectoryListing(ChannelHandlerContext ctx, Path dir) {
        StringBuilder sb = new StringBuilder()
            .append("<html><head><title>Index of ")
            .append(dir.getFileName())
            .append("</title></head><body><h1>Index of ")
            .append(dir.getFileName())
            .append("</h1><hr><pre>");

        try {
            Files.list(dir).forEach(path -> {
                String name = path.getFileName().toString();
                sb.append("<a href=\"")
                  .append(name)
                  .append("\">")
                  .append(name)
                  .append("</a>")
                  .append("        ")
                  .append(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(
                      new Date(path.toFile().lastModified())))
                  .append("        ")
                  .append(Files.isDirectory(path) ? "-" : path.toFile().length())
                  .append("\n");
            });
        } catch (Exception e) {
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
            return;
        }

        sb.append("</pre><hr></body></html>");
      
        FullHttpResponse response = new DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1,
            HttpResponseStatus.OK,
            Unpooled.copiedBuffer(sb.toString(), StandardCharsets.UTF_8)
        );
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(
            HttpVersion.HTTP_1_1,
            status,
            Unpooled.copiedBuffer("Error: " + status + "\r\n", StandardCharsets.UTF_8)
        );
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

二、代码关键解释

1. 架构分层
  • 主启动类:负责Netty服务端的启动配置
  • 初始化器:配置管道(Pipeline)的处理链
  • 业务处理器:实现核心文件服务逻辑
2. 安全防护机制
复制代码
// 路径注入防护
if (uri.contains("..") || uri.contains("/.")) {
    sendError(ctx, HttpResponseStatus.FORBIDDEN);
    return;
}

// 文件路径标准化处理
Path filePath = Paths.get(baseDir, uri).normalize().toAbsolutePath();
3. 文件传输优化
复制代码
// 零拷贝技术(DefaultFileRegion)
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength));

// SSL环境使用分块传输(ChunkedFile)
ctx.write(new ChunkedFile(raf));
4. 目录列表生成
复制代码
// 动态生成HTML目录索引
Files.list(dir).forEach(path -> {
    // 构建带文件属性的超链接
});

三、运行与测试

1. 准备测试环境
复制代码
mkdir -p server_files/test
echo "Hello Netty" > server_files/test.txt
dd if=/dev/urandom of=server_files/largefile.bin bs=1M count=100
2. 启动服务器
复制代码
mvn clean package exec:java -Dexec.mainClass="FileServer"
3. 测试用例
复制代码
# 获取文本文件
curl http://localhost:8080/test.txt

# 列出目录内容
curl http://localhost:8080/test/

# 下载大文件
wget http://localhost:8080/largefile.bin

# 错误请求测试
curl -v http://localhost:8080/../etc/passwd

四、扩展建议

1. 添加HTTPS支持
复制代码
// 在初始化器中添加SSL处理器
SslContext sslCtx = SslContextBuilder.forServer(cert, key).build();
pipeline.addFirst("ssl", sslCtx.newHandler(ch.alloc()));
2. 实现断点续传
复制代码
// 解析Range请求头
String rangeHeader = request.headers().get(HttpHeaderNames.RANGE);
if (rangeHeader != null) {
    // 处理形如"bytes=0-100"的请求
    // 设置206 Partial Content状态
    // 使用FileRegion指定传输范围
}
3. 添加缓存控制
复制代码
response.headers()
    .set(HttpHeaderNames.CACHE_CONTROL, "max-age=3600")
    .set(HttpHeaderNames.EXPIRES, new Date(System.currentTimeMillis() + 3600000));

该实现具备完整的文件服务功能,实际生产部署时建议增加:

  1. 访问日志记录
  2. 限速控制
  3. 身份验证
  4. 病毒扫描集成
  5. 监控指标采集

可根据具体业务需求进行功能扩展和性能调优。

相关推荐
北极象3 小时前
在Flutter中定义全局对象(如$http)而不需要import
网络协议·flutter·http
tiandyoin9 小时前
Chrome 通过FTP,HTTP 调用 Everything 浏览和搜索本地文件系统
前端·chrome·http·ftp·everything
小妖6669 小时前
nginx 编译添加 ngx_http_proxy_connect_module 模块
运维·nginx·http
花月C9 小时前
复杂业务场景下 JSON 规范设计:Map<String,Object>快速开发 与 ResponseEntity精细化控制HTTP 的本质区别与应用场景解析
java·前端·后端·http
全岛铁盒202310 小时前
生成https 证书步骤
网络协议·http·https
C137的本贾尼11 小时前
HTTPS
网络协议·http·https
00后程序员张12 小时前
面对 UI 差异化的调试难题:本地多设备测试中的 WebDebugX 应用实录
websocket·网络协议·tcp/ip·http·网络安全·https·udp
孤寂大仙v18 小时前
【计算机网络】应用层协议Http——构建Http服务服务器
服务器·计算机网络·http
触角云科技18 小时前
智慧充电桩数字化管理平台:环境监测与动态数据可视化技术有哪些作用?
netty
白棂1 天前
面试题——计算机网络:HTTP和HTTPS的区别?
计算机网络·http·https