277 lines
33 KiB
Markdown
277 lines
33 KiB
Markdown
|
# Глава 16. Введение в интерактивное голосовое меню
|
|||
|
|
|||
|
> _Однажды Алиса подошла к развилке и увидела на дереве Чеширского кота. - По какой дороге мне пойти?- спросила она._
|
|||
|
> _“Куда ты хочешь пойти?- был его ответ._
|
|||
|
> _“Не знаю, - ответила Алиса._
|
|||
|
> _“Тогда, - сказал кот, - это не имеет значения.”_
|
|||
|
>
|
|||
|
> - Льюис Кэролл
|
|||
|
|
|||
|
Термин интерактивое голосовое меню (на самом деле ответ) (IVR) часто неправильно используется для обозначения автосекретаря, но это очень разные вещи. Цель системы IVR состоит в том, чтобы принять входные данные от абонента, выполнить действие, основанное на этих входных данных (обычно, поиск данных во внешней системе, такой как база данных), и сообщить результат абоненту. Назначение автосекретаря (о котором мы говорили в [Главе 14](glava-14.md)) - маршрутизация вызовов. Первоначально IVR даже не должен был быть телефонной системой. Все, что принимало информацию от человека и выдавало на запрос результат, падало вместе с областью IVR. Традиционно системы IVR были сложными, дорогими и раздражающими в реализации. Asterisk все это изменил.
|
|||
|
|
|||
|
## Компоненты IVR
|
|||
|
|
|||
|
Самые основные элементы IVR очень похожи на элементы автосекретаря, хотя цель и отличается. Нам нужно по крайней мере одно приветствие чтобы сообщить вызывающему, что ожидает IVR, метод получения входных данных от вызывающего, логику для проверки что ответ вызывающего является допустимым вводом, логику определения следующего шага IVR, и, наконец, механизм хранения ответов, если это применимо. Мы могли бы думать об IVR как о дереве решений, хотя оно не должно иметь никаких ветвей. Например, опрос может представлять точно такой же набор подсказок для каждого вызывающего абонента, независимо от того, какой выбор делают абоненты и единственная логика маршрутизации, включенная в опрос, заключается в том, являются ли полученные ответы допустимыми для вопросов.
|
|||
|
|
|||
|
С точки зрения вызывающего абонента, каждый IVR должен начинаться с подсказки. Этот первоначальный запрос будет сообщать абоненту что он попал на IVR и попросит собеседника ввести первые данные. Мы обсуждали подсказки в автосекретаре в Главе 14. Позже мы создадим диалплан, который позволит вам лучше управлять несколькими голосовыми подсказками.
|
|||
|
|
|||
|
Второй компонент IVR - это метод получения входных данных от вызывающего абонента. Напомним, что в Главе 14 мы обсуждали `Background()` и `WaitExten()` как метод получения нового расширения. Хотя вы можете создать IVR с помощью `Background()` и `WaitExten()` обычно проще и практичнее использовать приложение `Read()`, которое обрабатывает как приглашение, так и захват ответа. Приложение `Read()` было разработано специально для использования с системами IVR. Его синтаксис выглядит следующим образом:
|
|||
|
|
|||
|
```
|
|||
|
Read(variable[,filename[&filename2...]][,maxdigits][,option][,attempts][,timeout])
|
|||
|
```
|
|||
|
|
|||
|
Аргументы описаны в Таблице 16-1.
|
|||
|
|
|||
|
*Таблица 16-1. Приложение Read()*
|
|||
|
|
|||
|
<table border="1" width="100%" cellpadding="5">
|
|||
|
<tr>
|
|||
|
<td><p align="left"><b>Аргумент</b></p></td>
|
|||
|
<td><p align="left"><b>Цель</b></p></td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td><code>variable</code></td>
|
|||
|
<td>Переменная, в которой хранится ответ абонента. Рекомендуется присвоить каждой переменной в IVR имя, аналогичное приглашению, связанному с этой переменной. Это поможет позже, если по деловым соображениям или простоте использования вам потребуется изменить порядок шагов IVR. Присвоение имен переменным <code>var1</code>, <code>var2</code> и т.д. может показаться более простым в краткосрочной перспективе, но позже в вашем жизненном цикле это сделает исправление ошибок более трудным.</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td><code>prompt</code></td>
|
|||
|
<td>Файл (или список файлов, соединенных вместе с символом <code>&</code>) для воспроизведения вызывающему абоненту, запрашивающий ввод. Не забудьте опустить расширение в конце каждого имени файла.</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td><code>maxdigits</code></td>
|
|||
|
<td>Максимальное количество символов, которые можно использовать в качестве входных данных. В случае вопросов "Да/нет" и "множественный выбор" рекомендуется ограничить это значение <code>1</code>. В случае более длинных значений вызывающий абонент всегда может прервать ввод, нажав клавишу <code>#</code>.</td>
|
|||
|
</tr>
|
|||
|
<tr>
|
|||
|
<td><code>options</code></td>
|
|||
|
<td><p><code>s</code>(skip)</p>
|
|||
|
<p>Немедленно выйти, если канал не отвечает.</p>
|
|||
|
<p><code>i</code>(indication)</p>
|
|||
|
<p>Вместо того чтобы воспроизводить подсказку, воспроизведите какой-либо сигнал индикации (например, сигнал набора номера).</p>
|
|||
|
<p><code>n</code>(no answer)</p>
|
|||
|
<p>Считывание цифр от абонента, даже если на линию еще не ответили.</p>
|
|||
|
<p><code>attempts</code></p>
|
|||
|
<p>Количество раз для воспроизведения подсказки. Если вызывающий не вводит ничего, приложение <code>Read()</code> может автоматически запросить пользователя. По умолчанию используется одна попытка.</p>
|
|||
|
<p><code>timeout</code></p>
|
|||
|
<p>Количество секунд, в течение которых вызывающий должен совершить свой ввод. Значение по умолчанию в Asterisk равно 10 секундам, хотя его можно изменить для одного приглашения с помощью этой опции или для всего сеанса, назначив значение с помощью функции диалплана <code>TIMEOUT(response)</code>.</p></td>
|
|||
|
</tr>
|
|||
|
</table>
|
|||
|
|
|||
|
Как только входные данные получены, они должны быть проверены. Если вы не проверите входные данные, то с большей вероятностью обнаружите, что ваши абоненты жалуются на нестабильное приложение. Недостаточно обрабатывать ожидаемые входные данные; вам также нужно обрабатывать входные данные, которых вы не ожидаете. Например, абоненты могут быть разочарованы и набрать 0 в вашем IVR; если вы сделали хорошую работу, вы будете обращаться с этим деликатно и соедините их с кем-то, кто может помочь им или предоставить полезную альтернативу. Хорошо спроектированный IVR (как и любая программа) будет пытаться предвидеть все возможные входные данные и предоставлять механизмы для изящной их обработки.
|
|||
|
|
|||
|
После проверки входных данных вы можете отправить их на внешний ресурс для обработки. Это может быть сделано с помощью запроса в базу данных, отправки в URI, программы AGI или многих других вещей. Это внешнее приложение должно выдать результат, который вы сможете передать обратно абоненту. Это может быть подробный результат такой как "Баланс вашего счета..." или простое подтверждение такое как "ваш счет был обновлен". Мы не можем придумать ни одного реального случая, когда не будет требоваться какой-то результат, возвращаемый звонящему.
|
|||
|
|
|||
|
Иногда IVR может иметь несколько шагов, и поэтому результат может включать запрос дополнительной информации от вызывающего абонента для перехода к следующему шагу приложения IVR.
|
|||
|
|
|||
|
Можно проектировать очень сложные системы IVR с десятками или даже сотнями возможных путей. Мы уже говорили об этом раньше и повторим еще раз: люди не любят разговаривать с вашей телефонной системой независимо от того насколько она умна. Держите ваше IVR простым для ваших абонентов и они гораздо более вероятно получат некоторую выгоду от него.
|
|||
|
|
|||
|
<table border="1" width="100%" cellpadding="5">
|
|||
|
<tr>
|
|||
|
<td>
|
|||
|
<p align="center"><b>Безупречно вкусное IVR</b></p>
|
|||
|
<p>Отличный пример IVR, который любят использовать люди - это тот, который используют многие компании по доставке пиццы: когда вы звоните, чтобы сделать заказ, IVR смотрит на Ваш CallerID и говорит: "Если вы хотите точно такой же заказ, как в прошлый раз, нажмите 1."</p>
|
|||
|
<p>Это все, что оно делает, и это прекрасно.</p>
|
|||
|
<p>Очевидно, что эти компании могли бы разработать массивно сложные IVR, которые позволили бы Вам выбрать каждую деталь вашего пирога ("для семизерновой корочки, нажмите 7"), но сколько нетрезвых, голодных клиентов могли бы успешно перемещаться по чему-то подобному в 3 часа ночи?</p>
|
|||
|
<p>Лучшее IVR - это те, которые требуют наименьшего количества входных данных от вызывающего абонента. Жми эту кнопку 1 и Ваша'ца уже в пути! Ура!</p>
|
|||
|
</td>
|
|||
|
</tr>
|
|||
|
</table>
|
|||
|
|
|||
|
## Конструктивные соображения IVR
|
|||
|
|
|||
|
При разработке вашего собственного IVR, есть некоторые важные вещи, которые следует иметь в виду. Мы составили этот список вещей, которые нужно и не нужно делать в вашем IVR.
|
|||
|
|
|||
|
Делать
|
|||
|
* Держать его простым.
|
|||
|
* Должна быть возможность набрать 0, чтобы связаться с живым человеком.
|
|||
|
* Корректно обрабатывать ошибки.
|
|||
|
|
|||
|
Не делать
|
|||
|
|
|||
|
* Подумать что IVR может полностью заменить людей.
|
|||
|
* Использовать ваше IVR, чтобы показать людям насколько вы умны.
|
|||
|
* Попробовать воспроизвести ваш сайт с помощью IVR.
|
|||
|
* Постараться построить IVR когда не можете принять числовой или устный ввод. Никто не хочет писать свое имя на клавиатуре телефона.<sup><a href="#sn1">1</a></sup>
|
|||
|
* Заставлять своих абонентов слушать рекламу. Помните, что они могут повесить трубку в любой момент, когда пожелают.
|
|||
|
|
|||
|
## Модули Asterisk для создания IVR
|
|||
|
|
|||
|
"Фронтенд" IVR (части, которые взаимодействуют с абонентами) может обрабатываться в диалплане. Можно построить систему IVR, используя только диалплан (возможно, с использованием astdb для хранения и извлечения данных); однако, как правило, вам нужно будет взаимодействовать с чем-то внешним по отношению к Asterisk (“бэкенд” IVR).
|
|||
|
|
|||
|
### CURL()
|
|||
|
|
|||
|
Функция диалплана `CURL()` в Asterisk позволяет охватить все веб-приложения одной строкой кода диалплана. Мы будем использовать его в нашем примере IVR в этой главе позже.
|
|||
|
|
|||
|
Возможно вы найдете `CURL()` довольно простым в использовании, создание веб-приложения потребует опыта работы с веб-разработкой.
|
|||
|
|
|||
|
### func_odbc
|
|||
|
|
|||
|
Используя `func_odbc` можно разрабатывать чрезвычайно сложные приложения в Asterisk, используя только код диалплана и поиск по базе данных. Если вы не являетесь сильным программистом, но очень хорошо разбираетесь в диалпланах Asterisk и базах данных, вы полюбите `func_odbc` так же, как и мы. Проверьте это в [Главе 15](glava-15.md).
|
|||
|
|
|||
|
### AGI
|
|||
|
|
|||
|
Интерфейс Asterisk Gateway является настолько важной частью интеграции внешних приложений с Asterisk, что мы посвятили ему отдельную главу. Дополнительную информацию вы найдете в [Главе 18](glava-18.md).
|
|||
|
|
|||
|
### AMI
|
|||
|
|
|||
|
Интерфейс Asterisk Manager - это интерфейс сокета, который можно использовать для получения информации о конфигурации и состоянии, запроса выполняемых действий и уведомления о событиях происходящих с вызовами. Мы также написали целую главу об АМИ. Дополнительную информацию вы найдете в [Главе 17](glava-17.md).
|
|||
|
|
|||
|
### ARI
|
|||
|
|
|||
|
Интерфейс Asterisk REST основан на знаниях, полученных в течение многих лет о том, как интегрировать Asterisk с веб-приложениями текущего поколения. Это настолько важно, что да, еще раз, есть целая глава, посвященная ему. Если вы хотите построить сложное IVR с помощью Asterisk, более подробно рассмотрите ARI в [Главе 19](glava-19.md).
|
|||
|
|
|||
|
## Простое IVR с использованием CURL()
|
|||
|
|
|||
|
Прежде чем приступить к написанию внешней программы для обработки чего-либо, мы всегда тщательно обдумываем, есть ли способ выполнить работу в диалплане. Один из мощных способов, которым Asterisk может взаимодействовать с внешними данными - это URL-адрес, что очень хорошо делает программа GNU/Linux cURL. В Asterisk функция `CURL()` является функцией диалплана.
|
|||
|
|
|||
|
Мы собираемся использовать `CURL()` в качестве примера того, как может выглядеть чрезвычайно простое IVR. Мы запросим наш внешний IP-адрес у [https://ipinfo.io/ip](https://ipinfo.io/ip).<sup><a href="#sn2">2</a></sup>
|
|||
|
|
|||
|
<table border="1" width="100%" cellpadding="5">
|
|||
|
<tr>
|
|||
|
<td>
|
|||
|
<p align="left"><b>Примечание</b></p>
|
|||
|
<p>На самом деле, большинство приложений IVR будут намного сложнее. Даже большинство применений <code>CURL()</code> будет сложным, так как URI может возвращать массивный и сильно изменяющийся объем данных, подавляющее большинство из которых будет непонятно Asterisk. Дело в том, что IVR - это не только диалплан; это также очень много о внешних приложениях, которые запускаются диалпланом, которые выполняют реальную работу IVR.</p>
|
|||
|
</td>
|
|||
|
</tr>
|
|||
|
</table>
|
|||
|
|
|||
|
Модуль `CURL()` был установлен во время нашего процесса установки несколько глав назад.
|
|||
|
|
|||
|
### Диалплан
|
|||
|
|
|||
|
Диалплан для нашего примера IVR очень прост. Функция `CURL()` извлекает ваш IP-адрес из [https://ipinfo.io/ip](https://ipinfo.io/ip), а затем `SayAlpha()` озвучит результат вызывающему абоненту:
|
|||
|
|
|||
|
```
|
|||
|
exten => *764,1,Verbose(2, Run CURL to get IP address from whatismyip.org)
|
|||
|
same => n,Answer()
|
|||
|
same => n,Set(MyIPAddressIs=${CURL(https://ipinfo.io/ip)})
|
|||
|
same => n,SayAlpha(${MyIPAddressIs})
|
|||
|
same => n,Hangup()
|
|||
|
```
|
|||
|
|
|||
|
Простота этого до невозможности крута. В традиционной системе IVR на программирование такого рода может уйти несколько дней, если предположить, что это вообще возможно.
|
|||
|
|
|||
|
## A Prompt-Recording IVR Function
|
|||
|
|
|||
|
В [Главе 14](glava-14.md) мы создали простой диалплан для записи подсказок. Он был довольно ограничен в том, что записывал только одно имя файла, и поэтому для каждого запроса требовалось отдельное расширение. Здесь мы расширим его чтобы создать полноценное меню для записи подсказок. Поскольку это сложная часть диалплана, а не подпрограмма или локальный канал, мы создадим новый раздел диалплана для различных функций и поместим туда такие вещи:
|
|||
|
|
|||
|
```
|
|||
|
;FEATURES
|
|||
|
[prompts]
|
|||
|
exten => s,1,Answer
|
|||
|
exten => s,n,Set(step1count=0) ; Инициализация счетчиков
|
|||
|
|
|||
|
; If we get no response after 3 times, we stop asking
|
|||
|
same => n(beginning),GotoIf($[${step1count} > 2]?end)
|
|||
|
same => n,Read(which,prompt-instructions,3)
|
|||
|
same => n,Set(step1count=$[${step1count} + 1])
|
|||
|
|
|||
|
; All prompts must be 3 digits in length
|
|||
|
same => n,GotoIf($[${LEN(${which})} != 3]?beginning)
|
|||
|
same => n,Set(step1count=0) ; Запрос успешен; сброс счетчиков
|
|||
|
same => n,Set(step2count=0)
|
|||
|
|
|||
|
same => n(step2),Set(step2count=$[${step2count} + 1])
|
|||
|
same => n,GotoIf($[${step2count} > 2]?beginning) ; Нет ответа после 3 попыток
|
|||
|
|
|||
|
; If the file doesn't exist, then don't ask whether to play it
|
|||
|
same => n,GotoIf($[${STAT(f,/var/lib/asterisk/sounds/${which}.wav)} = 0]?recordonly)
|
|||
|
same => n,Background(prompt-tolisten)
|
|||
|
|
|||
|
same => n(recordonly),Background(prompt-torecord)
|
|||
|
same => n,WaitExten(10) ; Ожидаем 10 секунд ответа
|
|||
|
same => n,Goto(step2)
|
|||
|
|
|||
|
same => n(end),Playback(goodbye)
|
|||
|
same => n,Hangup()
|
|||
|
|
|||
|
exten => 1,1,Set(step2count=0)
|
|||
|
same => n,Background(/var/lib/asterisk/sounds/${which})
|
|||
|
same => n,Goto(s,step2)
|
|||
|
|
|||
|
exten => 2,1,Set(step2count=0)
|
|||
|
same => n,Playback(prompt-waitforbeep)
|
|||
|
same => n,Record(${CHANNEL(uniqueid)}.wav)
|
|||
|
|
|||
|
same => n(listen),Playback(${CHANNEL(uniqueid)})
|
|||
|
same => n,Set(step3count=0)
|
|||
|
same => n,Read(saveornot,prompt-1tolisten-2tosave-3todiscard,1,,2,3)
|
|||
|
same => n,GotoIf($["${saveornot}" = "1"]?listen)
|
|||
|
same => n,GotoIf($["${saveornot}" = "2"]?saveit)
|
|||
|
same => n,GotoIf($["${saveornot}" = "3"]?tossit)
|
|||
|
same => n,Goto(listen)
|
|||
|
|
|||
|
same => n(tossit),System(rm -f /var/lib/asterisk/sounds/${CHANNEL(uniqueid)}.wav)
|
|||
|
same => n,Goto(s,beginning)
|
|||
|
|
|||
|
same => n(saveit),Noop('Set' app used to shorten example)
|
|||
|
same => n,Set(PromptToSave=/var/lib/asterisk/sounds/${CHANNEL(uniqueid)}.wav
|
|||
|
same => n,Set(WhereToSave=/var/lib/asterisk/sounds/${which}.wav
|
|||
|
same => n,System(mv -f ${PromptToSave} ${WhereToSave})
|
|||
|
same => n,Playback(prompt-saved)
|
|||
|
same => n,Goto(s,beginning)
|
|||
|
```
|
|||
|
|
|||
|
В этой системе имя запроса больше не является описательным; вместо этого оно является числом. Это означает, что вы можете записывать гораздо большее разнообразие приглашений, используя один и тот же механизм, но компромисс заключается в том, что ваши приглашения больше не будут иметь описательных имен.
|
|||
|
|
|||
|
Если вы хотите проверить его, вам нужно будет записать подсказки, которые использует эта функция IVR (это своего рода мета, но да, нашему создателю подсказок нужны подсказки).
|
|||
|
|
|||
|
Поместите это в свой диалплан:
|
|||
|
|
|||
|
```
|
|||
|
exten => 510,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-tolisten)) ; нажмите 1
|
|||
|
exten => 511,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-torecord)) ; нажмите 2
|
|||
|
exten => 512,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-instructions)) ;3-цифры (от 000 до 999)
|
|||
|
exten => 513,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-waitforbeep)) ; ждите сигнала
|
|||
|
exten => 514,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-1tolisten-2tosave-3todiscard))
|
|||
|
exten => 515,1,GoSub(subRecordPrompt,${EXTEN},1(prompt-saved))
|
|||
|
```
|
|||
|
|
|||
|
Затем позвоните им по одному и запишите по мере необходимости.
|
|||
|
|
|||
|
После того, как вы записали подсказки, необходимые вашему создателю подсказок, вы должны быть в состоянии проверить их.
|
|||
|
|
|||
|
```
|
|||
|
exten => *742,1,Noop(Prompts)
|
|||
|
same => n,Goto(prompts,s,1)
|
|||
|
same => n,Hangup()
|
|||
|
```
|
|||
|
|
|||
|
С этого момента вы можете записывать приглашения, используя только числовой идентификатор. Вам понадобится способ отслеживать что говорит подсказка, но с точки зрения записи вам не нужно больше писать диалплан каждый раз, когда нужна подсказка.
|
|||
|
|
|||
|
## Распознавание речи и преобразование текста-в-речь
|
|||
|
|
|||
|
Хотя традиционно и по-прежнему в большинстве случаев сегодня система IVR представляет предварительно записанные подсказки вызывающему абоненту и принимает ввод через панель набора номера, также возможно: а) искусственно генерировать подсказки, широко известные как преобразование текста-в-речь; и б) принимать устный ввод через механизм распознавания речи.
|
|||
|
|
|||
|
В то время как концепция возможности вести интеллектуальный разговор с машиной - это то, что авторы научной фантастики обещают нам в течение многих лет, реальная наука об этом остается сложной и подверженной ошибкам. Несмотря на свои удивительные возможности, компьютеры плохо приспособлены к задаче оценки тонких нюансов человеческой речи.
|
|||
|
|
|||
|
Тем не менее, следует отметить, что такие компании, как Google, достигли удивительных успехов как в преобразовании текста-в-речь так и в распознавании речи. Уже доступны API, которые могут проделать замечательную работу по осмыслению того, что им говорят. Google, конечно, выигрывает от наличия массивного бэкенда, который может выполнять почти чудесные трюки обработки; то, что ваш IVR не сможет полностью использовать.
|
|||
|
|
|||
|
### Преобразование текста-в-речь
|
|||
|
|
|||
|
Преобразование текста-в-речь (также известное как синтез речи) требует, чтобы система была способна искусственно создавать фразы из сохраненных данных. Хотя было бы неплохо, если бы мы могли просто назначить звук букве и заставить компьютер воспроизводить каждый звук, когда он читает буквы, письменный язык часто не фонетичен и редко отражает нюансы речи (английский, возможно, один из худших языков в этом отношении).
|
|||
|
|
|||
|
Существуют отличные API, доступные от Google (и других), которые сделают очень хорошую работу по чтению того, что было написано. На момент написания этой книги все еще очень очевидно, что речь идет о компьютере, но тем не менее можно генерировать системные подсказки на лету из текста, а не записывать их заранее. Полезность этого трудно оценить, так как люди все еще не заинтересованы в разговоре с вашими машинами; они позвонили потому что хотят поговорить с вами.
|
|||
|
|
|||
|
### Распознавание речи
|
|||
|
|
|||
|
Поскольку нам удалось убедить наши компьютеры говорить с нами, мы, естественно, хотим иметь возможность говорить и с ними.<sup><a href="#sn3">3</a></sup>
|
|||
|
|
|||
|
Распознавание речи раньше было сложным и дорогостоящим, но недавно Google выпустила API, который позволяет огромной мощности их возможностей распознавания речи быть доступной для внешних приложений.
|
|||
|
|
|||
|
## Вывод
|
|||
|
|
|||
|
Asterisk является отличной платформой IVR. Вся эта книга, во многом, учит вас навыкам, которые могут быть применены для развития IVR. В то время как основные СМИ действительно уделяют внимание Asterisk только как “свободной УАТС”, реальность такова, что Asterisk является наиболее мощной, когда используется в качестве IVR. В любой солидной организации очень вероятно, что системные администраторы Linux используют Asterisk для решения телекоммуникационных проблем, которые ранее были либо неразрешимыми, либо невероятно дорогими для решения. Это скрытая революция, но не менее значимая из-за своей относительной неизвестности.
|
|||
|
|
|||
|
Если вы занимаетесь IVR-бизнесом, вам обязательно нужно познакомиться с Asterisk.
|
|||
|
|
|||
|
---
|
|||
|
|
|||
|
<ol>
|
|||
|
<li id="sn1">Особенно если это что-то вроде Ван Меггелена.</li>
|
|||
|
<li id="sn2"> Эти бесплатные сайты поиска IP-адресов, похоже, все время покупаются и превращаются в рекламные шлюзы, поэтому то, что работало при написании этой книги, может больше не работать. Вам нужен сайт который вернет ваш IP-адрес и ничего больше. Сегодня, например <a href="https://ipinfo.io/ip">https://ipinfo.io/ip</a> К тому времени, когда вы прочтете это может быть что-то другое.</li>
|
|||
|
<li id="sn3">Вообще-то, большинство из нас разговаривает с компьютерами, но это редко бывает вежливо.</li>
|
|||
|
</ol>
|
|||
|
|
|||
|
[Глава 15. Интеграция реляционной базы данных](glava-15.md) | [Содержание](SUMMARY.md) | [Глава 17. AMI и файлы вызовов](glava-17.md)
|