Java: IP-телефония с нуля

    В предыдущей статье «Транслируем звук по сети с помощью Java» я описывал способ приема и трансляции звука по сети встроенными средствами Java.

    Здесь я продолжу развивать эту идею, и расскажу, как сделать с помощью Java простую систему IP-телефонии.

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

    Полностью исходники можно посмотреть на github.

    Всех заинтересовавшихся прошу под кат.

    NetworkingAudioServer — серверная часть системы


    Серверная часть работает на базе сервлетов, Apache Tomcat и MySQL.

    Структура базы данных

    База данных состоит из двух таблиц: users — хранит учетные данные пользователей и userinfo — сопоставляет каждому пользователю IP и время его последнего обновления.

    CREATE TABLE IF NOT EXISTS `users` (
        `email` varchar(100) NOT NULL PRIMARY KEY, 
        `password` varchar(100) NOT NULL, 
        `confirm` varchar(100) NOT NULL,
        `user_id` varchar(100) NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    


    Названия email и password говорят сами за себя, confirm — токен подтверждения почтового адреса либо 'done', если e-mail подтвержден, user_id — md5-хэш от адреса электронной почты (так как он имеет фиксированную длину, его удобно использовать для идентификации входящего звонка).

    CREATE TABLE IF NOT EXISTS `userinfo` (
        `email` varchar(100) NOT NULL PRIMARY KEY, 
        `user_id` varchar(100) NOT NULL, 
        `ip` varchar(100) NOT NULL, 
        `last_update` TIMESTAMP NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    


    Поля email и user_id соответствуют полям из предыдущей таблицы, что такое ip понятно из названия, last_update — время последнего обновления IP.

    Сервлеты

    RegisterServlet регистрирует пользователя в базе данных и высылает на электронную почту запрос на подтверждение с помощью следующего скрипта:

    #!/bin/sh
    email=$1
    confirm=$2
    SERVER_URL="http://tabatsky.ru/networkingaudio";
    TMP_FILE="/common_scripts/tmp/$confirm";
    
    echo "To: $email" > $TMP_FILE;
    echo "From: service@tabatsky.ru" >> $TMP_FILE;
    echo "Subject: Networking Audio e-mail confirmation" >> $TMP_FILE;
    echo >> $TMP_FILE;
    echo "$SERVER_URL/confirm?confirm=$confirm" >> $TMP_FILE;
    echo >> $TMP_FILE;
    
    sendmail $email < $TMP_FILE
    


    ConfirmServlet — выполняет подтверждение e-mail.

    UpdateIPServlet — обновляет IP клиента при запросе, IP определяется автоматически.

    GetUserInfoServlet — в качестве параметра принимает email или user_id, при запросе возвращает email или user_id и текущий IP, либо значение 'offline', если IP не обновлялся более трех минут.

    Настройка серверной части

    Для установки серверной части нужно:
    • Создать базу данных MySQL
    • Указать правильные значения логина, пароля и базы данных MySQL в классе MyDBConnect
    • Собрать и задеплоить на Tomcat
    • Указать правильное значение SERVER_URL в скрипте отправки почты, сам скрипт разместить по адресу /common_scripts/sendConfirm и установить права на выполнение
    • Создать папку /common_scripts/tmp и установить права на запись


    jNetworkingAudioClient — консольный клиент


    Структура клиента

    Консольный клиент состоит из четырех классов с программной логикой — Main, Master, Slave и IPUpdater, и двух вспомогательных классов — Util и DeclinedException.

    Класс Util хранит настройки клиента — такие как параметры звука и объем буфера.

    Класс Main отвечает за программную логику интерфейса.

    Класс IPUpdater каждые 90 секунд отправляет сервлету запрос на обновление IP.

    Класс Master слушает сетевой порт и, в свою очередь, содержит в себе два вложенных класса-потока: MicrophoneReader — читает данные с микрофона и Sender — отправляет данные второму абоненту.

    Класс Slave: инициирует соединение, отправляя второму абоненту md5-хэш адреса электронной почты, затем, если звонок принят, начинает читать данные из сокета и отправлять их на аудио-выход.

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

    Запуск клиента

    Здесь можно взять готовый исполняемый jar.

    Запуск:

    java -jar jNetworkingAudioClient.jar http://serverUrl 2>log.txt
    


    Желающие могут попробовать на моем сервере:

    java -jar jNetworkingAudioClient.jar http://tabatsky.ru/networkingaudio/ 2>log.txt
    


    Но: в целях хабраэффектоустойчивости сервера я установил ограничение в 100 зарегистрированных пользователей.

    И последнее: если Вы сидите за роутером, нужно сделать редирект порта 7373 на свою машину.

    UPD:


    Возникла небольшая проблема — когда я пытаюсь обновлять свой собственный IP через внешний хост, сервлет вместо моего внешнего IP получает IP моего роутера во внутренней сети — 192.168.1.1.

    Придумал небольшой костыль — триггер для MySQL:

    CREATE TRIGGER `my_host_ip` BEFORE INSERT ON `userinfo`
    FOR EACH ROW SET NEW.ip=IF(NEW.ip='192.168.1.1','tabatsky.ru',NEW.ip);
    
    • +1
    • 11,6k
    • 4
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 4
    • +4
      Очень замечательно, что Вы проявляете интерес к описанной Вами теме, но, уж извините, если хотите показать пример использования языка программирования или технологии для решения каких-то задач, то, пожалуйста, делайте это более ответственно, не учите неопытных людей плохому. Огромные методы без комментариев, да ещё с бесконечными циклами с метками! Не закрываете соединяния с БД, при каждом обращении создаёте новое. Зачем вам платформозависимый скрипт для отсылки подтверждения регистрации? Зачем тогда Java?
      • +1
        Спасибо, я учел Ваши замечания.
        Закрыл соединения с БД, сделал отправку почты средствами Java и прокомментировал код клиента.
      • +1
        на электронную почту запрос на подтверждение с помощью следующего скрипта:

        платформа дала вам javamail, нет, хочу использовать платформо-зависимые скрипты…
        extends HttpServlet

        есть же готовый jersey, почему нельзя использовать его для контроля над сервлетами? время 1.5 закончилось, уже не надо самому писать сервлеты километровые
        Указать правильные значения логина, пароля и базы данных MySQL в классе MyDBConnect

        на дворе заканчивался 2014 год, а люди всё ещё руками пишут коннект к базе…
        Консольный клиент

        в джаве нынче несколько GUI тулкитов встроено, зачем все эти танцы с консолью и парсингом строк?
        • +2
          Попытка разработки велосипеда в области VoIP — это замах на сверхкрупное:
          1. Вопросы представления и кодирования медиапотока, работа с неустойчивым подключением — вам придется сделать RTP
          2. Установление прямого соединения между клиентами, находящимися за NAT: предложенный костыль в виде проброса порта — это совершенно нерабочее решение. Кстати, а если речь идет о пользователях мобильной сети — они ведь все коллективно сидят за NAT, и при этом никакого проброса порта сделать не могут.
          3. Еще страшнее, когда клиент сидит за NAT, который сам подключен через NAT.
          4. Стандартное решение здесь — STUN, TURN, SIP/ICE — и то бывают ситуации, когда эти трюки не срабатывают.
          5. В VoIP главное сейчас не голос и даже не видео (эти штуки должны «просто работать») — важны те интеграционные сервисы, которые предоставляет сервер телефонии (а ведь такой сервер в разы умнее среднестатистической цифровой офисной АТС) — около вашей тут вертятся статьи о том, как путем нехитрых скриптовых действий Asterisk учат распознавать DEF-коды и исполнять другие трюки дрессировщика.
          6. В сети, где есть DNS или статические IP адреса, если надо «просто поговорить с видео», можно просто поставить Linphone и просто звонить по номеру toto@[адрес] или toto@[имя узла]
          7. Одна из причин становления Cisco на вершину пьедестала производителей телекоммуникационного оборудования состоит в том, что они создали коммутатор, способный интегрировать различные стандарты. То есть чтобы ваш проект развился до состояния юзабельности, придется реализовывать один из попсовых протоколов, учить сервер транскодированию потоков в реальном времени и так далее, чтобы из вашей сети можно было звонить в другие сети — не проще ли было попытаться написать свою реализацию хотя бы кусочка SIP?
          8. Классическое требование при решении любой задачи — прежде чем бросаться на нее, изучить существующий опыт: поднять Asterisk, прицепить к нему модем, еще лучше IP-телефон (железный, не программный) — количество встреченных на этом пути грабель должно создать определенное представление о сложности поставленной задачи и списке необходимых для ее решения вопросов.
          9. Ну и классическое ньютоновское: — «Если я видел дальше других, то потому, что стоял на плечах гигантов.», — попытайтесь влезть на плечи гигантов, а не выпрыгивать из-за их плеч.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое