按照《Netty权威指南》书中的内容 HTTP文件服务器,服务端已经接收到了消息,也将响应消息写入到缓存并冲刷,但是页面却显示没有什么响应。不知道为什么?
把上面的代码发出来看看,上面应该要绑定一个网络输出流。
package com.lsh.netty.http; 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.util.regex.Pattern; /** * @author :LiuShihao * @date :Created in 2021/6/2 11:55 上午 * @desc :HttpFile文件服务器处理类 */ public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private final String url ; public HttpFileServerHandler(String url) { this.url = url; } //当服务器接收到消息时,会自动触发 messageReceived方法 @Override protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { try { // 5.0.0 request.getDecoderRequest() 已经被弃用 //对HTTP请求消息的解码结果进行判断,如果解码失败则返回400错误 if (!request.decoderResult().isSuccess()) { System.out.println("解码失败返回400"); sendError(ctx, HttpResponseStatus.BAD_REQUEST); return; } //对请求方法进行判断,如果不是从浏览器或者表单设置为GET发起的请求(例如POST),则返回405错误 if (request.method() != HttpMethod.GET) { sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED); System.out.println("请求方式不是GET,返回405"); return; } //如果URI不合法 返回403错误 final String uri = request.uri(); System.out.println("request.uri : " + uri); final String path = sanitizeUri(uri); System.out.println("path : " + path); if (path == null) { sendError(ctx, HttpResponseStatus.FORBIDDEN); System.out.println("URI不合法 返回403错误"); return; } //如果文件不存在或者是系统隐藏文件 则返回404错误 File file = new File(path); if (file.isHidden() || !file.exists()) { sendError(ctx, HttpResponseStatus.NOT_FOUND); System.out.println("文件不存在或者是系统隐藏文件 则返回404错误"); return; } System.out.println("fileName : " + file.getName()); if (file.isDirectory()) { if (uri.endsWith("/")) { sendListing(ctx, file); } else { sendRedirect(ctx, uri + "/"); } return; } //判断文件合法性 if (!file.isFile()) { sendError(ctx, HttpResponseStatus.FORBIDDEN); return; } RandomAccessFile randomAccessFile = null; try { //以只读方式打开文件 randomAccessFile = new RandomAccessFile(file, "r"); } catch (FileNotFoundException e) { sendError(ctx, HttpResponseStatus.NOT_FOUND); return; } //获取文件的长度构造成功的HTTP应答消息 long fileLength = randomAccessFile.length(); // DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); HttpHeaderUtil.setContentLength(response, fileLength); setContentTypeHeader(response, file); //判断是否是keepAlive,如果是就在响应头中设置CONNECTION为keepAlive if (HttpHeaderUtil.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 channelProgressiveFuture) throws Exception { System.out.println("Transfer complete."); } @Override public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) throws Exception { if (total < 0) { System.err.println("Transfer progress: " + progress); } else { System.err.println("Transfer progress: " + progress + "/" + total); } } }); //如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中, //来标示所有的消息体已经发送完成,同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送 ChannelFuture lastContentFuture = ctx. writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //如果是非keepAlive的,最后一包消息发送完成后,服务端要主动断开连接 if (!HttpHeaderUtil.isKeepAlive(request)) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } }catch (Throwable e){ System.out.println(e.getMessage()); e.printStackTrace(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){ cause.printStackTrace(); if(ctx.channel().isActive()){ sendError(ctx,HttpResponseStatus.INTERNAL_SERVER_ERROR); } } private static final Pattern INSECURE_URI=Pattern.compile(".*[<>&\"].*"); /** * * * @param uri * @return */ private String sanitizeUri(String uri){ //对URL进行解码 解码成功后对URI进行合法性判断 如果URI与允许访问的URI一直或者是其子目录(文件),则检验通过否则返回空 try { uri = URLDecoder.decode(uri,"UTF-8"); }catch (UnsupportedEncodingException e){ try { uri = URLDecoder.decode(uri,"ISO-8859-1"); }catch (UnsupportedEncodingException e1){ throw new Error(); } } //解码成功后对uri进行合法性判断,避免访问无权限的目录 if(!uri.startsWith(url)){ return null; } if(!uri.startsWith("/")){ return null; } //将硬编码的文件路径分隔符替换为本地操作系统的文件路径分割符 uri = uri.replace('/',File.separatorChar); //对新的URI做二次合法性校验,如果校验失败则直接返回空 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 final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*"); /** * 发送目录的链接到客户端浏览器 * @param ctx * @param dir */ private static void sendListing(ChannelHandlerContext ctx,File dir){ //创建成功的http响应消息 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); //设置消息头的类型是html文件,不要设置为text/plain,客户端会当做文本解析 response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); //构造返回的html页面内容 StringBuilder buf = new StringBuilder(); String dirPath = dir.getPath(); buf.append("<!DOCTYPE html>\r\n"); buf.append("<html><head><title>"); buf.append(dirPath); buf.append("目录:"); buf.append("</title></head><body>\r\n"); buf.append("<h3>"); buf.append(dirPath).append("目录:"); buf.append("</h3>\r\n"); buf.append("<ul>"); buf.append("<li>链接:<a href=\"../\">..</a></li>\r\n"); for (File f : dir.listFiles()) { if (f.isHidden() || !f.canRead()) { continue; } String name = f.getName(); if (!ALLOWED_FILE_NAME.matcher(name).matches()) { continue; } buf.append("<li>链接:<a href=\""); buf.append(name); buf.append("\">"); buf.append(name); buf.append("</a></li>\r\n"); } buf.append("</ul></body></html>\r\n"); System.out.println("buf :" + buf); //分配消息缓冲对象 ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8); //将缓冲区的内容写入响应对象,并释放缓冲区 response.content().writeBytes(buffer); buffer.release(); //将响应消息发送到缓冲区并刷新到SocketChannel中 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); System.out.println("=========返回响应消息========="); } private static void sendRedirect(ChannelHandlerContext ctx,String newUri){ FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND); response.headers().set(HttpHeaderNames.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()+"\r\n",CharsetUtil.UTF_8)); response.headers().set(HttpHeaderNames.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(HttpHeaderNames.CONTENT_TYPE,mimetypesTypeMap.getContentType(file.getPath())); } }
package com.lsh.netty.http;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
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.HttpResponseDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @author :LiuShihao
* @date :Created in 2021/6/2 9:10 上午
* @desc :HTTP 服务端开发
*/
public class HttpFileServer {
private String ipAddress = "127.0.0.1";
private static final String DEFAULT_URL = "/src/com/exp/netty/";
public void run(final int port,final String url) throws Exception{
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//想ChannelPipeline中添加HTTP请求消息解码器
ch.pipeline().addLast("http-decoder",new HttpRequestDecoder());
//添加HttpObjectAggregator解码器,作用是将多个消息转换为单一的FullHttpRequest或者FullHttpResponse,
// 原因是HTTP解码器在每个HTTP消息中会生成多个消息对象。
ch.pipeline().addLast("http-aggregator",new HttpObjectAggregator(65536));
//增加HTTP响应编码器,对HTTP响应消息进行编码
ch.pipeline().addLast("http-encoder", new HttpResponseDecoder());
//添加Chunked handler 作用是支持异步发送大的码流(例如文件传输),但不占用过多的内存,防止发生Java内存溢出错误
ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());
//最后添加HTTPFileServerHandler 用于文件服务器的业务逻辑处理
ch.pipeline().addLast("fileServerHandler",new HttpFileServerHandler(url));
}
});
ChannelFuture future = b.bind(ipAddress, port).sync();
System.out.println("HTTP 文 件 目 录 服 务 器 启 动 ,网 址 是 :http://"+ipAddress+":"+port+url);
future.channel().closeFuture().sync();
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
String url = DEFAULT_URL;
new HttpFileServer().run(port,url);
}
}