社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
上一节对Netty的HTTP支持进行了基本使用,实现了两个案例,这个章节,主要是使用Netty完成一个HTTP服务器,
主要完成了如下功能:
其实和之前的没什么变化
package com.firewolf.java.io.http.ftp;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NettyFileServer {
private String url; //文件服务器根目录
private Integer port; //服务监听端口号
public NettyFileServer(String url, Integer port) {
this.url = url;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workergGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workergGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch)
throws Exception {
//HTTP请求消息解码器
ch.pipeline().addLast("http-decoder",
new HttpRequestDecoder());
//将多个消息转换为单一的FullHttpRequest或者FullHttpResponse
ch.pipeline().addLast("http-aggregator",
new HttpObjectAggregator(65536));
//HTTP响应消息编码器
ch.pipeline().addLast("http-encoder",
new HttpResponseEncoder());
//支持异步发送大的码流,但不占用过多内存
ch.pipeline().addLast("http-chunked",
new ChunkedWriteHandler());
ch.pipeline().addLast(
new NettyFileServerHandler(url));
}
});
ChannelFuture f = b.bind(port).sync();
System.out.println("HTTP文件服务器启动,网址是:" + "http://127.0.0.1:" + port + url);
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workergGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new NettyFileServer("/", 8989).start();
}
}
package com.firewolf.java.io.http.ftp;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpUtil.isKeepAlive;
import static io.netty.handler.codec.http.HttpUtil.setContentLength;
import com.firewolf.java.io.http.utils.NettyHttpUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.regex.Pattern;
import javax.activation.MimetypesFileTypeMap;
public class NettyFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
//匹配不合格文件名
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\.]*");
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request)
throws Exception {
//对HTTP请求消息的解码结果进行判断
if (!request.decoderResult().isSuccess()) {
//如果解码失败直接构造400错误返回
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 = absoluteFileUrl(uri);
//如果构造的路径不合法就返回403错误
if (path == null) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
//使用URI路径构造file对象,如果是文件不存在或是隐藏文件就返回404
File file = new File(path);
if (file.isHidden() || !file.exists()) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
//如果是目录就发送目录的链接给客户端
if (file.isDirectory()) {
if (uri.endsWith("/")) {
showDirectory(ctx, file);
} else {
sendRedirect(ctx, uri + "/");
}
return;
}
//判断文件合法性
if (!file.isFile()) {
sendError(ctx, HttpResponseStatus.FORBIDDEN);
return;
}
if (FileUtils.isASCIIFileBySuffix(file)) {
//如果是文件文件,展示文本内容
BufferedReader reader = new BufferedReader(new FileReader(file));
String s = null;
StringBuffer result = new StringBuffer();
while ((s = reader.readLine()) != null) {//使用readLine方法,一次读一行
result.append(System.lineSeparator() + s);
}
boolean keepAlive = HttpUtil.isKeepAlive(request);
//构造响应数据
FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), OK,
Unpooled.wrappedBuffer(result.toString().getBytes()));
response.headers()
.set(CONTENT_TYPE, "text/plain;charset=utf-8"); //设置响应类型
//给客户端响应信息
NettyHttpUtils.sendResponse(ctx, request, keepAlive, response);
return;
} else {
//下载文件
downloadFile(ctx, request, file);
}
//如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中,
//来标示所有的消息体已经发送完成,同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
ChannelFuture lastContentFuture = ctx.
writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
//如果是非keepAlive的,最后一包消息发送完成后,服务端要主动断开连接
if (!isKeepAlive(request)) {
lastContentFuture.addListener(ChannelFutureListener.CLOSE);
}
}
/**
* 下载文件
*/
private void downloadFile(ChannelHandlerContext ctx, FullHttpRequest request, File file) throws IOException {
RandomAccessFile randomAccessFile = null;
try {
//以只读的方式打开文件,如果打开失败返回404错误
randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException e) {
sendError(ctx, HttpResponseStatus.NOT_FOUND);
return;
}
//获取文件的长度构造成功的HTTP应答消息
long fileLength = randomAccessFile.length();
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
setContentLength(response, fileLength);
setContentTypeHeader(response, file);
//判断是否是keepAlive,如果是就在响应头中设置CONNECTION为keepAlive
if (isKeepAlive(request)) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
ChannelFuture sendFileFuture;
//通过Netty的ChunkedFile对象直接将文件写入到发送缓冲区中
sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
//为sendFileFuture添加监听器,如果发送完成打印发送完成的日志
sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
@Override
public void operationComplete(ChannelProgressiveFuture future)
throws Exception {
System.out.println("Transfer complete.");
}
@Override
public void operationProgressed(ChannelProgressiveFuture future, long progress, long total)
throws Exception {
if (total < 0) {
System.err.println("Transfer progress: " + progress);
} else {
System.err.println("Transfer progress: " + progress + "/" + total);
}
}
});
}
private final String url;
public NettyFileServerHandler(String url) {
this.url = url;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
if (ctx.channel().isActive()) {
sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
}
//获取文件的绝对路径
private String absoluteFileUrl(String uri) {
try {
//使用UTF-8对URL进行解码
uri = URLDecoder.decode(uri, "UTF-8");
} catch (Exception e) {
try {
//解码失败就使用ISO-8859-1进行解码
uri = URLDecoder.decode(uri, "ISO-8859-1");
} catch (Exception e2) {
//仍然失败就返回错误
throw new Error();
}
}
//解码成功后对uri进行合法性判断,避免访问无权限的目录
if (!uri.startsWith(url)) {
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;
}
//使用当前运行程序所在的工程目录+URI构造绝对路径
return System.getProperty("user.dir") + File.separator + uri;
}
//发送目录的链接到客户端浏览器
private static void showDirectory(ChannelHandlerContext ctx, File dir) {
//创建成功的http响应消息
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//设置消息头的类型是html文件,不要设置为text/plain,客户端会当做文本解析
response.headers().set(CONTENT_TYPE, "text/html;charset=UTF-8");
//构造返回的html页面内容
StringBuilder buf = new StringBuilder();
String dirPath = dir.getPath();
buf.append("<!DOCTYPE html>rn");
buf.append("<html><head><title>");
buf.append(dirPath);
buf.append("目录:");
buf.append("</title></head><body>rn");
buf.append("<h3>");
buf.append(dirPath).append("目录:");
buf.append("</h3>rn");
buf.append("<ul>");
buf.append("<li>链接:<a href="../">..</a></li>rn");
for (File f : dir.listFiles()) {
if (f.isHidden() || !f.canRead()) {
continue;
}
String name = f.getName();
if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
continue;
}
String type = f.isDirectory() ? "目录" : "文件";
buf.append("<li>").append(type).append(":<a href="");
buf.append(name);
buf.append("">");
buf.append(name);
buf.append("</a></li>rn");
}
buf.append("</ul></body></html>rn");
//分配消息缓冲对象
ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
//将缓冲区的内容写入响应对象,并释放缓冲区
response.content().writeBytes(buffer);
buffer.release();
//将响应消息发送到缓冲区并刷新到SocketChannel中
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void sendRedirect(ChannelHandlerContext ctx, String newUri) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
response.headers().set(LOCATION, newUri);
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.toString() + "rn", CharsetUtil.UTF_8));
response.headers().set(CONTENT_TYPE, "text/html;charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private static void setContentTypeHeader(HttpResponse response, File file) {
MimetypesFileTypeMap mimetypesTypeMap = new MimetypesFileTypeMap();
response.headers().set(CONTENT_TYPE, mimetypesTypeMap.getContentType(file.getPath()));
}
}
public class FileUtils {
/**************************** 先使用文件名后缀来判断文件类型 ****************************/
public static String getFileSuffix(File file) {
String fileName = file.getName();
return fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 判断是否是文本文件
*/
public static boolean isASCIIFileBySuffix(File file) {
List<String> asciis = Arrays.asList("html", "xml", "txt", "java", "md");
return asciis.contains(getFileSuffix(file).toLowerCase());
}
}
点击pom.xml:
点击java-io.iml:
点击文件夹,则会进入下一层。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!