Netty学习——使用Netty快速编写一个文件服务器
最终效果
简介
本Demo是使用Netty快速的创建一个文件服务器。实现了以下功能:
- 展示目录或文件,当为文件时显示文件大小
- 点击目录可进入自目录
- 可返回上一级
- 点击文件可下载文件
Demo环境:
- jdk1.8
- Netty 4.1.74.Final
实现代码
服务端
package vip.huhailong.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import vip.huhailong.handler.HttpFileServerHandler;
/**
* HTTP服务端
*/
public class HttpFileServer {
private static final String DEFAULT_URL = "/";
public void run(final int port, final String url) throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//http消息解码器
socketChannel.pipeline().addLast("http-decoder",new HttpRequestDecoder());
//将消息转为单一的FullHttpRequest或者FullHttpResponse,因为http解码器在每个http消息中会生成多个消息对象
socketChannel.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65535));
//对响应消息进行编码
socketChannel.pipeline().addLast("http-encoder",new HttpResponseEncoder());
//支持异步发送大大码流,但不占用过多但内存,防止发生Java内存溢出
socketChannel.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
socketChannel.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
}
});
ChannelFuture channelFuture = b.bind(port).sync();
System.out.println("HTTP File Server Url: http://127.0.0.1:"+port+url);
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new HttpFileServer().run(8080,"/");
}
}
服务端处理类
package vip.huhailong.handler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DecimalFormat;
import java.util.Objects;
import java.util.regex.Pattern;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpUtil.setContentLength;
/**
* HTTP File Server Handler
*/
public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String url;
private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9.]*");
public HttpFileServerHandler(String url){
this.url = url;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
System.out.println("come in uri is : "+request.uri());
/**
* 对http请求进行解码,如果解码失败则返回400错误
*/
if(!request.decoderResult().isSuccess()){
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
return;
}
/**
* 对请求方法进行判断,如果不是get请求直接返回405错误
*/
if(request.method() != HttpMethod.GET){
sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
return;
}
final String uri = request.uri();
final String path = sanitizeUri(uri);
if(path == null){
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
File file = new File(path);
if(file.isHidden()||!file.exists()){
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
if(file.isDirectory()){
if(uri.endsWith("/")){
sendListing(ctx, file);
}else{
sendRedirect(ctx, uri+'/');
}
return;
}
if(!file.isFile()){
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
RandomAccessFile randomAccessFile;
try{
randomAccessFile = new RandomAccessFile(file,"r"); //只读模式
} catch (FileNotFoundException e){
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
long fileLength = randomAccessFile.length(); //文件大小
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
setContentLength(response,fileLength);
setContentTypeHeader(response,file);
if(isKeepAlive(request)){
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) {
if(total < 0){
System.err.println("Transfer progress:"+progress);
}else{
System.err.println("Transfer progress:"+progress+"/"+total);
}
}
@Override
public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) {
System.out.println("Transfer complete.");
}
});
ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
if(!isKeepAlive(request)){
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
if(ctx.channel().isActive()){
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 对url进行编码校验
* @param uri 访问对url
* @return 返回校验后对路径
*/
private String sanitizeUri(String uri){
try{
uri = URLDecoder.decode(uri, CharsetUtil.UTF_8.name()); //对URL进行编码
} catch (UnsupportedEncodingException e){
try{
uri = URLDecoder.decode(uri,CharsetUtil.ISO_8859_1.name());
} catch (UnsupportedEncodingException e2){
throw new Error(e2.getLocalizedMessage());
}
}
if(!uri.startsWith(url)&&!"/favicon.ico".equals(uri)){
return null;
}
if(!uri.startsWith("/")){
return null;
}
uri = uri.replace('/',File.separatorChar);
if(uri.contains(File.separator+'.')||uri.contains('.'+File.separator)||uri.startsWith(".")||uri.endsWith(".")
||INSECURE_URI.matcher(uri).matches()){
return null;
}
return File.separator+uri;
}
private static void sendListing(ChannelHandlerContext ctx, File dir){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=UTF-8");
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
buf.append("<!DOCTYPE html>\r\n");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("</title></head><body style='background-color:#eeeeee;'>\r\n");
buf.append("<h3 style='background-color:#333333; color:white; padding:10px; border-radius:5px;'>");
buf.append("当前路径:");
buf.append(dirPath);
buf.append("</h3>\r\n");
buf.append("<ul>");
buf.append("<li><a href=\"../\">⬅︎ 上一级</a></li>\r\n");
DecimalFormat df = new DecimalFormat("######0.00");
for(File f : Objects.requireNonNull(dir.listFiles())){
if(f.isHidden()||!f.exists()){
continue;
}
String name = f.getName();
if(!ALLOWED_FILE_NAME.matcher(name).matches()){
continue;
}
buf.append("<li><div style=\"display:flex\"><div style='min-width:300px;'>");
if(f.isDirectory()){
buf.append("\uD83D\uDCC1");
}else{
buf.append("\uD83D\uDCC4");
}
buf.append("<a style='margin-left:5px;' href=\"");
buf.append(name);
buf.append("\">");
buf.append(name);
buf.append("</a></div><div style='margin-left:10px; font-weight:bold;'>");
if(!f.isDirectory()){
double v = f.length() / 1024.0;
buf.append(df.format(v));
buf.append("KB");
}
buf.append("</div></div></li>\r\n");
}
buf.append("</ul></body></html>\r\n");
ByteBuf buffer = Unpooled.copiedBuffer(buf,CharsetUtil.UTF_8);
response.content().writeBytes(buffer);
buffer.release();
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUrl){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.FOUND);
response.headers().set(HttpHeaderNames.LOCATION,newUrl);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status){
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,status,Unpooled.copiedBuffer("Failure: "+status+"\r\n",CharsetUtil.UTF_8));
response.headers().set(HttpHeaderNames.CONTENT_TYPE,HttpHeaderValues.TEXT_PLAIN);
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file){
MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
response.headers().set(HttpHeaderNames.CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getParent()));
}
}
通过上面两个文件就可以实现文件服务器了,服务端的代码和前面我们练习的服务端代码写法类似,唯一的区别是在自定义处理类前面我们加了Http协议相关的一些解码器:
HttpRequestDecoder
:对HTTP请求消息进行解码器。HttpObjectAggregator
:将多个消息转换为单一的FullHttpRequest
或者FullHttpResponse
,因为HTTP解码器在每个HTTP消息中会生成多个消息对象:HttpRequest
/HttpResponse
HttpContent
LastHttpContent
HttpResponseEncoder
:对HTTP响应消息进行编码。ChunkedWriteHandler
:用于支持异步发送大的码流(例如大的文件传输),但不占用过多的内存,防止发生Java内存溢出错误。HttpFileServerHandler
:我们自定义的用于文件服务器的逻辑处理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律