Criando um Servidor de Arquivos com Netty e HTTP

Adicionando a Dependência

O primeiro passo é incluir o módulo HTTP do Netty no projeto. Utilize o seguinte trecho no arquivo pom.xml do Maven:

<dependency>
   <groupId>io.netty</groupId>
   <artifactId>netty-codec-http</artifactId>
   <version>4.1.45.Final</version>
</dependency>

Estrutura do Servidor

O servidor utiliza o ServerBootstrap para configurar o canal NIO. A pipeline inclui decodificadores HTTP e um agregador para mensagens completas.

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;

public class FileServerLauncher {
    public static void main(String[] args) throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        ChannelFuture future = bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new HttpServerCodec());
                        pipeline.addLast(new HttpObjectAggregator(65536));
                        pipeline.addLast(new RequestHandler());
                    }
                }).bind(8080).sync();

        System.out.println("Servidor de arquivos iniciado na porta 8080");
        future.channel().closeFuture().sync();
    }
}

Manipulador de Requisições

O handler principal processa requisições GET, servindo arquivos ou lisatndo diretórios. A pasta base é definida pela constante ROOT_PATH.

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import java.io.*;
import java.net.URLDecoder;

public class RequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private static final String ROOT_PATH = "/home/usuario/meus_arquivos";

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
        String uri = req.uri();
        if (!"GET".equals(req.method().name())) {
            sendError(ctx, "Apenas método GET é aceito");
            return;
        }

        File target = new File(ROOT_PATH + URLDecoder.decode(uri, "UTF-8"));
        if (!target.exists()) {
            sendError(ctx, "Recurso não encontrado");
            return;
        }

        if (target.isFile()) {
            handleFileDownload(target, ctx);
        } else {
            handleDirectoryListing(ctx, target, uri);
        }
    }

    private void handleFileDownload(File file, ChannelHandlerContext ctx) throws IOException {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());

        Channel channel = ctx.channel();
        try (FileInputStream fis = new FileInputStream(file)) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                response.content().writeBytes(buffer, 0, bytesRead);
            }
            channel.writeAndFlush(response);
        }
    }

    private void handleDirectoryListing(ChannelHandlerContext ctx, File dir, String uri) {
        StringBuilder htmlBuilder = new StringBuilder();
        File[] children = dir.listFiles();
        if (children != null) {
            for (File child : children) {
                String link = uri.equals("/") ? uri + child.getName() : uri + File.separator + child.getName();
                htmlBuilder.append("<li><a href='").append(link).append("'>").append(child.getName()).append("</a></li>");
            }
        }
        sendResponse(ctx, htmlBuilder.toString());
    }

    private void sendResponse(ChannelHandlerContext ctx, String content) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
        String page = "<html><meta charset='UTF-8'>" + content + "</html>";
        try {
            response.content().writeBytes(page.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        ctx.channel().writeAndFlush(response);
    }

    private void sendError(ChannelHandlerContext ctx, String message) {
        sendResponse(ctx, "<h1>" + message + "</h1>");
    }
}

Versão Aprimorada

Na versão final, adiciona-se ordenação alfabética com locale chinês e ícones para diferenciar pastas e arquivos. O código completo é exibido a seguir.

package com.exemplo.servidor;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import java.io.*;
import java.net.URLDecoder;
import java.text.Collator;
import java.util.*;

public class FileServer {
    private static final String ROOT = "/dados/publicos";

    public static void main(String[] args) throws Exception {
        ServerBootstrap bootstrap = new ServerBootstrap();
        ChannelFuture future = bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new HttpServerCodec());
                        pipeline.addLast(new HttpObjectAggregator(65536));
                        pipeline.addLast(new RequestProcessor());
                    }
                }).bind(8888).sync();

        System.out.println("Servidor FTP sobre HTTP iniciado");
    }

    static class RequestProcessor extends SimpleChannelInboundHandler<FullHttpRequest> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
            String uri = req.uri();
            if (!"GET".equals(req.method().name())) {
                respondError(ctx, "Requisição inválida");
                return;
            }

            File resource = new File(ROOT + URLDecoder.decode(uri, "UTF-8"));
            if (!resource.exists()) {
                respondError(ctx, "Arquivo ou diretório não encontrado");
                return;
            }

            if (resource.isFile()) {
                downloadFile(resource, ctx);
            } else {
                listDirectory(ctx, resource, uri);
            }
        }

        private void listDirectory(ChannelHandlerContext ctx, File dir, String uri) {
            StringBuilder page = new StringBuilder();
            File[] files = dir.listFiles();
            if (files == null || files.length == 0) {
                page.append("<p>Diretório vazio</p>");
            } else {
                List<String> sortedNames = new ArrayList<>();
                for (File f : files) sortedNames.add(f.getName());
                Collator collator = Collator.getInstance(Locale.CHINA);
                sortedNames.sort(collator);

                page.append("<hr>");
                for (String name : sortedNames) {
                    File child = new File(dir, name);
                    String href = uri.equals("/") ? uri + name : uri + File.separator + name;
                    String icon = child.isDirectory()
                            ? "<img src='https://archive.apache.org/icons/folder.gif'>"
                            : "<img src='https://archive.apache.org/icons/text.gif'>";
                    page.append("<li style='list-style:none;margin-left:2%;'>")
                        .append(icon)
                        .append("<a style='margin-left:8px;font-size:14px;' href='")
                        .append(href).append("'>").append(name).append("</a></li>");
                }
                page.append("<hr>");
            }
            sendHtml(ctx, page.toString());
        }

        private void downloadFile(File file, ChannelHandlerContext ctx) throws IOException {
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());

            Channel channel = ctx.channel();
            try (FileInputStream fis = new FileInputStream(file)) {
                byte[] buf = new byte[2048];
                int len;
                while ((len = fis.read(buf)) != -1) {
                    response.content().writeBytes(buf, 0, len);
                }
                channel.writeAndFlush(response);
            }
        }

        private void sendHtml(ChannelHandlerContext ctx, String content) {
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8");
            String html = "<html><meta charset=\"UTF-8\">" + content + "</html>";
            try {
                response.content().writeBytes(html.getBytes("UTF-8"));
            } catch (Exception e) {
                e.printStackTrace();
            }
            ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }

        private void respondError(ChannelHandlerContext ctx, String message) {
            sendHtml(ctx, "<h1>" + message + "</h1>");
        }
    }
}

Exemplo de Interface

Após a execução, o servidor exibe a listagem dos diretórios com links clicáveis e ícones para cada item.

Tags: Netty HTTP java servidor de arquivos nio

Publicado em 7-21 16:49