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.