четверг, 6 февраля 2014 г.

Netty: делаем лёгкий сервер с блэкджеком и аннотациями

Допустим вам нужно обрабатывать http-запросы в своём приложении... Пишем на servlet-ах! Spring!! ЕщёКакойТоФреймворк!!!
A теперь нам нужно слушать websocket... Выбор сужается? А завтра потребуется добавить поддержку SMPP или какого-нибудь ещё "необычного" протокола? Рано или поздно вам прийдётся создать консольное java-приложение и начать изучать "встраиваемые" сервера. Встроить можно много чего, но что если фантазия разработчиков "с той стороны internet-а" родит совсем уже неведомый протокол? И тут мы вспомним о Netty. На его основе можно реализовать практически что угодно, при чём такая универсальность не пойдёт в ущерб ни производительности ни простоте. Чтобы подтвердить свою мысль я ниже сделаю свой "крошечный" http-сервер, в который можно будет добавлять "сервлетообразные" обработчики, "навешивая" их на url с помощью аннотаций.

Собственно, код:

import com.cty.httpServer.handlers.Mapped;
import com.cty.httpServer.handlers.UriHandlerBased;
import com.cty.tools.ReflectionTools;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
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.*;
import io.netty.util.CharsetUtil;
 
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
 
import static io.netty.handler.codec.http.HttpHeaders.Names.*;
import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
 
public class Server {
 
    private final int port;
 
    public Server(int port) {
        this.port = port;
    }
 
    public void start() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ServerInitializer());
            Channel ch = b.bind(port).sync().channel();
            ch.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
 
    private class ServerInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline p = ch.pipeline();
            p.addLast("decoder", new HttpRequestDecoder());
            p.addLast("encoder", new HttpResponseEncoder());
            p.addLast("handler", new ServerHandler());
        }
    }
 
    class ServerHandler extends SimpleChannelInboundHandler<Object> {
 
        private HttpRequest request;
        private final StringBuilder buf = new StringBuilder();
        private Map<String, UriHandlerBased> handlers = new HashMap<String, UriHandlerBased>();
 
        public ServerHandler() {
            if (handlers.size()==0) {
                try {
                    for (Class c : ReflectionTools.getClasses(getClass().getPackage().getName() + ".handlers")) {
                        Annotation annotation = c.getAnnotation(Mapped.class);
                        if (annotation!=null) {
                            handlers.put(((Mapped) annotation).uri(), (UriHandlerBased)c.newInstance());
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
 
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.flush();
        }
 
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
            UriHandlerBased handler = null;
            if (msg instanceof HttpRequest) {
                HttpRequest request = this.request = (HttpRequest) msg;
                QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.getUri());
                buf.setLength(0);
                String context = queryStringDecoder.path();
                handler = handlers.get(context);
                if (handler!=null) {
                    handler.process(request, buf);
                }
            }
            if (msg instanceof LastHttpContent) {
                FullHttpResponse response = new DefaultFullHttpResponse (
                        HTTP_1_1,
                        ((LastHttpContent) msg).getDecoderResult().isSuccess()? OK : BAD_REQUEST,
                        Unpooled.copiedBuffer(buf.toString(), CharsetUtil.UTF_8)
                );
                response.headers().set(CONTENT_TYPE, handler!=null ? handler.getContentType() : "text/plain; charset=UTF-8");
 
                if (isKeepAlive(request)) {
                    response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
                    response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
                }
                ctx.write(response);
            }
        }
 
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
}
 
A теперь посмотрим на него подробнее.
Инициализируем наш сервер номером порта, который он должен слушать и вызываем метод start(). Тут создаются многопоточные обработчики сообщений, назначается handler и т.п.
В реализации ServerHandler-a как раз и кроется вся логика. Впрочем её там немного.
В реализации метода channelRead0() читаем очередное сообщение и в зависимости от его типа обрабатываем его или(и) пишем ответ. В ответе указываем все положенные заголовки. Обработку делаем вызывая тот или иной обработчик в зависимости от url. Все обработчики наследуют общего предка:

import io.netty.handler.codec.http.HttpRequest;
 
public abstract class UriHandlerBased{
 
    public abstract void process(HttpRequest request, StringBuilder buff);
 
    public String getContentType() {
        return "text/plain; charset=UTF-8";
    }
}

В нём определён абстрактный метод в котором нужно будет реализовать конкретную логику для каждого http-ресурса. Напремер, так:

import io.netty.handler.codec.http.HttpRequest;
 
@Mapped(uri = "/h1")
public class UriHandler1 extends UriHandlerBased {
 
    @Override
    public void process(HttpRequest request, StringBuilder buff) {
        buff.append("HELLO HANDLER1!");
    }
}

Чтобы наш сервер смог найти какой url кем обслуживается, используем аннотацию:

import java.lang.annotation.*;
 
@Target(value= ElementType.TYPE)
@Retention(value= RetentionPolicy.RUNTIME)
public @interface Mapped {
    String uri();
}
 
В конструкторе заполняем таблицу обработчиков (поле handlers). Делаем это только если она пуста, т.е. при первом запуске. Все кастомные обработчики лежат в одном пакете. Достаём все классы из пакета с помощью такого вот инструмента:

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
 
public class ReflectionTools {
 
    public static Class[] getClasses(String packageName)
            throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Enumeration<URL> resources = classLoader.getResources(packageName.replace('.', '/'));
        ArrayList<Class> classes = new ArrayList<Class>();
        while (resources.hasMoreElements()) {
            File directory = new File(resources.nextElement().getFile());
            if (directory.exists()) {
                // Unpacked jar
                System.out.println("dir "+directory.getPath()+" exists!");
                File[] files = directory.listFiles();
                if (files!=null) {
                    for (File f : files) {
                        if (f.getName().endsWith(".class")) {
                            classes.add(Class.forName(packageName + '.' + f.getName().substring(0, f.getName().length() - 6)));
                        }
                    }
                }
            } else {
                // Packed jar
                String[] parts = directory.getPath().substring("file:".length()).split("!");
                if (parts.length==2) {
                    Enumeration e = new JarFile(parts[0]).entries();
                    while (e.hasMoreElements()) {
                        String jen = ((JarEntry) e.nextElement()).getName();
                        if (jen.startsWith(packageName.replace('.', '/')) && jen.endsWith(".class")) {
                            jen = jen.substring(packageName.length() + 1);
                            jen = jen.substring(0, jen.length() - 6);
                            classes.add(Class.forName(packageName + '.' + jen));
                        }
                    }
                }
            }
        }
        return classes.toArray(new Class[classes.size()]);
    }
}
 
Вот, собственно, и всё. Ничего сверхъестественного мы не сделали: хороший кусок вышепреведённых исходников можно найти в стандартных примерах Netty. Цель этого поста в другом: мы можем реализовать любой протокол достаточно просто, так как нам нужно и в таком  окружении, в котором захотим. По-моему, такая свобода заслуживает внимания.



3 комментария: