目录
[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));
该实现具备完整的文件服务功能,实际生产部署时建议增加:
- 访问日志记录
- 限速控制
- 身份验证
- 病毒扫描集成
- 监控指标采集
可根据具体业务需求进行功能扩展和性能调优。