关于netty:Netty-实现HTTP文件服务器

4次阅读

共计 13426 个字符,预计需要花费 34 分钟才能阅读完成。

一,需要

文件服务器应用 HTTP 协定对外提供服务。用户通过浏览器拜访文件服务器,首先对 URL 进行查看,若失败返回 403 谬误;若通过校验,以链接的形式关上当前目录,每个目录或文件都以超链接的模式展示,可递归拜访,并下载文件。

二,要害实现代码

①文件服务器启动类

须要增加的通道处理器如下:

@Override
                protected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                    ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                    ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                    ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                    ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
                }

1) HttpRequestDecoder

Decodes {@link ByteBuf}s into {@link HttpRequest}s and {@link HttpContent}s.

它负责把字节解码成 Http 申请。

2) HttpObjectAggregator

A {@link ChannelHandler} that aggregates an {@link HttpMessage} and its following {@link HttpContent}s into a single {@link FullHttpRequest} or {@link FullHttpResponse} (depending on if it used to handle requests or responses)

它负责把多个 HttpMessage 组装成一个残缺的 Http 申请或者响应。到底是组装成申请还是响应,则取决于它所解决的内容是申请的内容,还是响应的内容。这其实能够通过 Inbound 和 Outbound 来判断,对于 Server 端而言,在 Inbound 端接管申请,在 Outbound 端返回响应。

It is useful when you don’t want to take care of HTTP messages whose transfer encoding is ‘chunked’.

如果 Server 向 Client 返回的数据指定的传输编码是 chunked。则,Server 不须要晓得发送给 Client 的数据总长度是多少,它是通过分块发送的,参考分块传输编码

Be aware that you need to have the {@link HttpResponseEncoder} or {@link HttpRequestEncoder} before the {@link HttpObjectAggregator} in the {@link ChannelPipeline}.

留神,HttpObjectAggregator 通道处理器必须放到 HttpRequestDecoder 或者 HttpRequestEncoder 前面。

3) HttpResponseEncoder

当 Server 解决完音讯后,须要向 Client 发送响应。那么须要把响应编码成字节,再发送进来。故增加 HttpResponseEncoder 处理器。

4)ChunkedWriteHandler

A {@link ChannelHandler} that adds support for writing a large data stream asynchronously neither spending a lot of memory nor getting {@link OutOfMemoryError}.

该通道处理器次要是为了解决大文件传输的情景。大文件传输时,须要简单的状态治理,而 ChunkedWriteHandler 实现这个性能。

5) HttpFileServerHandler

自定义的通道处理器,其目标是实现文件服务器的业务逻辑。

通道处理器增加结束之后,须要启动服务器。代码如下:

ChannelFuture f = b.bind("localhost", port).sync();
f.channel().closeFuture().sync();

因为在 Netty 中所有的事件都是异步的,因而 bind 操作是一个异步操作,通道的敞开也是一个异步操作。因而应用 ChannelFuture 来作为一个 palceholder, 代表操作执行之后的后果。

最初敞开事件线程,代码如下:

bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

②文件处理器类

HttpFileServerHandler.java 是自定义的通道处理器,用来实现 HTTP 文件服务器的业务逻辑。从下面增加的 Handler 能够看出,在 HTTP 文件服务器的实现过程中,Netty 曾经为咱们解决了很多工作,如:HttpRequestDecoder 主动帮咱们解析 HTTP 申请 (解析 byte);再比方:HttpObjectAggregator 把多个 HTTP 申请中的数据组装成一个,当服务器发送的 response 当时不晓得响应的长度时就很有用。
参考:HttpChunkAggregator 剖析
文件处理器通过继承 SimpleChannelInboundHandler 来实现,代码如下:

public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

    private final String url;
    
    public HttpFileServerHandler(String url) {this.url = url;}
    
    @Override
    protected void messageReceived(ChannelHandlerContext ctx,
            FullHttpRequest request) throws Exception {if(!request.decoderResult().isSuccess())
        {sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        if(request.method() != HttpMethod.GET)
        {sendError(ctx, HttpResponseStatus.METHOD_NOT_ALLOWED);
            return;
        }

当服务器接管到音讯时,会主动触发 messageReceived 办法。该办法首先对 URL 进行判断,并只承受 GET 申请。

相干的验证通过后,通过 RandomAccessFile 类关上文件,并结构响应。

RandomAccessFile randomAccessFile = null;
        try{randomAccessFile = new RandomAccessFile(file, "r");
        }catch(FileNotFoundException fnfd){sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        
        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

如果申请中带有“KEEP-ALIVE”,则不敞开连贯。

if(HttpHeaderUtil.isKeepAlive(request)){response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }

进行数据的发送

sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            
            @Override
            public void operationComplete(ChannelProgressiveFuture future)
                    throws Exception {System.out.println("Transfer complete.");
                
            }ctx.write(response);

当发送完数据之后,因为采纳的是 Transfer-Encoding:chunk 模式来传输数据,因而须要在发送一个长度为 0 的 chunk 用来标记数据传输实现。
参考:HTTP 协定头部与 Keep-Alive 模式详解

ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if(!HttpHeaderUtil.isKeepAlive(request))
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        
    }

应用 Keep-Alive,能够缩小 HTTP 连贯建设的次数,在 HTTP1.1 中该选项是默认开启的。

Connection: Keep-Alive When the server processes the request and generates a response, it also adds a header to the response: Connection: Keep-Alive When this is done, the socket connection is not closed as before, but kept open after sending the response. When the client sends another request, it reuses the same connection. The connection will continue to be reused until either the client or the server decides that the conversation is over, and one of them drops the connection.

在应用 Keep-Alive 的状况下,当 Server 解决了 Client 的申请且生成一个 response 后,在 response 的头部增加 Connection: Keep-Alive 选项,把 response 返回给 client,此时 Socket 连贯并不会敞开。

【若没有 Keep-Alive,一次 HTTP 申请响应之后,本次 Socket 连贯就敞开了】

因为连贯还没有敞开,当 client 再发送另一个申请时,就会重用这个 Socket 连贯,直至其中一方 drops the connection.
对于 Keep-Alive 的探讨,参考:
整个源码参考:

package 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.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;

public class HttpFileServer {
    private static final String DEFAULT_URL = "/src/";
    
    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 ch) throws Exception {ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                    ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                    ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                    ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                    ch.pipeline().addLast("fileServerHandler", new HttpFileServerHandler(url));
                }
            });
            
            ChannelFuture f = b.bind("localhost", port).sync();
            System.out.println("HTTP 文件服务器启动, 地址是:" + "http://localhost:" + port + url);
            f.channel().closeFuture().sync();}finally{bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();}
    }
    
    public static void main(String[] args)throws Exception {
        int port = 8888;
        if(args.length > 0)
        {
            try{port = Integer.parseInt(args[0]);
            }catch(NumberFormatException e){port = 8080;}
        }
        
        String url = DEFAULT_URL;
        if(args.length > 1)
            url = args[1];
        new HttpFileServer().run(port, url);
    }
}


package httpFileServer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;
import javax.swing.text.html.MinimalHTMLWriter;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
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.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage;
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.codec.http2.Http2Headers;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

public class HttpFileServerHandler extends SimpleChannelInboundHandler<FullHttpRequest>{

    private final String url;
    
    public HttpFileServerHandler(String url) {this.url = url;}
    
    @Override
    protected void messageReceived(ChannelHandlerContext ctx,
            FullHttpRequest request) throws Exception {if(!request.decoderResult().isSuccess())
        {sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        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 = null;
        try{randomAccessFile = new RandomAccessFile(file, "r");
        }catch(FileNotFoundException fnfd){sendError(ctx, HttpResponseStatus.NOT_FOUND);
            return;
        }
        
        long fileLength = randomAccessFile.length();
        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        HttpHeaderUtil.setContentLength(response, fileLength);
//        setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        
        
        
        if(HttpHeaderUtil.isKeepAlive(request)){response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        
        ctx.write(response);
        ChannelFuture sendFileFuture = null;
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        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);
            }
        });
        
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if(!HttpHeaderUtil.isKeepAlive(request))
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {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{uri = URLDecoder.decode(uri, "UTF-8");
        }catch(UnsupportedEncodingException e){
            try{uri = URLDecoder.decode(uri, "ISO-8859-1");
            }catch(UnsupportedEncodingException e1){throw new Error();
            }
        }
        
        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;}
        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){FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
//        response.headers().set("CONNECT_TYPE", "text/html;charset=UTF-8");
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
        
        String dirPath = dir.getPath();
        StringBuilder buf = new StringBuilder();
        
        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");
        
        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 newUri){FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
//        response.headers().set("LOCATIN", newUri);
        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 mimetypesFileTypeMap = new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getPath()));
    }
}
正文完
 0