1i7 (1i7) wrote,
1i7
1i7

Categories:

Сервер Роботов: запуск управляющего сервера на Java в облаке Amazon

Пришло время делать интересные вещи. Подключим плату ChipKIT WF32 к Серверу Роботов, запущенному в облаке в глубинах интернета, и помигаем из него лампочкой на плате, которая лежит у нас на столе.

Робот Сервер: управление платой ChipKIT WF32 из облака from 1i7 on Vimeo.


Сервер Роботов запущен в облаке (виртуальная машина Амазон) по адресу robotc.lasto4ka.su:1116 и слушает подключения. Робот Клиент (плата ChipKIT WF32) подключается к Серверу Роботов через интернет и переходит в режим приема команд: включить или выключить лампочку. Пользователь отправляет команды подключенной плате через интерфейс командной строки, запущенный на удаленной системе в оболочке ssh. Плата принимает и распознает команду, включает или выключает лампочку и отправляет ответ.

Примерный путь команды от терминала к плате: пользователь вводит команду в приглашение на экране на стационарном компьютере в Нижнем Новгороде. Через сессию ssh команда отправляется от нижегородского провайдера интернета Дом.ру, пересекает Атлантику и попадает на виртуальную машину в дата-центре Амазона в Орегоне (зона us-west-2b) на Сервер Роботов. Сервер роботов из Орегона отправляет команду подключенному Роботу Клиенту (плата WF32): обратно через Атлантику, через провайдера мобильного интернета Мегафон Нижний Новгород (плата WF32 подключена к точке доступа WiFi на смартфоне Ётафон с Андроид с мобильным интернетом), через беспроводную сеть WiFi смарфтона, команда попадает на плату WF32. Плата WF32 принимает команду, зажигает или тушит лампочку и отправляет ответ через беспроводную сеть на смартфон, через Мегафон Нижний Новгород в Атлантику в дата-центр Амазон в Оригоне на Сервер Роботов. Сервер Роботов получает ответ и печатает результат в сессию ssh - из Оригона, через Атлантику, через Дом.ру Нижний Новгород на экран стационарного компьютера в Нижнем Новгороде.

В качестве Роботов Клиентов мы будем использовать плату ChipKIT WF32, смартфон с Android Ётафон и обычное приложение на Java. Сегодня напишем код Сервера Роботов на языке Java и запустим его принимать подключения на виртуальной машине в облаке Amazon.



Исходники

1) Сервер Роботов и Робот Клиент на Java: chipkit-cloud-wifi/JavaTcpServerMaster
2) Робот Клиент на ChipKIT WF32: chipkit-cloud-wifi/chipkit_tcp_client_slave
3) Робот Клиент на Android: chipkit-cloud-wifi/AndroidTcpClientSlave

Протокол
Протокол общения между Сервером Роботов и Роботом Клиентом (плата, смартфон или что-то еще): сервер отправляет команду, клиент выполняет команду и присылает ответ; команды и ответы строковые.

Весь полезный функционал определим двумя основными командами:
ledon - включить лампочку;
ledoff - выключить лампочку.

Ответы от клиента:
ok - в случае успеха выполнения команды;
dontunderstand - команда не распознана.

Также для Сервера Роботов введём одну вспомогательную команду kick для разрыва соединения между сервером и подключенным клиентом; команда не отправляется клиенту, разрыв осуществляется в одностороннем порядке.

Предварительные приготовления

0.1) Нужна плата из серии ChipKIT, которая умеет выходить в интернет (на аналогичных платах Arduino этот код не заработает): ChipKIT Uno32+WiFi Shield, ChipKIT WF32 или ChipKIT Wi-FIRE.

0.2) Среда работки приложений на Java: вариантов множество, по умолчанию рекомендую простой и функциональный Netbeans или перегруженный, но тоже функциональный Eclipse (для того, чтобы открыть проект из примера, в среде должен быть установлен плагин Maven).

1) Лабораторная работа: Регистрация собственной виртуальной машины в Амазоне с бесплатным годом пользования.

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

