高性能 Netty 之結合 Http 協議應用

經過了這麼多篇文章,其實大家也肯定知道, Netty 主要是在 OSI 七層網絡層的應用層進行數據處理的( 因爲 Socket 是出於傳輸層以上的東西,是應用層與傳輸層的一個抽象層 )。所以肯定明白 Netty 在協議這方面肯定是能夠掌控的。

 

高性能 Netty 之結合 Http 協議應用

 

 

HTTP

說到網絡協議,相信大家最熟悉的協議也就是 HTTP 協議了。 HTTP 協議是建立在 TCP 傳輸協議上的應用層協議,屬於應用層的面向對象協議。由於簡捷,快速的方式,適用於分佈式超媒體信息系統。

HTTP 特點

HTTP 協議主要有如下特點:

  1. 支持 CS 模式。
  2. 簡潔。客戶端向服務端請求只需要指定服務 URL,攜帶必要的請求參數或者消息體。
  3. 靈活。 HTTP 允許傳輸任意類型的數據對象,傳輸內容類型是 HTTP 消息頭的 Content-type 加以標記。
  4. 高效。客戶端和服務端之間是一種一次性連接,它限制了每次連接只處理一個請求。當服務器返回本次請求的應答後便立即關閉連接,等待下一次重新建立連接。這樣做是爲了顧慮到日後介入千萬個用戶,及時地釋放連接可以大大提高服務器的執行效率。
  5. 無狀態。 HTTP 協議是無狀態協議,指的是對於事務處理沒有任何記憶能力。缺少記憶能力的後果就是導致後續的處理需要之前的信息,則客戶端必須攜帶重傳,這樣可能導致每次連接傳送的數據亮增大。另一方面,在服務器不需要先前信息時它的應答較快,負載較輕。

知道完特點後,我再簡單介紹一下 HTTP 的數據構成,這樣有助於我們理解下面的內容。

HTTP 數據構成

使用 Netty 搭建 HTTP 服務器

由於這次搭建的是 Http 服務器,那說明我們可以通過瀏覽器作爲客戶端來進行訪問服務端了,也就是我們不需要去寫客戶端代碼。

服務端

首先編寫的是服務端 HttpFileServer.java

public class HttpFileServer {
    //項目訪問上下文
    private static final String DEFAULT_URL = "/httpProject";   

    public void run(final int port, final String url) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //http 解碼
                            ch.pipeline().addLast("http-decoder", new HttpRequestDecoder());
                            //http 接收編碼
                            ch.pipeline().addLast("http-aggregator", new HttpObjectAggregator(65536));
                            //http 編碼工具類
                            ch.pipeline().addLast("http-encoder", new HttpResponseEncoder());
                            //chunked 操作
                            ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
                            //文件系統業務邏輯
                            ch.pipeline().addLast("fileServerHandler", new HttpFileServerHander(url));
                        }
                    });
            ChannelFuture f = serverBootstrap.bind("127.0.0.1", port).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    public static void main(String[] args) {
        new HttpFileServer().run(8080, DEFAULT_URL);
    }
}
複製代碼

注意到這次的服務端代碼在 ChannelInitializer 的初始化跟以往有點不同,這次上初始了多個 handler。下面我簡單說下它們的用法。

 

高性能 Netty 之結合 Http 協議應用

 

 

然後我們開始寫 HttpFileServerHander.java

public class HttpFileServerHander extends
        SimpleChannelInboundHandler<FullHttpRequest> {

    private String url;

    public HttpFileServerHander(String url) {
        this.url = url;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        //由於我們上面使用 netty 的解碼器 FullHttpRequest 來封裝信息
        if (!request.getDecoderResult().isSuccess()) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        //如果不是 get 方法則拋棄
        if (request.getMethod() != HttpMethod.GET) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        //獲取訪問的路徑
        final String uri = request.getUri();
        //解碼路徑
        final String path = sanitizeUri(uri);
        //如果路徑爲空,說明錯誤請求
        if (path == null) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        //新建文件對象
        File file = new File(path);
        //如果不在,返回 404
        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.BAD_REQUEST);
            return;
        }
        //具備權限訪問的文件
        RandomAccessFile randomAccessFile = null;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");
        }catch (FileNotFoundException e) {
            sendError(ctx, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        long fileLength = randomAccessFile.length();
        //新建一個 DefaultFullHttpResponse
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        //設置文件長度
        HttpHeaders.setContentLength(response, fileLength);
        setContentTypeHeader(response, file);
        if (HttpHeaders.isKeepAlive(request)) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        //ctx寫入 response
        ctx.write(response);
        //設置寫入文件的 listener
        ChannelFuture  sendFileFuture;
        sendFileFuture = ctx.write(new ChunkedFile(randomAccessFile, 0, fileLength, 8192), ctx.newProgressivePromise());
        //設置監聽僅需的的 Listener
        sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
            @Override
            public void operationProgressed(ChannelProgressiveFuture future, long progress, long total) throws Exception {
                if (total < 0) { // total unknown
                    System.err.println("Transfer progress: " + progress);
                } else {
                    System.err.println("Transfer progress: " + progress + " / " + total);
                }
            }

            @Override
            public void operationComplete(ChannelProgressiveFuture future) throws Exception {
                System.out.println("Transfer complete.");
            }
        });
        //最後輸出空內容
        ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        if (HttpHeaders.isKeepAlive(request)) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
    }

    private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

    //解析 url
    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(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(" 目錄:");
        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.NOT_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("failures: " + status.toString() + "\r\n", CharsetUtil.UTF_8));

        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
    
    //設置頭部內容
    private static void setContentTypeHeader(HttpResponse response, File file) {
        MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        if (ctx.channel().isActive()) {
            sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
複製代碼

客戶端

客戶端在這個例子中並不存在。因爲我們電腦的瀏覽器就是一個一個客戶端,它們底層也是通過 Socket 來與服務端進行交互的,原理都是一樣的。所以我們只需要訪問以下地址:

http://localhost:8080/httpProject

 

高性能 Netty 之結合 Http 協議應用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章