Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Контейнер серверного java-кода с помощью непрерывного соединения

Anna | 5.06.2014 | нет комментариев

Все, описанное в статье, является личным утилитарным навыком и не претендует на звание «истины в последней инстанции».

Преамбула

Здравствуйте. Я увлекаюсь созданием компьютерных игр. Моим любимым направлением, в котором я непрерывно усердствую совершенствоваться и узнавать что-то новое, являются браузерные многопользовательские игры.
Для создания прототипа для одной идеи в качестве контейнера сервлетов применяется Apache Tomcat. Он общается с клиентской частью по http протоколу. Для такого типа игры схема абсолютно действующая, причем довольно простая в реализации.
Но одной из несвоевременных оптимизаций(да, это нехорошо, но здесь я решил себе это дозволить) стала идея применять непрерывное соединение между сервером и заказчиком, т.к. в такой схеме не расходуется время на открытиезакрытие соединения в всяком запросе. Для реализации схемы рассматривалось WebSocket API для Tomcat, но стало увлекательно написать свой велосипед, следственно, встречайте рассказ о разработке под катом. 

Инструменты

Выходит, для реализации данной идеи применялись:

  • NetBeans 7.2.1: в ней, собственно, написан каждый java-код для решения этой задачи
  • JDK 1.7
  • Netty: было решено применять nio, Дабы сервер был максимально продуктивным на большом числе подключений, данный фреймворк отменно подошел.
  • Socket IO: на стороне заказчика
  • Apache Tomcat 7.0.27: для сравнительных тестов
  • Maven: для сборки каждого этого добродушна

Зодчество


Вначале разглядим логику работы приложения:

Контейнер представляется основным классом SocketServletContainer. Он служит для запуска/остановки контейнера, также содержит способы для управления сервлетами. Хочу подметить, что в статье термин сервлет обозначает объект, содержащий в себе способ с серверным кодом, и не имеет ничего всеобщего со спецификацией Servlet от JCP. Легко мне комфортнее называть такие объекты именно сервлетами.

Собственно, мы имеет базовый класс Servlet, от которого наследуются все сервлеты пользователя, класс сессии соединения(SocketSession), служащий для хранения информации о сессии и для отправки сообщений пользователю(отчего я сделал именно так, объясню позднее). Также были реализованы классы входящего и исходящего буфера(InputBuffer и OutputBuffer) соответственно.

Также потребовалось реализовать вспомогательный класс Config, отвечающий за парсинг файла конфигурации в формате xml. Следует упомянуть и о классах QueueHandler и TaskHandler.

QueueHandler является обработчиком очереди запросов и содержит способ для добавления экземпляра классаTask на обработку.
TaskHandler реализует интерфейс Runnable. В способе run содержится обработка переданного запроса.
Класс Task содержит в себе информацию о пришедшем запросе(к какому сервлету обратиться и параметры, переданные на сервер) и способы для работы с сетью(readwrite).

Сейчас разглядим организацию работы с сетью:

Я не буду детально расписывать работу с Netty, потому что это теснее сделали до меня( отдельное спасибо програюзеру Rena4ka за ее статью по Netty). Читайте на прогре либо документацию на официальном сайте, как вам будет комфортнее. Разгляжу только ту часть, которая нужна для понимания основных тезисов человеком, не имеющим навыка программирования с Netty.
Класс ServerPipelineFactory является фабрикой ChannelPipeline и необходим для функционирования Netty. Также пришлось реализовать 3 класса: DecoderEncoderNioHandler.
Первые 2- это обработчики пакетов, пришедших на сервер. Декодер отвечает за верный разбор пакета, поступившего из сети и возвращает экземпляр класса Task. Encoder отвечает за правильную запись экземпляра Task в сеть и отправку на заказчик.
NioHandler, по сути, является сетевым администратором: принимает соединения, отправляет задачи на обработку и управляет сессиями.

Протокол

Для общения заказчика и сервера необходим свой протокол. Я решил сделать его довольно простым и текстовым.
В результате, заказчик посылает на сервер строку запроса, имеющую дальнейший вид: имя_сервлета[sysDiv]параметры_запроса.
Формат списка параметров запроса: name1=value1, name2=value2,…

Пример: «TS[sysDiv]message=Hello habrahabr.ru».

Необходимо подметить, что протокол является симметричным в том смысле, что заказчик получает строку с указанием сервлета, сформировавшего результат, и списком переданных параметров.

А сейчас перейдем непринужденно к рассмотрению кода нашего контейнера. Но обо каждому по порядку.

Формат конфигурационного файла

 

<?xml version="1.0" encoding="utf-8"?>
<config>
  <address>localhost</address>
  <port>9999</port>
  <workThreadCount>2</workThreadCount>
  <processThreadCount>2</processThreadCount>
