Asterisk звонилка на .NET

в 15:52, , рубрики: .net, asterisk, c#.net

Добрый день! Хочу рассказать как мы пытались совершать звонки c Asterisk'a используя при этом.NET (C#).

Предыстория...
В один прекрасный день нам потребовалось организовать обзвоны используя Asterisk. Так как я более менее знаком с языком C# (ну и платформой .NET), то было решено разрабатывать «звонилку» именно на этом языке. Руководствуясь первым правилом (для меня), которое гласит: «Никогда не стоит изобретать велосипед», я начал искать готовые решения. На самом деле существует очень много бесплатных .NET библиотек для работы с Asterisk'ом, одну из которых я и начал использовать, и это — AsterNet. Написав более менее рабочее приложение, начали тестировать, и уже практически сразу стало понятно, что библиотека нам категорически не подходит. Не подходит, потому что начала выбрасывать исключение — TimeoutException. Очень сильно расстроившись, написал об ошибке на странице проекта (на Codeplex). Кому интересно, вот ссылка — asternet.codeplex.com/discussions/569974. Поняв, что спасение утопающего дело рук самого утопающего, и то, что времени уже практически нет, решил сам написать тот минимальный функционал, который мне нужен.

Начало...

Для работы с сетью выбрал TcpClient, на сколько я понял — это обёртка над обычными сокетами, но работать с нею гораздо удобнее. Минимальный функционал, который мне был необходим (отправка команды Originate, и получения Response, и Cdr) я реализовал достаточно быстро.
В двух словах структура приложения была такова: TcpClient подключается к Aster'у, получает поток в который нужно отсылать команды, и слушать ответы (NetworkStream). Исходя из описания TcpClient'a и его NetworkStream'a — один NetworkStream можно одновременно использовать для чтения и записи, при этом никаких ошибок не возникнет. Соответственно, создал два потока в приложении, куда передавал NetworkStream, и там в «вечном цикле» тот крутился, записывая/считывая данные.

Проблема...

Все сломалось когда Aster начинал отвечать кучей данных. В потоке, который отправлял данные выбрасывался IOException, гласивший примерно о том, что "… ваш хост-компьютер разорвал соеденение...". При этом в потоке, который читал данные все было замечательно, никаких Exception'ов, только свойство NetworkStream'aDataAvailable всегда false. И тут мы погрузились с головой в мир Wireshark'a, пытаясь выяснить, что же все таки происходить в сети. Но все неудачно. Немного поразмыслив, было принято решение переписать все на родные Socket'ы. И опять результат тот же, но зато уже появились некоторые предположения.
Оказалось, что если записывать данные из события Cdr (которое бросает Aster) в БД, то все рушится. Если же не записывать — все прекрасно работает. Было принято вернутся к более удобному TcpClient'y!

Сами проблемы создали, сами и решаем...

Если проблема из-за записи в БД, то смотрим код потока, который считывает информацию, допустим это Method1:

while(true)
{
     if (!_stream.DataAvailable)
     {
          Thread.Sleep(1);
     }
     else
     {
          while (_stream.DataAvailable && (_readBytesCount = _stream.Read(_data, 0, _data.Length)) > 0)
          {
               _buffer.Append(Encoding.ASCII.GetString(_data, 0, _readBytesCount));
          }
          //делаем Splt, что бы распарсить все сообщение от Aster'a
          var eventsAndMessages = _buffer.ToString().Split(new string[] { "rnrn" }, StringSplitOptions.None);
          _buffer.Clear();
          //блокируем доступ к очереди событий, что бы другой поток, пока мы добавляем данные ничего не натворил
          lock (_events)
          {
                    foreach (var i in eventsAndMessages)
                              _events.Enqueue(i);
          }
     }
     Thread.Sleep(1);
}

Это примитивный код, который позволяет читать данные из потока, потом их парсит что бы получить все сообщения от Aster'a, и добавляет эти сообщения в очередь (Queue на основе string с названием _events), предварительно блокируя ее.

А это код уже другого метода (назовем например Method2), который работает так же в другом потоке, и который получает сообщение из очереди (переменная _events), предварительно ее блокируя:

 while (true)
{
     lock (_events)
     {
          //нужно вычитать все сообщения из очереди
          while (_events.Count > 0)
          {
               //тут получаем сообщение из очереди, парсим его, понимаем что это за событие, если оно нам нужно, и на него
               //кто-то подписан - тогда генерируем event, например OnCdr
          }
     }
     Thread.Sleep(1);
}

Вот Method2 и происходили все плохие дела.

Если чисто теоретически пройтись по алгоритму работы программы, то получается:
1. в Method1 мы получили данные то Aster'a, заблокировали переменную очереди _events, и начали добавлять в нее данные. После чего идет разблокировка очереди (переменной _events)
2. Method2 дождался момента когда очередь не заблокирована, блокирует ее сам, и начинает генерировать события (выбирая элементы очереди). Если событие Cdr, то метод генерирует OnCdr передает туда всю информацию, дальше обработчик события после получения инфы, пытается добавить ее БД. После вставки в БД управление возвращается в опять в Method2, который например получает уже второй элемент очереди, опять генерит событие, и опять работа с БД. И так пока в очереди не останется ни одного элемента.
3. Method1 в это же время, уже опять получил данные от Aster'a, но так как переменная очереди _events заблокирована в Method2, он просто ждет. Пока он ждет, Aster еще несколько раз присылает данные, и через некоторое время в другом потоке приложения, который отправляет данные происходит IOException. А в методе Method1 при этом все отлично, никаких исключений нет. Когда переменная _events станет доступной, он добавит в нее полученные данные от Aster'a, и все, ТИШИНА. Больше читать он ничего не будет, так как _stream.DataAvailable будет всегда возвращать false. Для меня это странное поведение.

Решение!

Как только мы примерно поняли на чем может «затыкаться» приложение, сразу начали думать как можно улучшить код в методе Method2. Решение довольно таки простое — это вместо while (_events.Count > 0) сделать if(_events.Count > 0). Таким образом мы будем получать только одно значение из очереди. Из-за этого будет задержка в получении данных от Aster'a, так как при стабильном обзвоне количество строк в _events будет только расти. Но все это уже некритично, главное что все данные приходят, и пускай даже с задержкой в одну минуту, но они будут записаны в БД.
string data = null;

lock (_events)
{
     if (_events.Count > 0)
     {
          data = _events.Dequeue();
     }
}
if (data != null)
{
     //если удалось получить данные, то генерим события и тд и тп
}

При таком подходе удалось блокировать очередь на минимальное время, и при этом на данный момент приложение стабильно работает!

Вопрос...

Буду очень благодарен, если кто-то может объяснить почему так происходит, что если длительное время не читать с потока, но при этом отправлять данные, то разрывается соединение.

Автор: stalsoft

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js