Netty 入门案例之静态文件服务器实现
最近用Netty实现了一个类似于Nginx的静态文件服务器功能。遂在个人博客中记录下过程
设计阶段
功能设计
- 支持自定义配置文件(提供默认配置文件和外部配置文件两种方式)
- 支持静态文件路由配置
- 静态文件下载
实现过程
配置读取
设计一个ResourcesService类,使用单例模式,在程序启动时读取默认路径下的config.properties文件。同时也读取Jar文件所在目录的的config.properties文件。
静态文件的路由配置需要按照规则,使用web.static.path做为前缀。例如web.static.path.hello.a=/data/static/,那么请求的url为 /hello/a/test.js时,就会去/data/static 目录下找test.js文件
ResourcesService 中有2个map对象,一个用于存储路由配置,另一个用于存储其他配置,暂时没有用到。
Http请求解析与响应
可以使用ByteBuf对Http请求做解析,但是这样很浪费时间,Netty内置了Http请求编解码器和Http响应编解码器,我们可以使用它对Http请求进行解析与响应。由于这次写的是服务端,所以只需要用到Http请求解码器和Http响应编码器,他们分别是HttpRequestDecoder 和 HttpResponseEncoder,仅仅使用这两个编/解码器还不够,HttpRequestDecoder会将Http请求解析为 HttpResquest、HttpContent、LastHttpContent 这几个对象。所以我们需要在引入一个HttpObjectAggregator 解码器,它可以将多个消息体转换为单一的HttpFullRequest对象。最后我们还要使用一个ChunkedWriteHandler 帮助我们可以在程序中异步发送文件的码流,而且不会占用过多的内存导致Java内存溢出。
参考代码
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel socketChannel) throws Exception {
//HttpObjectAggregator HTTP 消息解码器, 作用时将多个消息转换为1个FullHttpRequest 或者 FullHttpResponse 对象
/**
* HttpRequestDecoder 会将每个 HTTP 消息转换为 多个消息对象
* HttpResquest / HttpResponse
* HttpContent
* LastHttpContent
*/
//将请求和应答消息编码或解码为HTTP消息
socketChannel.pipeline().addLast(new HttpRequestDecoder());
socketChannel.pipeline().addLast(new HttpObjectAggregator(65536));// 目的是将多个消息转换为单一的request或者response对象
socketChannel.pipeline().addLast(new HttpResponseEncoder());
socketChannel.pipeline().addLast(new ChunkedWriteHandler());//目的是支持异步大文件传输()
socketChannel.pipeline().addLast("file-handler", new FileServerHandler());
socketChannel.pipeline().addLast("handler", new ServerHandler(false));
}
}
FileServerHandler就是我们的静态文件服务器主要的处理逻辑,如果请求不是对静态文件的请求,那么就会进入下一个ServerHandler,ServerHandler主要是用于转发请求(目前还未实现)
静态文件处理的思路
首先从请求中拿到请求的URI。先判断是否为静态文件请求,支持GET请求,如果不是,则当前Handler处理完成,直接进入下一个Handler中进行处理。
如果是静态文件请求,则解析其URL,去ResourcesService类的单例对象中找到是否有对应得配置路径。如果没有,则返回404。如果有对应的物理路径那么需要将拼接好物理路径,使用RandomAccessFile对象对文件进行读取,然后使用ChunkedNioFile对其进行封装返回Http响应。
参考代码
public class FileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) throws Exception {
//request.retain();
HttpResponse response = null;
RandomAccessFile randomAccessFile = null;
try{
// 状态为1xx的话,继续请求
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(channelHandlerContext);
}
String uri = request.uri();
if(!uri.endsWith(".js") && !uri.endsWith(".css") && !uri.endsWith(".html")){
channelHandlerContext.fireChannelRead(request);
return;
}
// hello/a.js
int index = uri.lastIndexOf("/") + 1;
if(index == -1){
DonkeyHttpUtil.writeResponse(request, OK, channelHandlerContext);
return;
}
String filename = uri.substring(index);
uri = uri.substring(0, index-1);
String path = ResourcesService.getInstance().getPath(uri);
if(StringUtil.isNullOrEmpty(path)){
DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
return;
}
String fullPath = path+ "/"+filename;
File file = new File(fullPath);
try {
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException e) {
DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
e.printStackTrace();
return;
}
if(!file.exists() || file.isHidden()){
DonkeyHttpUtil.writeResponse(request, NOT_FOUND, channelHandlerContext);
return;
}
long fileLength = randomAccessFile.length();
response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
setContentType(response, file);
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
channelHandlerContext.write(response);
ChannelFuture sendFileFuture = channelHandlerContext.write(new ChunkedNioFile(randomAccessFile.getChannel()), channelHandlerContext.newProgressivePromise());
// 写入文件尾部
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture future,
long progress, long total) {
if (total < 0) { // total unknown
System.out.println("Transfer progress: " + progress);
} else {
System.out.println("Transfer progress: " + progress + " / "
+ total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if (!keepAlive) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}finally {
if(randomAccessFile != null){
try {
randomAccessFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void setContentType(HttpResponse response, File file){
//MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
if(file.getName().endsWith(".js")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/x-javascript");
}else if(file.getName().endsWith(".css")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/css; charset=UTF-8");
}else if (file.getName().endsWith(".html")){
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
}
实现效果
总结
- 本次学习了如何使用Netty对Http请求做响应,对Netty中的FullHttpRequest,DefaultHttpResponse等类有了深入的了解,对Http协议也了解了更多。
- 虽然实现了静态文件服务器的基本功能,但是缺点也有很多,1.配置文件的设计做得不够灵活;2.没有实现请求转发功能
完整代码地址: https://github.com/catch-fish-with-the-hands/Netty-WEB/tree/zxc_netty_web_server