</config>

workThreadCount — число потоков, тот, что принимают сообщения из сети и пишут в сеть(необходимо для инициализации Netty).
processThreadCount — число потоков, обрабатывающих всеобщую очередь запросов, пришедших на сервер. В них, собственно, происходит парсинг строк-запросов, работа каждого серверного кода и образование результатов.

SocketServletContainer

Данный класс представляет собой синглтон, от того что он является «центральным» классом и так к нему будет комфортнее обращаться с других классов программы. И, разумеется, подразумевается 1 копия сервера на приложение(следственно не требуется потокобезопасная реализация синглтона). Что, по моему суждению, разумно.

public class SocketServletContainer {
    private Channel channel;
    private ServerBootstrap networkServer;
    private QueueHandler queueHander;
    private Map<String, Servlet> servlets;    
    private Config conf;
    private static SocketServletContainer server= null;

    private static List<SocketSession> list= new ArrayList<SocketSession>();

    public List<SocketSession> getListSession()
    {
        return list;
    }

    static public SocketServletContainer getInstance()
    {
        if (server==null)
        {
            server= new SocketServletContainer();
        }

        return server;
    }

    private SocketServletContainer()
    {
        conf= new Config("conf.xml");
       //Парсим конфиг, в случае ошибки- кидаем Exception.
        try
        {
            conf.read();
        }
        catch(Exception e)
        {
            throw new ContainerInitializeException(e.toString());
        }

        servlets= new HashMap<String, Servlet>();
    }

    public void start()
    {
        //Инициализируем Netty
        ExecutorService bossExec = new OrderedMemoryAwareThreadPoolExecutor(1, 400000000, 2000000000, 60, TimeUnit.SECONDS);
        ExecutorService ioExec = new OrderedMemoryAwareThreadPoolExecutor(conf.getWorkThreadCount(), 400000000,
                2000000000, 60, TimeUnit.SECONDS);
        networkServer = new ServerBootstrap(new NioServerSocketChannelFactory(bossExec, ioExec,  conf.getWorkThreadCount()));
        networkServer.setOption("backlog", 500);
        networkServer.setOption("connectTimeoutMillis", 10000);
        networkServer.setPipelineFactory(new ServerPipelineFactory());
        channel = networkServer.bind(new InetSocketAddress(conf.getAddress(), conf.getPort()));
        //Создаем обработчик очереди запросов
        queueHander= new QueueHandler(conf.getProcessThreadCount());

        System.out.println("Ready");
    }
    //Метод «жесткой» остановки сервера
    public void stop()
    {
        if (channel.isOpen())
        {
            ChannelFuture future= channel.close();
            future.awaitUninterruptibly();
        }   

        queueHander.stop();
    }

    public QueueHandler getQueueHandler()
    {
        return this.queueHander;
    }
    //Метод регистрации сервлета в нашем контейнере
    public void registerServlet(Servlet servlet, String name)
    {
        //Если сервлет еще не зарегистрирован- добавляем его в HashMap.
        synchronized(servlets)
        {
            if (!servlets.containsKey(name))
            {
                servlets.put(name, servlet);
            }
        }   
   }

    public Servlet getServlet(String name)
    {
        return servlets.get(name);
    }
}

 

Servlet

Здесь все легко и ясно. Способ doRequest вызывается, когда приходит пакет с указанием вызвать данный сервлет.
Заместитель: передача сессии в способ doRequest сделано с той целью, Дабы сервлет мог получить List всех имеющихся сессии и разослать им сообщение. Скажем, при реализации чата.

abstract public class Servlet {
    abstract public void doRequest(InputBuffer input, OutputBuffer output, SocketSession session);
}

 

SocketSession

Всякая сессия имеет свой неповторимый id. Существует пул id-ников на 20000 подключенных заказчиков. При превышении этого лимита сервер, при попытке сделать сессию, будет логировать ошибку, отправлять сообщение об ошибке не заказчик и закрывать канал.
Значение размера пула отменнее вычислить опытным путем, в идеале он должен быть чуть огромнее числа максимально допустимых единовременно подключенных заказчиков на вашем сервере.

public class SocketSession {  

     private static byte[] idPool;

    public int generateId()
    {
        synchronized(idPool)
        {
            if (idPool==null)
            {
                idPool= new byte[20000];
                for (int j=0;j<idPool.length;j  )
                {
                    idPool[j]=0;
                }
            }
            for (int j=0;j<idPool.length;j  )
            {
                if (idPool[j]==0)
                {
                    idPool[j]=1;
                    return j;
                }
            }
            return -1;
        }
    }

    private int id;
    private Channel channel;

    //Создаем сессию и добавляем ее в List теснее имеющихся.
    public SocketSession(Channel channel)
    {
        this.channel= channel;
        this.id= generateId();
        //если мест огромнее нет
        if (this.id==-1)
        {
            OutputBuffer out= new OutputBuffer();
            out.setPar("error", "Connection limit error");
            send(out, "System Servlet");
            //Залогируем ошибку
            System.err.println("Connection limit error");
            return;
        }

        SocketServletContainer.getInstance().getListSession().add(this);
    }

    public int getId()
    {
        return id;
    }
    //Отправка сообщению заказчику. Способ вынесен в класс Сессии для комфортной синхронизации отправки с нескольких потоков.
    public void send(OutputBuffer output, String servletName)
    {

        synchronized(channel)
        {
            channel.write(new Task(servletName, output.toString()));
        }
    }
    //Закрытие сессии, скажем, при обрыве соединения либо в каком-то «служебном» сервлете
    public void close()
    {
        synchronized(idPool)
        {
            idPool[this.id]= 0;
        }
        channel.close();
        SocketServletContainer.getInstance().getListSession().remove(this);
    }
}

 

InputBuffer

В конструкторе происходит инициализация, строка source должна содержать список параметров запроса в заданном формате.

public class InputBuffer {

    private Map<String, String> map= new HashMap<String, String>();

    public InputBuffer(String source)
    {
        String[] par= source.split(",");
        for (int j=0; j< par.length; j  )
        {
            if (!par[j].contains("="))
            {
                continue;
            }
            String[] data= par[j].split("=");
            if (data.length<2)
            {
                System.err.println("Parsing Error");
                continue;
            }
            map.put(data[0], data[1]);
        } 
   }

    public String getPar(String key)
    {
        return map.get(key);
    }
}

 

OutputBuffer

Интерфейс класса довольно внятен. Главное примечание- необходимо переопределить способ toString(), от того что именно он применяется для образования результата в классе SocketSession.

public class OutputBuffer {

    private List<String> list= new ArrayList<String>();

    public void setPar(String key, String par)
    {
        list.add(key "=" par);
    }

    @Override
    public String toString()
    {
        StringBuilder res= new StringBuilder();

        for (int j=0; j< list.size();j  )
        {
            res.append(list.get(j));
            if (j!=list.size()-1)
            {
                res.append(",");
            }
        }
        return res.toString();
    }
}

 

Config

Реализацию этого класса я приводить не буду, потому что интерфейс его внятен из того, что применяется вSocketServletContainer, а реализаций xml-парсеров на java в интернете довольно много и, я верю, читатель обнаружит для него особенно подходящую.
Лично я применял DOM-парсер.

QueueHandler

Данный класс тоже дюже примитивен в реализации. Внутри он содержит пул потоков, тот, что занимается выполнением задач(TaskHandler). Проектирование я переложил на верную и проверенную реализацию threadPool. Для создания пула применяется фабрика Executors.newFixedThreadPool(n).

При вызове способа stop, теснее имеющиеся задачи, стоящие в очереди, будут обрабTask task= Task.read(frame); checkpoint(DecoderState.READ_LENGTH); return task; default: throw new Error( “Shouldn’t reach here” ); } } }

Encoder

 

public class Encoder extends OneToOneEncoder {

    @Override
    protected Object encode(ChannelHandlerContext channelhandlercontext, Channel channel, Object obj) throws Exception {
         //Если передан не экземпляр Task, то легко кидаем его выше
        if(!(obj instanceof Task))
        {
            return obj;
        }

        Task task= (Task)obj;

        ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
        //Пишем task в сеть
        Task.write(task, buffer);
        return buffer; 
    }
}

 

NioHandler

Данный объект обрабатывает основные события работы с сетью: подключение заказчиков, приобретение сообщений, обрыв соединения.

public class NioHandler extends SimpleChannelUpstreamHandler {

    private SocketSession session;
    @Override
    public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {

        //Тут заводим сессию работы сокета
        session= new SocketSession(e.getChannel());
        System.out.println("Has connect");
    }

    @Override
    public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
        session.close();
 }
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
        if(e.getChannel().isOpen())
        {
            //Получаем экземпляр Task и передаем его на обработку в QueueHandler.
            Task message= (Task)e.getMessage();
            SocketServletContainer.getInstance().getQueueHandler().addTaskToProcess(message, session);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
        // Случилась оплошность. Логируем ошибку, закрываем канал и сессию.
        session.close();
        e.getCause().printStackTrace(System.err);
        ctx.getChannel().close();
    }
}

Пример сервлета

 

public class TS extends Servlet {

    @Override
    public void doRequest(InputBuffer input, OutputBuffer output, SocketSession session) {
        output.setPar("request", input.getPar("message") session.getId());
    }
}

Как это работает либо основной класс приложения

Собственно, в приложении не так уж много строк кода, все легко и прозрачно.

