Asterisk звонилка на .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[] { "\r\n\r\n" }, 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) {      //если удалось получить данные, то генерим события и тд и тп } 

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

Вопрос…

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

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


Комментарии

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

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