一、前言:为什么选择Netty?
在传统SpringBoot项目中,我们通常使用Servlet容器(如Tomcat)处理文件传输。但当面临高并发、大文件传输等场景时,基于NIO的Netty框架展现出了显著优势:
- 非阻塞I/O模型,支持更高并发连接
- 内存管理优化,减少GC压力
- 灵活的编解码器扩展
- 更细粒度的网络控制
本教程将手把手带你实现基于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
六、性能优化建议
-
内存管理
- 使用
PooledByteBufAllocator减少内存分配开销 - 配置合适的
maxContentLength
- 使用
-
大文件处理
- 采用零拷贝技术(
FileRegion) - 分块传输编码(Chunked Encoding)
- 采用零拷贝技术(
-
安全增强
- 文件大小限制
- 文件类型白名单校验
- 病毒扫描集成
七、常见问题排查
Q1:上传大文件时内存溢出
- 检查是否遗漏
ChunkedWriteHandler - 调整
HttpObjectAggregator的maxContentLength
Q2:下载速度慢
- 检查是否启用NATIVE传输
- 增加
SO_SNDBUF和SO_RCVBUF
Q3:并发上传失败
- 确认ChannelHandler是否标记为@Sharable
- 检查线程模型配置
八、Last
通过本教程,我们实现了:
- 基于Netty的文件上传下载核心逻辑
- 与SpringBoot的优雅整合
- 基础的安全防护措施
扩展方向建议:
- 集成分布式文件存储(MinIO/S3)
- 实现断点续传功能
- 添加文件加密传输
- 结合WebSocket实现进度实时推送