public class App 
{
    public static void main( String[] args ) throws ContainerInitializeException
    {
        SocketServletContainer server= SocketServletContainer.getInstance();
        server.registerServlet(new TS(), "TS");
        server.start();
    }
}

 

Немножко тестов

Что ж, контейнер написан, он работает. Заморачиваться с созданием клиентской обертки под него я не стал, ограничился прямой записью в сокет, выглядит это приблизительно так:

socket = new Socket("127.0.0.1", 9999);
DataOutputStream dos= new DataOutputStream(socket.getOutputStream());
DataInputStream dis= new DataInputStream(socket.getInputStream());
String buffer= "TS[sysDiv]message=IloveJava";

dos.writeInt(buffer.getBytes().length 4);
dos.writeInt(buffer.getBytes().length);
dos.write(buffer.getBytes());
dos.flush();

В всеобщем, как и говорилось в самом начале статьи, создание такой системы- это одна из несвоевременных оптимизаций, которую я решил себе дозволить. Следственно тупо было бы не провести пару тестов, раз уж мы все это написали.

Собственно, я решил сравнить это решение с контейнером сервлетов, работающем по http.
Для тестов был написан сервлет, вертящийся в Tomcat и сервлет, работающий внутри сделанного контейнера.

Заместитель: Я специально сравнил эффективность http-протокола и решения на сокетах, от того что web-socket, которые Tomcat удачно поддерживают, мною не рассматривались для реализации данного плана игры.

Особенности теста:

  • Оба сервлета исполняли приблизительно идентичные операции, а именно, записывали идентичную строку в выходной поток
  • Волнующий меня параметр тестирования- время отклика от сервера при обработке запроса
  • Замеры производились на локалхосте
  • Время отклика я получал с поддержкой стандартных средств языка Java
  • Меня волновали не определенные цифры, а сопоставление порядков итогов, от того что тест был написан крайне «дерзко»
  • Для всякого теста было исполнено по 10 000 запросов с идентичными параметрами, позже чего было вычислено среднее значение

А итог таков: в среднем, обработка 1 «пустого» сервлета для Tomcat заняла 0, 99 мс.
Описанный в статье контейнер совладал с аналогичной задачей за 0, 09 мс.

Мы имеем 2 итога, различающихся на порядок. Но с учетом того, что мысль применять сокеты пришла ко мне не из-за скорости, а из-за необходимости иметь вероятность достучаться от сервера к заказчику, итог дозволено признать больше, чем удовлетворительным.

TODO:

Также остался маленький список, что еще дозволено было бы реализовать для сходственной системы, тот, что я также приведу:

  1. Валидацию входных данных. У input-буфера дозволено добавить способ validate(String mask), тот, что по маске типов данных для соответствующих параметров бы механически конвертировал их к необходимому(не только строковому) типу. Выглядеть это могло бы приблизительно так: validate(“message:String, count:int”);
  2. Добавить шифрование данных. Именно для этого заложена запись в буфер byte[], а не writeUTF8(), правда протокол и является текстовым. Дозволено реализовать interface Crypto{}, тот, что имел бы 2 способа: code() и encode(). И Реализацию такого интерфейса передавать в SocketServletContainer(), для комфортной смены либо выбора алгорифма криптографии.
  3. Работу с аннотациями(как это сделано в Tomcat) и отложенную инициализацию сервлетов.
  4. Больше «безвредный» парсинг входного буфера с экранированием разделителя
  5. Кучу других пригодных мелочей, тот, что могли бы потребоваться в такой системе.

 

Взамен завершения

Итоги работы контейнера абсолютно удовлетворили мои ожидания. Применение NIO дозволило экономично расходовать потоки и сконфигурировать контейнер под имеющееся сталь так, Дабы он работал максимально результативно.
Функционал, тот, что предоставляет контейнер, разрешает довольно комфортно разрабатывать приложение, не заботясь о «низкоуровневых» вещах, типа парсинга пакетов и так дальше(мне, привыкшему разрабатывать на tomcat, все показалось довольно комфортным:)).

Но я все-таки не решился применять сходственное решение как основу реального плана, потому что для меня присутствие сокетов, безусловно, было бы дюже кстати(для обратной связи сервер-заказчик), но, в тезисе, не критично. А вот эффективность и безопасность Tomcat, проверенного годами и тысячами разработчиков, не вызывает вопросов.
Я планирую применять реализованную систему в «тесных», но некритичных местах, в которых http-протокол применять вовсе уж плохо, скажем, она отменно подойдет для реализации чата.

Верю, данная статья будет увлекательна для читателей и кому-нибудь окажется пригодной. Усердствовал передать целостную картину, допустимо, немножко переборщил с рассуждениями и числом кода. С удовольствием выслушаю ваши вопросы и предложения по написанному материалу.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB