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. Цель этого поста в другом: мы можем реализовать любой протокол достаточно просто, так как нам нужно и в таком окружении, в котором захотим. По-моему, такая свобода заслуживает внимания.
здравствуйте, спасибо за статью. Сервер сможет обработать запрос вида http://tempname.com/h1 ?
ОтветитьУдалитьYfdthyjt )
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалить