SpringBoot整合Netty实现高效文件上传下载实战指南

436 阅读2分钟

一、前言:为什么选择Netty?

在传统SpringBoot项目中,我们通常使用Servlet容器(如Tomcat)处理文件传输。但当面临高并发、大文件传输等场景时,基于NIO的Netty框架展现出了显著优势:

  1. 非阻塞I/O模型,支持更高并发连接
  2. 内存管理优化,减少GC压力
  3. 灵活的编解码器扩展
  4. 更细粒度的网络控制

本教程将手把手带你实现基于Netty的文件服务系统。


二、环境准备

2.1 技术栈

  • JDK 17+
  • SpringBoot 3.1.0
  • Netty 4.1.92
  • Lombok

2.2 项目初始化

<!-- pom.xml关键依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.92.Final</version>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

三、Netty服务端实现

3.1 服务端配置类

@Configuration
public class NettyServerConfig {

    @Value("${netty.port:8080}")
    private int port;

    @Bean
    public ServerBootstrap serverBootstrap(FileUploadHandler fileUploadHandler) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             protected void initChannel(SocketChannel ch) {
                 ChannelPipeline pipeline = ch.pipeline();
                 // HTTP编解码器
                 pipeline.addLast(new HttpServerCodec());
                 // 聚合HTTP报文
                 pipeline.addLast(new HttpObjectAggregator(65536));
                 // 大文件分块处理
                 pipeline.addLast(new ChunkedWriteHandler());
                 // 自定义业务处理器
                 pipeline.addLast(fileUploadHandler);
             }
         })
         .option(ChannelOption.SO_BACKLOG, 128)
         .childOption(ChannelOption.SO_KEEPALIVE, true);
        
        return b;
    }

    @PreDestroy
    public void shutdown() throws InterruptedException {
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }
}

3.2 文件上传处理器

@Slf4j
@ChannelHandler.Sharable
public class FileUploadHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    private static final String UPLOAD_PATH = "./uploads/";

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
        // 处理文件上传请求
        if (request.method() == HttpMethod.POST && "/upload".equals(request.uri())) {
            try {
                ByteBuf content = request.content();
                String fileName = parseFileName(request);
                saveFile(content, fileName);
                
                sendResponse(ctx, "Upload success: " + fileName, HttpResponseStatus.OK);
            } catch (Exception e) {
                sendResponse(ctx, "Upload failed: " + e.getMessage(), 
                            HttpResponseStatus.INTERNAL_SERVER_ERROR);
            }
        }
        // 处理文件下载请求
        else if (request.method() == HttpMethod.GET && request.uri().startsWith("/download/")) {
            String fileName = request.uri().substring("/download/".length());
            sendFile(ctx, fileName);
        }
    }

    private String parseFileName(HttpRequest request) {
        String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
        String[] tokens = contentType.split(";");
        for (String token : tokens) {
            if (token.trim().startsWith("filename=")) {
                return token.substring(token.indexOf('=')+1).trim().replace(""", "");
            }
        }
        return "unknown_" + System.currentTimeMillis();
    }

    private void saveFile(ByteBuf content, String fileName) throws IOException {
        Path path = Paths.get(UPLOAD_PATH + fileName);
        Files.createDirectories(path.getParent());
        try (FileOutputStream out = new FileOutputStream(path.toFile())) {
            content.readBytes(out, content.readableBytes());
        }
    }

    private void sendFile(ChannelHandlerContext ctx, String fileName) {
        File file = new File(UPLOAD_PATH + fileName);
        if (!file.exists()) {
            sendResponse(ctx, "File not found", HttpResponseStatus.NOT_FOUND);
            return;
        }

        try {
            RandomAccessFile raf = new RandomAccessFile(file, "r");
            long fileLength = raf.length();
            
            HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            HttpUtil.setContentLength(response, fileLength);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
            response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, 
                                 "attachment; filename="" + fileName + """);
            
            ctx.write(response);
            ctx.write(new ChunkedFile(raf), ctx.newProgressivePromise());
            ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        } catch (Exception e) {
            ctx.close();
            log.error("File transfer error", e);
        }
    }

    private void sendResponse(ChannelHandlerContext ctx, String message, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(message, CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}

四、SpringBoot整合

4.1 主启动类

@SpringBootApplication
public class NettyFileApplication implements CommandLineRunner {

    @Autowired
    private ServerBootstrap serverBootstrap;
    
    @Value("${netty.port:8080}")
    private int port;

    public static void main(String[] args) {
        SpringApplication.run(NettyFileApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        serverBootstrap.bind(port).sync();
        System.out.println("Netty server started on port: " + port);
    }
}

五、功能测试

5.1 文件上传测试

curl -X POST -F "file=@test.zip" http://localhost:8080/upload

5.2 文件下载测试

wget http://localhost:8080/download/test.zip

六、性能优化建议

  1. 内存管理

    • 使用PooledByteBufAllocator减少内存分配开销
    • 配置合适的maxContentLength
  2. 大文件处理

    • 采用零拷贝技术(FileRegion
    • 分块传输编码(Chunked Encoding)
  3. 安全增强

    • 文件大小限制
    • 文件类型白名单校验
    • 病毒扫描集成

七、常见问题排查

Q1:上传大文件时内存溢出

  • 检查是否遗漏ChunkedWriteHandler
  • 调整HttpObjectAggregator的maxContentLength

Q2:下载速度慢

  • 检查是否启用NATIVE传输
  • 增加SO_SNDBUFSO_RCVBUF

Q3:并发上传失败

  • 确认ChannelHandler是否标记为@Sharable
  • 检查线程模型配置

八、Last

通过本教程,我们实现了:

  • 基于Netty的文件上传下载核心逻辑
  • 与SpringBoot的优雅整合
  • 基础的安全防护措施

扩展方向建议:

  1. 集成分布式文件存储(MinIO/S3)
  2. 实现断点续传功能
  3. 添加文件加密传输
  4. 结合WebSocket实现进度实时推送