Замечание: Еще один любопытный сервис, на котором можно получить виртуальный хостинг - koding.com: его создатели дошли до того, что реализовали командную строку управления сервером на JavaScript прямо в браузере (не нужно отдельно запускать ssh или putty). В качестве операционной системы у них была как и у нас Ubuntu, но это для самостоятельных экспериментов.

2) Лабораторная работа: Подключение платы ChipKIT WF32 к точке доступа WiFi.

Плата должна уметь выходить в интернет. Я использовал точку доступа на смартфоне Android с мобильным интернетом, но пойдет обычный домашний вайфай-роутер.

Управляющий Сервер Роботов на Java

Т.к. наш Робот Сервер после подключения к нему Робота Клиента находится в активном режиме отправки команд клиенту, будем называть Сервер Роботов - управляющим сервером (англ. Master - хозяин), а Робота Клиента подчинённым клиентом (в английских терминах Slave - раб).

Код сервера - проект JavaTcpServerMaster в формате Maven (откроется в Netbeans или Eclpse с плагинами Maven).

Внутри проекта весь код Сервера Роботов находится в одном файле RobotServer1.java.

Подключим все необходимые классы и объявим наш главный класс edu.nntu.robotserver.RobotServer1:


package edu.nntu.robotserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
*
* @author benderamp
*/
public class RobotServer1 {


Константы для протокола:


    // Локальные команды для сервера
    public static final String SCMD_KICK = "kick";


Значения для сетевых операций. Порт сервера для приёма входящих подключений (по умолчанию 1116), он должен быть обязательно открыт в настройках безопасности сервера (фаерволе или брандмауэре). Таймаут получения ответа от клиента - если клиент слишком долго не присылает ответ на отправленный запрос, обрываем с ним связь до следующего подключения.


    public static final int DEFAULT_SERVER_PORT = 1116;
    
    /**
     * Таймаут для чтения ответа на команды - клиент должет прислать ответ за
     * 5 секунд, иначе он будет считаться отключенным.
     */
    private static final int CLIENT_SO_TIMEOUT = 5000;

    private int serverPort = DEFAULT_SERVER_PORT;
    private ServerSocket serverSocket;


Весь код сервера в методе startServer():


    /**
     * Запускает сервер слушать входящие подключения на указанном порте
     * serverPort.
     *
     * Простой однопоточный сервер - ждет ввод от пользователя, отправляет
     * введенную команду клиенту, ждет ответ и дальше по кругу.
     *
     * Сбросить подключенного клиента - ввести локальную команду 'kick'.
     *
     * @throws java.io.IOException
     */
    public void startServer() throws IOException {
        System.out.println("Starting server on port " + serverPort + "...");



Создаём серверный сокет, который будет принимать входящие подключения от внешних клиентов на порте 1116 (не забыть открыть порт в настройках безопасности сервера):


        // Открыли сокет
        serverSocket = new ServerSocket(serverPort);


Принимаем входящее подключение - достаём клиентский сокет ClientSocket из очереди внутри серверного сокета ServerSocket при помощи вызова serverSocket.accept(). Наша программа работает в одном потоке, поэтому мы обмениваемся данными только с одним клиентским сокетом за раз: процесс обмена данными с новым клиентом начинается только после того, как активная сессия работы с текущим клиентом окончена. Вызов serverSocket.accept() блокируется до момента подключения первого клиента.

Замечание: Стоит отметить, что во время работы с текущим клиентом, параллельные подключения других внешних клиентов не блокируются (на их стороне связь устанавливается без ошибок, даже если сервер в этот момент занят) - серверный сокет просто помещает их в свою внутреннюю очередь подключений до тех пор, пока код программы не достанет их оттуда при помощи метода ServerSocket.accept(). Для того, чтобы работать с несколькими внешними клиентами одновременно, код сервера должен использовать многопоточность.

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


        Socket clientSocket = null;
        while (true) {
            try {
                System.out.println("Waiting for client...");
                // Ждём подключения клиента (робота)
                clientSocket = serverSocket.accept();
                System.out.println("Client accepted: " + clientSocket.getInetAddress().getHostAddress());



Установим максимальное время ожидания для получения ответа от клиента (по умолчанию 5 секунд, хотя для медленных подключений стоит выставить побольше). Если мы отправили клиенту запрос и ответ не пришел в установленное время, клиент считается отключенным и связь с ним разрывается со стороны сервера, даже если с ним на самом деле всё нормально и ответ просто не успел дойти из-за медленного интернета.

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

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


                // Клиент подключился:
                // Установим таймаут для чтения ответа на команды -
                // клиент должет прислать ответ за 5 секунд, иначе он будет
                // считаться отключенным (в нашем случае это позволит предотвратить
                // вероятные зависания на блокирующем read, когда например клиент
                // отключился до того, как прислал ответ и сокет не распрознал это
                // как разрыв связи с выбросом IOException)
                clientSocket.setSoTimeout(CLIENT_SO_TIMEOUT);



Открываем потоки ввода и вывода - через них будем отправлять данные клиенту и получать ответы:


                // Получаем доступ к потокам ввода/вывода сокета для общения
                // с подключившимся клиентом (роботом)
                final InputStream clientIn = clientSocket.getInputStream();
                final OutputStream clientOut = clientSocket.getOutputStream();


Пользователь, запустивший сервер роботов в облаке (конечно, через оболочку удаленного доступа ssh), вводит команды для подключившегося клиента-робота на клавиатуре - их принимает вызов userInputReader.readLine().

Замечание: вызов userInputReader.readLine() блокирует выполнение программы до тех пор, пока пользователь не введет что-то с клавиатуры и не нажмет клавишу Enter. По этой причине в такой реализации мы сможем узнать о том, что клиент отключился, только в том случае, если введем в приглашение любую команду и попробуем отправить ее клиенту - в том случае, если соединение разорвано, процесс отправки и получения ответа завершится с ошибкой.


                // Ввод команд из консоли пользователем
                final BufferedReader userInputReader = new BufferedReader(
                    new InputStreamReader(System.in));
                String userLine;
                System.out.print("enter command: ");
                while (!clientSocket.isClosed() && (userLine = userInputReader.readLine()) != null) {
                    if (userLine.length() > 0) {



Отправляем команду клиенту через поток вывода OutputStream. Обязательно делаем слив clientOut.flush(), чтобы записанные байты не остались в каком-нибудь внутреннем буфере сокета (ожидать накопления более крупной порции данных), а сразу отправились через интернет к адресату.


                        // отправить команду клиенту
                        System.out.println("Write: " + userLine);
                        clientOut.write((userLine).getBytes());
                        clientOut.flush();


Команду отправили, ждем ответ. Вызов InputStream.read(buffer) блокируется до тех пор, пока в него не поступят данные или пока не произойдет сбой связи.

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

Неблагоприятный сценарий
В случае сбоя связи всё гораздо разнообразнее - метод read ниже может оборваться одним из следующих способов:
1) Выбросить исключение IOException (клиент оборвал связь)
2) Вернуть значение readSize (количество считанных байт) = -1 (клиент оборвал связь)
3) Выбросить исключение SocketTimeoutException (ответ от клиента не пришел вовремя, что вполне может значить, что клиент оборвал связь, но это не вызвало выброса IOException или возврата read с readSize = -1)

Есть также 4й вариант завершить сессию общения с клиентом - в том случае, если клиент оборвал связь до того, как ему была отправлена очередная команда, IOException может выбросить вызов clientOut.write(bytes). Но в некоторых случаях может и не выбросить - тогда переходим к вариантам разблокировки read.

Замечание 1: Особенно любопытно, что конкретная линия поведения в неблагоприятной ситуации очень сильно зависит от того, на какой платформе запущен сервер и что из себя представляет подключившийся клиент. Например в случае запуска и сервера и клиента на локальном компьютере с Oracle Java, после остановки клиента связь разрывается корректно и логично с IOException при попытке записать команду в сокет. Однако уже в случае с запуском в точности этого сервера в облаке на OpenJDK (клиент тот же на Oracle Java), clientOut.write(bytes) и clientIn.read(buffer) запись и чтение из разорванного сокета прощают, в итоге связь разрывается только в read с SocketTimeoutException. В случае с клиентом на Си на ChipKIT WF32 или клиентом на Android комбинации разрыва соединения с отключившимся клиентом могут быть другими: кто-то выдаст IOException на read, кто-то вернет readSize с -1.

Замечание 2: Как хорошо видно из замечания 1, реализация благоприятного сценария работы в хорошем приложении может занимать 10-20% полезного кода и времени; остальные 80-90% - это анализ и обработка всего множества остальных сценариев, которые не приводят приложение к полезному результату, но защищают его от непредвиденных сбоев и прочих проблем. Если эта работа проделана правильно, пользователь никогда ее не заметит и будет оценивать количество труда, вложенного в приложение, по количеству его полезного функционала. Однако если эту работу делать не достаточно тчательно, неучтенные проблемы обязательно вылезут наружу в процессе эксплуатации, и к претензиям о недостающих в приложении возможностях добавится ярлык глючной поделки.


                        // и сразу прочитать ответ
                        final byte[] readBuffer = new byte[256];
                        final int readSize = clientIn.read(readBuffer);
                        if (readSize != -1) {
                            final String clientLine
                                    = new String(readBuffer, 0, readSize);
                            System.out.println("Read: " + clientLine);
                        } else {
                            // Такая ситуация проявляется например при связи
                            // Server:OpenJDK - Client:Android - клиент отключается,
                            // но сервер это не распознаёт: запись write завершается
                            // без исключений, чтение read возвращается не дожидаясь
                            // таймаута без исключения, но при этом возвращает -1.
                            throw new IOException("End of stream");
                        }



Обрабатываем исключения - обрыв связи по таймауту:


            } catch (SocketTimeoutException ex1) {
                // Попадем сюда, если клиент не отправит ответ во-время
                // (установленное с clientSocket.setSoTimeout):
                // это не значит, что соединение нарушено (он может просто решил
                // не отвечать), но все равно отключим такого клиента, чтобы он
                // не блокировал сервер.
                System.out.println("Client disconnected: " + ex1.getMessage());
                if (clientSocket != null) {
                    clientSocket.close();
                }
            }


Или по ошибке ввода-вывода:


            } catch (IOException ex2) {
                // Попадём сюда только после того, как клиент отключится и сервер
                // попробует отправить ему любую команду
                // (в более правильной реализации можно добавить в протокол
                // команду проверки статуса клиента 'isalive' и отправлять её
                // клиенту с некоторой периодичностью).
                System.out.println("Client disconnected: " + ex2.getMessage());
            }



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


        Socket clientSocket = null;
        while (true) {
            try {
                System.out.println("Waiting for client...");
                ...
                while (!clientSocket.isClosed()...) {
                    ...
                }
            } catch (SocketTimeoutException ex1) {
                ...
            } catch (IOException ex2) {
                ...
            }
        }



Также добавим локальную серверную команду KICK (пиннуть) - если пользователь ввел команду "kick", сервер не отправляет ее клиенту, о просто разрывает с ним соединение:


                while (!clientSocket.isClosed() && (userLine = userInputReader.readLine()) != null) {
                    if (SCMD_KICK.equals(userLine)) {
                        // отключить клиента
                        clientIn.close();
                        clientOut.close();
                        clientSocket.close();

                        System.out.println("Client disconnected: KICK");
                    }
                    ...
                }



Запускаем сервер через обычный main:


    public static void main(String args[]) {
        final RobotServer1 server = new RobotServer1();
        try {
            server.startServer();
        } catch (IOException ex) {
            Logger.getLogger(RobotServer1.class.getName()).log(Level.SEVERE, null, ex);
        }
    }



Запускаем Сервер Роботов в облаке Амазон

О том, как получить себе год бесплатного хостинга на Амазоне с полным доступом к виртуальной машине Ubuntu Linux, каким образом на ней производить настройки безопасности, запускать команды, устанавливать программное обеспечение и загружать файлы с локальной машины, я подробно и по шагам рассказывал в отдельной лабораторной работе Виртуальный хостинг на Амазоне.

Поэтому, чтобы двигаться дальше, рекомендую еще раз пролистать слайды по ссылке примерно со второй половины. Здесь только напомню, что:
- для получения командной строки на далеком сервере, в Linux используется команда ssh, которая по умолчанию установлена в любом дистрибутиве; в Windows нужно скачать и установить специальную программу PuTTY.
- Для загрузки файлов на далёкий сервер в Linux можно использовать команду sftp из командной строки или любой графический файловый менеджер с поддержкой протокола "sftp:" (в KDE4 это Dolphin). Для Windows можно скачать любимый sftp-клиент, лично я использую WinSCP.

Настройки безопасности: открыть порт 1116

Настройки безопасности в Консоли управления AWS (AWS Management Console) - нужно открыть порт 1116, иначе Роботы Клиенты не смогут подключиться к Серверу Роботов через интернет:

aws_security_open_port_1116.png

Загрузить Java-архив Jar с приложением на сервер

Для проектов Maven система сборки автоматически генерирует Java-архив Jar со всеми нашими классами внутри. Архив появляется в каталоге проекта: JavaTcpServerMaster/target/JavaTcpServerMaster-1.0-SNAPSHOT.jar.

Загрузим этот файл с локального компьютера на удаленный сервер в каталог /home/ubuntu/robotc/ (на Linux при помощи любимого sftp-клиента, на Windows с WinSCP) - получим на удаленной машине файл:

/home/ubuntu/robotc/JavaTcpServerMaster-1.0-SNAPSHOT.jar

Запуск Сервера Роботов

Заходим по SSH на наш сервер (в Linux просто команда ssh, в Windows через PuTTY):

] ssh robotc.lasto4ka.su

konsole_ssh0_lasto4ka.png

Установим платформу Java, если не сделали этого ранее:

] sudo apt-get update
] sudo apt-get install default-jre


Проверим, что всё ОК:
] java -version
java version "1.7.0_51"
OpenJDK Runtime Environment (IcedTea 2.4.4) (7u51-2.4.4-0ubuntu0.13.10.1)
OpenJDK 64-Bit Server VM (build 24.45-b08, mixed mode)

Проверим, что архив Jar с приложением на месте:
] cd robotc
] ls


konsole_ssh1_robotserver_check.png


Запустим наше приложение Сервер Роботов, как любое другое Java-приложение, с явным указанием исполняемого класса (он содержит функцию public static void main):

] java -cp JavaTcpServer-1.0-SNAPSHOT.jar edu.nntu.robotserver.RobotServer1

konsole_ssh2_robotserver_start.png

На этом пауза - как написать Робота Клиента, который будет подключаться к Серверу Роботов и выполнять его команды на плате ChipKIT WF32 и на смартфоне Андроид, рассмотрим на следующих занятиях.


Но для полноты картины всё-таки немного забежим вперед и посмотрим, как будет вести себя Сервер Роботов, если к нему подключится плата ChipKIT WF32.

К Серверу Роботов подключился Робот Клиент - можно вводить команды:

konsole_ssh3_robotserver_connected1.png

Включим лампочку: ledon, Enter:

konsole_ssh4_robotserver_ledon.png

server_chipkit1_ledon.jpg

Выключим лампочку:  ledoff, Enter:

konsole_ssh5_robotserver_ledoff.png

server_chipkit2_ledoff.jpg

Избавимся от подключенного клиента: kick

konsole_ssh6_robotserver_kick.png


Чтобы завершить работу сервера, нужно набрать комбинацию Ctrl+C.

Замечание: Если приложение не было остановлено явным образом и при запуске новой сессии ssh старый Сервер Роботов по какой-то причине еще работает (например открытый терминал с запущенным сервером остался на работе, а дома возникло желание погонять его из дома) и занимает указанный порт 1116, перед запуском нового сервера старый можно убить командой 'killall java' (это остановит все java-процессы на машине, в том числе Сервер Роботов).




исходники занятия, подсветка синтаксиса.
Tags: chipkit, облако, сервер роботов, типовые задачи
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment