Греем уши (часть 1)

от автора

Признайтесь, было ли у Вас когда-нибудь желание послушать, о чем говорят друзья, жены, мужья и т.д. без Вас? Было бы классно набрать телефонный номер, и чтобы связь сама устанавливалась и не ждать, когда пользователь ответит на звонок. Я уже активно пользуюсь этим функционалом и это, скажу Вам, затягивает. Реализовывая его в рамках одного мобильного приложения, я долго искал готовые решения, но, конечно же, ничего не нашел. В итоге за 2 вечера (6 часов), мне удалось реализовать эту задачу. Но обо всем по порядку…

Сформируем задачу: Реализация возможности принудительных звонков клиентам VoIP-сети. Клиенты работают на мобильных телефонах IOS и Android.

Условно давайте разобьем задачу на северную сторону и клиентскую. В этой части я напишу о сервере. Итак, начнем…

Перед тем как развернуть собственный сервер, я искал какие-то бесплатные аналоги. Мне требовалось чтобы внутри VoIP сети провайдер не брал деньги и предоставлял возможность API регистрации клиентов. Нет, такого нет, или одно, или другое. Ну я не готов платить, даже 0.003$ за минуту, мне бы всё сразу и бесплатно. Выход один, стать самому VoIP провайдером.

Разворачиваем виртуальную машину
Предлагаю сначала определиться, что делает сервер. Я не записывал телефонные разговоры и поэтому решил ограничится обычной виртуальной машиной с маленьким жестким диском, обязательным статическим IP-адресом, CPU:400 MHz и RAM:512 МB.
 Накатил на нее Debian и выполнил ряд стандартных команд

apt-get update && apt-get upgrade — обновляем версии пакетов.

apt-get install asterisk — устанавливаем последнюю версию телефонии из репозитория.

apt-get install asterisk-mysql — возможность хранить пользователей в базе данных и синхронизировать с Asterisk

apt-get install mysql-server5 — устанавливаем сервер баз данных MySQL.

apt-get install mc — устанавливаем midnight commander, для удобства редактирования файлов конфигурации.

Если на этом этапе посыпались ошибки или что-то не получилось, то дальше можете не читать, сначала прочтите об установке пакетов в Debian.

Развертываем и настраиваем Asterisk
    Asterisk из пакета вполне работоспособна, лично мне не было необходимости производить сборку из исходников. Перед настройкой давайте определимся, что мы от него хотим.
Возможность принимать звонки внутри нашей VoiP сети.
Абонент может находитбся где угодно, ему не нужно быть физически в одной подсети, между сервером и клиентом интернет, с его шлюзами, НАТами и еще черт знает чем.
Хранение пользователей в специальной базе данных.
Свое красивое имя (телефонный номер) в моем случае ivan@bestmyfamily.com

С требованиями определились, давайте приступать к конфигурации.
Открываем MC и переходим в каталог /etc/asterisk

Находим файл asterisk.conf, нажимаем f4 и добавляем в конец файла вот такие строки

[compat]
pbx_realtime=1.6
res_agi=1.6
app_set=1.6

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

В начале этого файла в секции [options] Вы можете поставить два параметра:

verbose = 64
debug = 64

теперь в консоли, после запуска команды asterisk -r мы будем видеть все что происходит с нашим сервером, так сказать, в режиме реального времени.

Теперь настроим прием звонков. В нашей папке конфигурационных файлов Asterisk, находим файл extensions.conf и в его конец добавляем

[myfamily]    # Тут можете написать что угодно,
                   # это название Вашего плана звонков,
                   # но он должен совпадать с Вашим полем context в Sip.conf

exten => _[a-z].,1,Dial(SIP/${EXTEN},60)
exten => _[A-Z].,2,Dial(SIP/${EXTEN},60)

Давайте разберем это на примере обычного звонка.
ivan@bestmyfamily.com звонит своей жене по телефону Zaya@bestmyfamily.com

Вызов поступает к серверу и происходит следующее:
_[a-z].  — вот этой записью Астериск ищет, что делать с этим телефонным номером. Сюда он подставит нашу “Zaya”. И так как Zaya у нас с большой буквы, то произойдет переход к следующей строчке. К какой строчке переходить указываете Вы, после запятой порядковым номером. Соответственно, все звонки в моем случае прогоняются через первую строчку с №1 и, если такой номер не найден, тогда направляем его на №2.
В нашем случае Zaya подходит ко второй строчке и мы можем что-то с ней сделать, например, включить ей музыку ожидания, направить в какой-то отдел или еще что-то. Это все можно сделать специальными командами. Самая простая из них — Dial. Она просто пытается дозвониться по заданным каналам пользователю за отведенное время.
Мы указали SIP канал и 60 секунд ожидания. Если перевести вот эту строчку SIP/${EXTEN},60 на русский, получим следующее: направить вызов на этот телефон SIP/Zaya@bestmyfamily.com и подождать 60 секунд.

Нам осталось настроить самый главный файл для SIP телефонии — sip.conf. Тут нужно быть осторожным, мне пришлось попрыгать с бубном пару часов, чтобы всё заработало. И если всё работает из одной подсети, это не значит, что Вы сможете нормально принимать или соединять двух абонентов в разных сетях за НАТ. Я настроил работу следующим образом:

[general]
allowguest=no    
allowoverlap=no         
context=myfamily; контекст по умолчанию для входящих звонков
bindport=5060; порт UDP который "слушает" asterisk
bindaddr=0.0.0.0

pedantic=no
directmedia=no
rtptimeout=10
rtpholdtimeout=300

; для realtime
; они заставляют asterisk кэшировать данные
; и команда sip show peers будет нормально отображать
; всех зарегистрированных realtime пользователей
rtcachefriends = yes
rtcache=yes

; подключение к базе данных, вообще их тут писать не обязательно с версии 1.2
dbhost = 127.0.0.1
dbname = ***
dbuser = ***
dbpass = ***
dbport = 3306

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

Нам остался последний файл res_config_mysql.c, тут мы указываем настройки подключения к базе данных.

[general]
dbhost = 127.0.0.1
dbname = ***
dbuser = ***
dbpass = ***
dbport = 3306

Теперь делаем перезагрузку Астериск командой /etc/init.d/asterisk restart если нет ошибок, тогда можете радоваться, сервер настроен и готов к обработке звонков.

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

CREATE TABLE `sipusers` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`accountcode` VARCHAR(20) NULL DEFAULT NULL,
`disallow` VARCHAR(100) NULL DEFAULT 'all',
`allow` VARCHAR(100) NULL DEFAULT 'g729;ilbc;gsm;ulaw;alaw',
`allowoverlap` ENUM('yes','no') NULL DEFAULT 'yes',
`allowsubscribe` ENUM('yes','no') NULL DEFAULT 'yes',
`allowtransfer` VARCHAR(3) NULL DEFAULT NULL,
`amaflags` VARCHAR(13) NULL DEFAULT NULL,
`autoframing` VARCHAR(3) NULL DEFAULT NULL,
`auth` VARCHAR(40) NULL DEFAULT NULL,
`buggymwi` ENUM('yes','no') NULL DEFAULT 'no',
`callgroup` VARCHAR(10) NULL DEFAULT NULL,
`callerid` VARCHAR(80) NULL DEFAULT NULL,
`cid_number` VARCHAR(40) NULL DEFAULT NULL,
`fullname` VARCHAR(40) NULL DEFAULT NULL,
`call-limit` INT(8) NULL DEFAULT '0',
`callingpres` VARCHAR(80) NULL DEFAULT NULL,
`canreinvite` CHAR(6) NULL DEFAULT 'yes',
`context` VARCHAR(80) NULL DEFAULT NULL,
`defaultip` VARCHAR(15) NULL DEFAULT NULL,
`dtmfmode` VARCHAR(7) NULL DEFAULT NULL,
`fromuser` VARCHAR(80) NULL DEFAULT NULL,
`fromdomain` VARCHAR(80) NULL DEFAULT NULL,
`fullcontact` VARCHAR(80) NULL DEFAULT NULL,
`g726nonstandard` ENUM('yes','no') NULL DEFAULT 'no',
`host` VARCHAR(31) NOT NULL DEFAULT 'dynamic',
`insecure` VARCHAR(20) NULL DEFAULT NULL,
`ipaddr` VARCHAR(15) NOT NULL DEFAULT '',
`language` CHAR(2) NULL DEFAULT NULL,
`lastms` VARCHAR(20) NULL DEFAULT NULL,
`mailbox` VARCHAR(50) NULL DEFAULT NULL,
`maxcallbitrate` INT(8) NULL DEFAULT '384',
`mohsuggest` VARCHAR(80) NULL DEFAULT NULL,
`md5secret` VARCHAR(80) NULL DEFAULT NULL,
`musiconhold` VARCHAR(100) NULL DEFAULT NULL,
`name` VARCHAR(80) NOT NULL DEFAULT '',
`nat` VARCHAR(30) NOT NULL DEFAULT 'no',
`outboundproxy` VARCHAR(80) NULL DEFAULT NULL,
`deny` VARCHAR(95) NULL DEFAULT NULL,
`permit` VARCHAR(95) NULL DEFAULT NULL,
`pickupgroup` VARCHAR(10) NULL DEFAULT NULL,
`port` VARCHAR(5) NOT NULL DEFAULT '',
`progressinband` ENUM('yes','no','never') NULL DEFAULT 'no',
`promiscredir` ENUM('yes','no') NULL DEFAULT 'no',
`qualify` CHAR(3) NULL DEFAULT NULL,
`regexten` VARCHAR(80) NOT NULL DEFAULT '',
`regseconds` INT(11) NOT NULL DEFAULT '0',
`rfc2833compensate` ENUM('yes','no') NULL DEFAULT 'no',
`rtptimeout` CHAR(3) NULL DEFAULT NULL,
`rtpholdtimeout` CHAR(3) NULL DEFAULT NULL,
`secret` VARCHAR(80) NULL DEFAULT NULL,
`sendrpid` ENUM('yes','no') NULL DEFAULT 'yes',
`setvar` VARCHAR(100) NOT NULL DEFAULT '',
`subscribecontext` VARCHAR(80) NULL DEFAULT NULL,
`subscribemwi` VARCHAR(3) NULL DEFAULT NULL,
`t38pt_udptl` ENUM('yes','no') NULL DEFAULT 'no',
`trustrpid` ENUM('yes','no') NULL DEFAULT 'no',
`type` VARCHAR(6) NOT NULL DEFAULT 'friend',
`useclientcode` ENUM('yes','no') NULL DEFAULT 'no',
`defaultuser` VARCHAR(80) NOT NULL DEFAULT '',
`usereqphone` VARCHAR(3) NOT NULL DEFAULT 'no',
`videosupport` ENUM('yes','no') NULL DEFAULT 'yes',
`vmexten` VARCHAR(80) NULL DEFAULT NULL,
`useragent` VARCHAR(80) NULL DEFAULT NULL,
`regserver` VARCHAR(80) NULL DEFAULT NULL,
`callbackextension` VARCHAR(80) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `name` (`name`),
INDEX `name_2` (`name`)
)
COLLATE='cp1251_general_ci'
ENGINE=MyISAM
ROW_FORMAT=DYNAMIC
AUTO_INCREMENT=1;

И сразу добавим двух наших пользователей. Я добавляю их через специальный PHP скрипт для работы с SIP базой данных:

<?php    // // Менеджер работы с SIP сервером // class M_SIP { 	private static 	$instance; 		// Экземпляр класса. 	 	const host 		= "bestmyfamily.com"; 	const user 		= "***"; 	const password 	= "***"; 	const db 		= "***"; 	 	public $dbSIP; // Указатель на базу данных. 	 	// 	// Синглтон. 	// 	public static function Instance() 	{ 		if(self::$instance == null) 			self::$instance = new M_SIP(); 				 		return self::$instance; 	} 	 	// 	// Подключение 	// 	function __construct() 	{ 		// Подключаемся к базе данных 		$this->dbSIP = mysqli_connect(self::host, self::user, self::password) or exit; 		$result = mysqli_select_db($this->dbSIP, self::db) or exit; 		mysqli_query($this->dbSIP, 'SET NAMES utf8');   	} 	  	 	 	// Зарегистрировать пользователя на сервере 	public function RegUserOnServer($pid, $uid) 	{ 		$arr = array( 					'accountcode' 				=> Null,  					'disallow'    				=> 'all', 					'allow'   	  				=> 'ulaw;alaw', 					'allowoverlap'   	  		=> 'no', 					'allowsubscribe'   	  		=> 'no', 					'allowtransfer'   	  		=> Null, 					'amaflags'   	  			=> Null, 					'autoframing'   	  		=> Null, 					'auth'   	  				=> Null, 					'buggymwi'   	  			=> 'no', 					'callgroup'   	  			=> Null, 					'callerid'   	  			=> Null, 					'cid_number'   	  			=> Null, 					'fullname'   	  			=> Null, 					'call-limit'   	  			=> 0, 					'callingpres'   	  		=> Null, 					'canreinvite'   	  		=> "no", 					'context'   	  			=> 'myfamily', 					'defaultip'   	  			=> Null, 					'dtmfmode'   	  			=> Null, 					'fromuser'   	  			=> Null, 					'fromdomain'   	  			=> Null, 					'fullcontact'   	  		=> Null, 					'g726nonstandard'   	  	=> 'no', 					'host'   	  				=> 'dynamic', 					'insecure'   	  			=> Null, 					'ipaddr'   	  				=> Null, 					'language'   	  			=> 'en', 					'lastms'   	  				=> 0, 					'mailbox'   	  			=> Null, 					'maxcallbitrate'   	  		=> 384, 					'mohsuggest'   	  			=> Null, 					'md5secret'   	  			=> Null, 					'musiconhold'   	  		=> Null, 					'name'   	  				=> "ИМЯ", 					'nat'   	  				=> 'force_rport,comedia', 					'outboundproxy'   	  		=> Null, 					'deny'   	  				=> Null, 					'permit'   	  				=> Null, 					'pickupgroup'   	  		=> Null, 					'port'   	  				=> '', 					'progressinband'   	  		=> 'no', 					'promiscredir'   	  		=> 'no', 					'qualify'   	  			=> Null, 					'regexten'   	  			=> 1000001, 					'regseconds'   	  			=> 0, 					'rfc2833compensate'   	  	=> 'no', 					'rtptimeout'   	  			=> Null, 					'rtpholdtimeout'   	  		=> Null, 					'secret'   	  				=> $uid, 					'sendrpid'   	 		 	=> 'yes', 					'setvar'   	  				=> '', 					'subscribecontext'   	  	=> Null, 					'subscribemwi'   	  		=> Null, 					't38pt_udptl'   	  		=> 'no', 					'trustrpid'   	  			=> 'no', 					'type'   	  				=> 'friend', 					'useclientcode'   	  		=> 'no', 					'defaultuser'   	  		=> "Имя", 					'usereqphone'   	  		=> 'no', 					'videosupport'   	  		=> 'no', 					'vmexten'   	  			=> Null, 					'useragent'   	  			=> Null, 					'regserver'   	  			=> Null, 					'callbackextension'   	  	=> Null 					); 							 					 		return $this->Insert('sipusers', $arr);			 	} 	 	 	 	 	 	 	// 	// Вставка строки 	// $table 		- имя таблицы 	// $object 		- ассоциативный массив с парами вида "имя столбца - значение" 	// результат	- идентификатор новой строки 	// 	private function Insert($table, $object, $isReplace = false) 	{		 		$columns = array(); 		$values = array(); 	 		foreach ($object as $key => $value) 		{ 			$key = mysqli_real_escape_string($this->dbSIP, $key); 			$columns[] = "`" . $key . "`"; 			 			if ($value === null) 			{ 				$values[] = 'NULL'; 			} 			else 			{ 				$value = mysqli_real_escape_string($this->dbSIP, $value);							 				$values[] = "'$value'"; 			} 		} 		 		$columns_s = implode(',', $columns); 		$values_s = implode(',', $values); 			 			// Если на замену? 		if ($isReplace)	 			$query = "REPLACE INTO $table ($columns_s) VALUES ($values_s)"; 		else 			$query = "INSERT IGNORE  INTO $table ($columns_s) VALUES ($values_s)"; 	 	 		$result = mysqli_query($this->dbSIP, $query); 		 		if (!$result) 			die($this->SqlError(mysqli_error($this->dbSIP)));  		$id = mysqli_insert_id($this->dbSIP); 		 		return $id; 	} 	 	 	// 	// Обработчик ошибок SQL 	// 	function SqlError($error) 	{ 		return json_encode(array( 								 'code' 		=> 99, 								 'error' 		=> $error 								) 						   );  	} }  

Последний шаг в настройке — покупка нашего имени и привязка его к ip адресу. Я завел домен третьего уровня для этих целей voip.bestmyfamily.com и привязал к нему ip адрес. Если Вы забудете этот шаг, Ваши телефонные номера не будут столь красивыми ivan@192.168.1.1:5060

Для тестирования можно установить любые SIP телефоны. Конечно, они не такие классные и не будут втихаря поднимать трубку, кому мы звоним. Но обычную телефонную связь Вы настроили. Во второй части мы напишем наши волшебные клиенты на Android и IOS.
Сразу оговорюсь, что у Вас могут быть проблемы с одобрением в яблоке. Мы ооочень долго проходили процесс согласования приложения. Это не стандартные 2 недели, у нас изучали код и тестировали функционал перед размещением приложения. В итоге сошлись на возможности запускать прослушку только с детским режимом.

ссылка на оригинал статьи https://habrahabr.ru/post/282187/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *