Netty开发HTTP文件服务器

开发HTTP协议的服务端涉及到请求和响应,使用的类io.netty.handler.codec.http.HttpHeaderNames和io.netty.handler.codec.http.HttpHeaderUtil在netty-all-5.0.0.Alpha2.jar包中才有在netty-all-5.0.0.Alpha1.jar中没有,需要注意。netty-all-5.0.0.Alpha2.jar包下载地址:https://bintray.com/netty/downloads/netty/

1、首先是HTTP文件服务器的启动类HttpFileServer

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 HttpFileServer
{
    //文件服务器的根目录
    private static final String DEFAULT_URL="/src/com/exp/netty/";
    
    public void run(final int port,final String url)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()
            {

                @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 HttpFileServerHandler(url));
                }
                
            });
            ChannelFuture f=b.bind("127.0.0.1",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
    {
        int port =8888;
        if (args!=null&&args.length>0)
        {
            port=Integer.valueOf(args[0]);
        }
        new HttpFileServer().run(port, DEFAULT_URL);
    }
}

2、文件服务器的业务逻辑处理类

import static io.netty.handler.codec.http.HttpHeaderNames.*;
import static io.netty.handler.codec.http.HttpHeaderUtil.*;
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.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;

public class HttpFileServerHandler extends SimpleChannelInboundHandler 
{
    private final String url;
    public HttpFileServerHandler(String url){
        this.url=url;
    }
    
    //消息接入方法
    @Override
    protected void messageReceived(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=sanitizeUri(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("/")){
                sendListing(ctx,file);
            }else {
                sendRedirect(ctx,uri+"/");
            }
            return;
        }
        //判断文件合法性
        if(!file.isFile()){
            sendError(ctx,HttpResponseStatus.FORBIDDEN);
            return;
        }
        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);
                }
            }
        });
        //如果使用chunked编码,最后需要发送一个编码结束的空消息体,将LastHttpContent.EMPTY_LAST_CONTENT发送到缓冲区中,
        //来标示所有的消息体已经发送完成,同时调用flush方法将发送缓冲区中的消息刷新到SocketChannel中发送
        ChannelFuture lastContentFuture=ctx.
            writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        //如果是非keepAlive的,最后一包消息发送完成后,服务端要主动断开连接
        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);
        }
    }
    
    private static final Pattern INSECURE_URI=Pattern.compile(".*[<>&\"].*");
    
    private String sanitizeUri(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 final Pattern ALLOWED_FILE_NAME=Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
    //发送目录的链接到客户端浏览器
    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(CONTENT_TYPE,"text/html;charset=UTF-8");
        //构造返回的html页面内容
        StringBuilder buf=new StringBuilder();
        String dirPath=dir.getPath();
        buf.append("\r\n");
        buf.append("");
        buf.append(dirPath);
        buf.append("目录:");
        buf.append("\r\n");
        buf.append("

"); buf.append(dirPath).append("目录:"); buf.append("

\r\n"); buf.append("
    "); buf.append("
  • 链接:..
  • \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("
  • 链接:"); buf.append(name); buf.append("
  • \r\n"); } buf.append("
\r\n"); //分配消息缓冲对象 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()+"\r\n",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())); } }

客户端通过浏览器访问:

Netty开发HTTP文件服务器_第1张图片

如果访问不存在的文件:

Netty开发HTTP文件服务器_第2张图片

访问无权限的目录:

Netty开发HTTP文件服务器_第3张图片

Chrome上测试点击文件会自动下载。

参考书籍《Netty权威指南》

你可能感兴趣的:(Netty)