воскресенье, 13 декабря 2009 г.

Twisted в действии — memcache на python

Преамбула

В связи с выходными потратил немного времени на реализацию сервера Memcache с использованием python-фреймворка Twisted. В итоге я получил быстродействие в два раза более низкое, что я не считаю очень критичным, а также возможность реализовать парочку расширений оригинального протокола. Также возможны оптимизации, которые еще улучшат быстродействие.
Протокол не был реализован полностью - есть еще моменты над которыми можно поработать, но стандартные set/get вполне работоспособны и готовы к использованию.

Средства

Для хранения кеша используем базовый класс dict. Как вы догадываетесь, реализация dict в python быстра, этот базовый тип используется в python настолько активно, что его не оставили без детальной оптимизации. Таким образом, мы автоматом имеем структуру для хранения кеша в памяти. Осталось реализовать протокол memcache, для предоставления доступа к dict другим программам.

Для реализации сервера используем Twisted. Есть множество вариаций неблокирующего IO для python на сегодня, но Twisted это уже классика, и имеет в своем арсенале достаточно средств для легкого решения подобных задач.



Реализация сетевого протокола

Как реализуют протоколы? Первым делом вам конечно же нужно найти описание протокола. Я нашел его здесь - http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt

После прочтения протокола становится понятно, что от клиента мы получим одну или две строки, причем первую строку мы можем смело разбивать на элементы по пробелам. Вторая строка используется в командах, которые передают серверу данные - set, add, replace и т.п. Если вам хочется подробнее вникнуть в статью, то отправлю вас почитать описание самостоятельно, цели выложить его перевод сюда не было.

Вооруженные этим знанием, смотрим, что нам может предложить Twisted для решения этой задачи, и сразу находим LineOnlyReceiver - протокол из базовой поставки Twisted, который работает только с протоколами, обменивающимися строками, то есть то, что надо.
                                                                                                                  
class MemcacheProtocol(LineOnlyReceiver):                                                                                    
    """                                                                                                                      
    Реализует базис протокола - прием сообщений от клиента                                                                   
    и отдачу результата.                                                                                                     
    """                                                                                             
    def lineReceived(self,line):            
        debug(repr(line))                   
        if not 'parameters' in self.instruction:
            parameters = line.split(' ')        
            debug("Got new command "+parameters[0])
            self.instruction['parameters']=parameters

            # Если данных не ожидается, то к исполнению
            if parameters[0] in Cache.oneline_commands:
                self.process()                         
        else:                                          
            # Получены данные к двухстрочной команде, к исполнению
            debug("Got data "+line)                               
            self.instruction['data']=line                         
            self.process()                                        

    def process(self):
        # Cache.call возвращает генератор
        for line in Cache.call(self.instruction):
            # И мы отсылаем все что он нагенерирует отдельными строками
            debug("Send line "+line)                                   
            self.sendLine(line)                                        
        # Готовы к дальнейшим инструкциям, насяльника!                 
        self.instruction={}                                            

    def connectionMade(self):
        debug("Connected!")  
        self.instruction={}  

Как видно из кода, для собственно работы используется Cache. Это синглетон, по сути просто класс, методы которого обернуты декоратором @classmethod. Вызов Cache.call должен вернуть генератор, которые будет возвращать строки, которые, в свою очередь, наша реализация протокола, будет отдавать клиенту.

Разбираем запрос от клиента

Первая строка это команда и параметры, разделенные пробелами, поэтому используем строковый метод split, и на выходе получаем список. Далее его надо разобрать на составляющие, перед тем как с данными начнет работать команда. Я использую класс, так как мне нравится перспектива обращаться к параметрам, указывая их через точку. Приведенный ниже код уже требует прочтения описания протокола, а для ленивых пара наводящих строк:
Команды записи данных:                                                                                                       
     [noreply]\r\n                                                                 
cas      [noreply]\r\n                                                               

Получение данных:
get *\r\n   
gets *\r\n  
delete \r\n 

Ну и тому подобное.
Реализация разбора:


class Instruction(object):
    def __init__(self, i):
        p = i['parameters']
        self.cmd = p.pop(0)

        # Проверяем noreply
        if p[-1]=='noreply':
            self.reply=False
            # Выкидываем его
            p.pop(-1)       
        else:               
            self.reply=True 

        if self.cmd in Cache.storage_commands:
            # Если CAS то есть еще один параметр (т.е. особый случай)
            if self.cmd == "cas":                                    
                self.unique = p.pop(-1)                              

            # Теперь все параметры однозначны, но мы хотим расширить протокол,
            # потому все не так просто, как dict(zip())                       
            self.bytes = p.pop(-1)                                            
            self.exptime = p.pop(-1)                                          
            self.flags = p.pop(-1)                                            
            self.data = i.get('data',None)                                    

        # incr, decr
        elif self.cmd in ["incr","decr"]:
            self.change_value = p.pop(-1)

        self.keys = p

    def __str__(self):
        return str(self.__dict__)

Реализация хранения кеша и работы с ним

Протокол мною сразу же был расширен, а именно есть возможность работы с вложенными данными. Кеш переделан в древовидный, и все операции, которые по стандарту указывают один ключ, могут указывать список ключей, разделенных пробелами. Впрочем от этого легко избавиться, но тогда будет совсем неясен смысл работы.

В качестве единицы хранения реализован класс Entry, в котором содержится словарь(childs типа dict) с дочерними экземплярами Entry. Более того - верхней точкой в иерархии также является экземпляр класса Entry.

Здесь же я приведу фрагмент синглетона Cache:


class Cache(object):
    # consts
    storage_commands = ["set", "add", "replace", "append", "prepend","cas"]
    oneline_commands = ["get", "gets","getn", "delete", "incr", "decr", "stats"]

    # cache storage
    data = Entry(0,0,0)

    # cache operations
    @classmethod
    def call(cls, instruction):
        i = Instruction(instruction)
        debug(i)
        command = getattr(cls,i.cmd)
        return command(i)

    @classmethod
    def set(cls, i):
        "set, поддержка вложенных ключей"
        parent = cls.data.get_child(i.keys[:-1])
        if parent:
            parent.set_child(i.keys[-1], Entry(i.data,i.flags,i.exptime))
            yield "STORED"
        else:
            yield "NOT_STORED"

    @classmethod
    def get(cls, i):
        "get, не обрабатывает вложенные ключи"
        for key in i.keys:
            entry = cls.data.get_child([key])
            if entry:
                yield ' '.join(( "VALUE", key, entry.flags, str(len(entry.data)) ))
                yield entry.data
        yield "END"

Код Entry и всего остального смотрим тут - http://github.com/Deepwalker/tx-cache/blob/master/mck.py

суббота, 7 ноября 2009 г.

FreeSWITCH, сервис эха.

Обычно эхо делается так:

  • приветствуем

  • бибикаем

  • портим текст записываем в файл

  • бибикаем

  • проигрываем файл

  • откладываем скрипку прощаемся



Во FreeSWITCH есть более изящный вариант - приложение delay_echo. Единственный параметр это длительность задержки (буфера) перед воспроизведением звука обратно. Буфер размещается в памяти, диск не дергается, система не упирается в диск, но любит память. По моему восхитительно, учитывая еще и необходимость файлы стирать.


 1     <extension name="delay_echo">
 2       <condition field="destination_number" expression="^echo123$">
 3         <action application="answer"/>
 4         <action application="sleep" data="1000"/>
 5         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-greeting.wav"/>
 6         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-record_message.wav"/>
 7         <action application="sleep" data="1000"/>
 8         <action application="gentones" data="%(200,0,800)"/>
 9         <!--action application="sched_broadcast" data="+10 gentones::%(200,0,800)"/-->
10         <action application="sched_transfer" data="+20 after_echo XML public"/>
11         <action application="delay_echo" data="10000"/>
12       </condition>
13     </extension>
14
15     <extension name="delay_echo_next">
16       <condition field="destination_number" expression="after_echo">
17         <action application="sleep" data="1000"/>
18         <action application="playback" data="/opt/freeswitch/sounds/ru/RU/elena/voicemail/8000/vm-goodbye.wav"/>
19         <action application="sleep" data="1000"/>
20         <action application="hangup"/>
21       </condition>
22     </extension>
23

среда, 28 октября 2009 г.

Регистратор SIP на twisted.

Питонисты в курсе, что есть такая чудо библиотека для работы с сетевыми протоколами как Twisted. Она немного сложна поначалу, но после просветления становится совершенно необходимым инструментом.
В стандартной поставке с ней идет множество уже реализованных протоколов - IMAP, XMPP, HTTP (в сочетании с обработкой URL через джанго и какой-нибудь библиотекой шаблонов весьма полезный инструмент) и т.д. Количество же протоколов, которые реализованы на/для Twisted не поддается подсчету. Таким образом Tornado может спать спокойно - сравнивать его с Twisted вообще некорректно.

Среди стандартных протоколов нашел SIP - последний раз обновлялся 3 года назад, есть проект VoIP телефона shtoom, который также заглох. Но тем не менее вот вам регистратор на Twisted:

#!/usr/bin/env python
# coding: utf-8

from twisted.application import internet, service

import sip

DOMAIN='192.168.9.5'

application = service.Application("JuzzCallBack")

sip.RegisterProxy.registry = sip.InMemoryRegistry(DOMAIN)
sip.RegisterProxy.locator = sip.RegisterProxy.registry
sip.RegisterProxy.debug = True
proxy = internet.UDPServer(5060,sip.RegisterProxy())

proxy.setServiceParent(application)

И что удивительнее всего - он работает. На досуге хочется попробовать сделать простейший SIP-телефон, будет интересно. Назову его Shtoom ressurection, он не будет обладать даже функциями предшественника, и сдохнет на версии -0.1, ожидайте!

среда, 14 октября 2009 г.

HighLoad 2009

Из прослушки в онлайне интересующих меня секций вывел основной Highload тренд - все быстро в асинхронность! Twisted как бы уже давно и прочно занял свою нишу, особенно в свете comet/longpoll технологий.

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

среда, 6 мая 2009 г.

FreeSWITCH, mod_opal

В блоге VoIP написали о mod_opal. А почему я вообще стараюсь не упоминать об этом модуле, как о прорыве в мир H323? Да потому, что мне кажется этот модуль плохо вливается в инфраструктуру FreeSWITCH.

Самый главный минус модуля - RTP обрабатывается через Opal, то есть не используется RTP стек самого FreeSWITCH. Это в свою очередь означает, что кодеки надо делать под оба стека.

Никакого Celt, Siren, G729 не будет, по крайней мере сразу. Последнего так уж точно, нет у меня лично никакого интереса писать еще один вариант.

Ну и любой программист вам сразу скажет, что это ужасно поддерживается в разработке - два(!!!) RTP стека это двойная работа по отлову багов, разработке расширений и тому подобное. И нет никаких преимуществ от того, что глюками Opal занимаются его разработчики - ошибки в Opal также будут искать и в сообществе FreeSWITCH, причем гораздо активнее, чем в каких либо еще сообществах.

Поэтому я не рассматриваю mod_opal как подходящий вариант ставящий точку в вопросе поддержки H323 в FreeSWITCH.

суббота, 25 апреля 2009 г.

Python, plasma, KDE 4.2

Бороздил интернет в поисках упоминания меня и наткнулся на интересную запись - http://neithere.livejournal.com/419237.html
Понял почему не работали мои плазмоиды (недосуг было с ними разбираться). Дело было в одной маленькой строчке которую надо добавить в metadata.desktop:

X-Plasma-MainScript=code/main.py

Полезно почитать про себя любимого.

четверг, 23 апреля 2009 г.

Здравствуй, хозяин!

Первым дистрибутивом Linux, в котором я смог нормально начать работать, был Slackware. Произошло это потому, что он не обманывал меня всякими мастерами, в отличие от Mandrake. Так вот очень мне запомнились сообщения программы Fortune, которая выводит изречения, цитаты и прочее текстовое, что обладает достаточно малым размером.

А есть так же программа "cowsay", которая выводит в псевдографике персонажа с текстом как в комиксах. В частности изначально это была корова, но в моем дистрибутиве персонажей много. В общем я совместил Fortune и Cowsay, что не сильно то ново. Но главное я написал маленький скрипт, чтобы персонаж также выбирался случайным образом.

Итого:

kuku@kuku$ fortune | cowsay -f `python ~/rand_cow.py`
kuku@kuku$ cat rand_cow.py
import os
import random
# Хмм, а краткость то сестра криптографов...
print random.choice([i[:-4] for i in os.listdir("/usr/share/cowsay/cows/")])

вторник, 21 апреля 2009 г.

Новый вариант G729 для FreeSWITCH

В прошлый раз мне в комментариях подкинули исходники g729 для Asterisk с использованием библиотеки от ITU-T. С тех пор я и планировал переписать mod_g729 на его использование. Это может быть полезным если вы не используется x86 архитектуру, или вам влом качать 300 Мб с сайта Intel, или, в конце концов, вы один из тех, кто уже третью ночь воюет с адски непрофессионально написанным Makefile.
Одним слово я собрался все таки и написал. Как вы понимаете, когда за дело берется администратор, а не программист, ваши риски серьезно увеличиваются. Так что берем, компилируем, ищем баги и тп. А я пойду уже спать, до побудки оставалось 4-е часа...

http://github.com/Deepwalker/fs_itu_g729/tree/master

пятница, 17 апреля 2009 г.

А все таки она развивается!

История такая - в городе Томске трафик в его пределах бесплатен. То есть вы платите за подключение и качаете сколько влезет на скорости до 100Mbit/s.
Ну и логично совершенно вытекает, что в городе много зеркал всего. В частности есть зеркало для репозитариев Ubuntu - ubuntu.opentomsk.net, Slackware - slackware.tomsk.ru и тп.

Так вот, не знаю почему, но репозитарии от 8.10 они грохнули и выложили 9.04. Волей не волей, а пришлось обновляться, и делал я это с замиранием сердца - я вообще часто свой рабочий компьютер держу на пике прогресса, и печального опыта у меня полно.

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

P.S. Правда еще что то стало со шрифтами, но я не великий борец за их идеальное начертание и тп.

вторник, 17 февраля 2009 г.

fs2web развивается

А я все продолжаю развивать fs2web - web-приложение для управления FreeSWITCH через xml_curl.

Что уже есть:
* поддержка привязки directory, то есть конфигурация пользователей;
* поддержка dialplan, конфигурация номерного плана;
* управление конференциями через xml_rpc.

На данный момент все управление в основном работает через встроенную админку Django, что не мешает функционированию.

В планах дальнейшее развитие - удобное редактирование номерного плана, поддержка шлюзов (gateways).

Чего не хватает - не хватает пользователей, которые тестируют и желают развития.

Использование.



Вам понадобится установленный фреймворк Django, желательно trunk версия, и lxml.

Запуск - cd fs2web; ./manage.py runserver

Для редактирования настроек надо зайти в административный интерфейс: http://127.0.0.1:8000/admin/
Логин admin, пароль kuku.

В conf/autoload_configs/xml_curl.conf.xml:

<configuration name="xml_curl.conf" description="cURL XML Gateway">
<bindings>
<binding name="fs2web_user_fetcher">
<param name="gateway-url" value="http://127.0.0.1:8000/user/get/" bindings="directory"/>
</binding>
<binding name="fs2web_dialplan_fetcher">
<param name="gateway-url" value="http://127.0.0.1:8000/dialplan/get/" bindings="dialplan"/>
</binding>
</bindings>
</configuration>


И включить загрузку модуля xml_curl в conf/autoload_configs/modules.conf.xml

понедельник, 9 февраля 2009 г.

freeswitch.ru

Создан проект русского wiki посвященного FreeSWITCH - http://freeswitch.ru

Сейчас там мало информации, так как времени пополнять ее сейчас также мало. И этим постом я приглашаю всех к работе над наполнением базы.

вторник, 20 января 2009 г.

Путешествие группы формант по голосовому тракту. Глава вторая.

Обсуждение плана. Прелести монтажа под открытым небом в хорошую погоду. - То же - в дурную погоду. Принимается компромиссное решение. Первые впечатления от Монморенси. Не слишком ли он хорош для этого мира? Опасения отброшены как необоснованные. Заседание откладывается.


Аналоговая сигнализация


Простейшая сигнализация используется в аналоговой телефонии - вы поднимаете трубку, а оттуда раздается радостный, располагающий к набору номера, гудок. Гудок вам посылает АТС, которая узнала о вашем проявлении интереса по снятой трубке => замкнутой цепи. Вы с радостью тыкаете кнопки, а телефон, также радостно, безусловно, отщелкивает ваши нажатия импульсами, или пропевает его комбинациями двух частот АТС. АТС, сама или с товарками быстренько разбирается в сути вашего щелкуче-певучего послания и найдя вашего адресата, подает сигнал его аппарату, что пора трезвонить, чем этот аппарат и займется со всей щенячьей радостью. А пока аппарат вызываемого абонента будет дергаться в параксизмах довольства, вы будете слышать длинные гудки, которые будут успокаивающе сообщать вам, что АТС о вас не забыла. Если аппарат дергался зря, и трубку никто так и не возьмёт, вы получите серию коротких гудков.

Нетрудно заметить, что аналоговая сигнализация такого типа рассчитана отнюдь не на автоматику. Короткие и длинные гудки гарантировано не являются стандартными - ведь характеристика короткий/длинный вполне человеком воспринимается. А значит и заботиться тут не о чем.

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

А вот где и вправду очень обидно становится - это дозвон до абонента традиционной телефонной сети. Есть во FreeSWITCH такая переменная call_timeout (подобное есть в параметрах Dial в Asterisk), которая задает время попытки дозвониться. Если в течении скажем 20 секунд трубку никто не взял, то вызов уйдет в голосовую почту, например. Так вот если вы настроите переадресацию на свой сотовый аппарат, и захотите в случае неудачи перевести звонок в голосовую почту - ничего не выйдет. Шлюзу фактически невозможно определить идет ли вызов или уже кто то взял трубку (если это конечно не GSM шлюз, в GSM шлюзах с сигнализацией все хорошо). Также определить по коротким гудка занят ли абонент, или положил трубку после разговора, сложно.

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

Таким образом видно моё отношение к перспективам аналоговых стыков с городскими АТС - нет перспектив - аналоговая сигнализация в цифровом веке это атавизм, зачем эти костыли, когда АТС совершенно точно знает взяли на том конце трубку или нет. Весь вопрос всего лишь в том, что она об этом никак не сообщает, рассчитывая на то, что у аппарата человек, который признает собрата.

Очевидно, что стык надо делать или цифровым (ну тут из вариантов я только E1 видел, и тот R1.5, и PRI еще поискать надо, хотя, возможно, где то есть еще и ISDN BRI), или делать VoIP стык. Сейчас уже многие провайдеры предоставляют городские телефоны через VoIP.

В следующей серии зянятно-развлекательное описание протокола SIP.

воскресенье, 4 января 2009 г.

G.729 для FreeSWITCH готов

То есть я собрал из библиотек IPP, кодека для Asterisk, и mod_g729 от FreeSWITCH свой mod_g729. Кодирование работает прекрасно, декодирование пока вызывает вопросы.

Забавный эффект - собеседника, подключенного по G.729 слышно, но есть артефакты, иногда, если сильно тараторить в трубку, фразы начинают долетать медленно. Хотя возможно во всем виновата связь - тестировал с удаленным шлюзом, так как найти G.729 в другом месте не смог.

Забирайте тут. Тестируйте, правьте код.

Обсуждение здесь.

Обновление: тестирование с моим Nokia E61 показало хорошие результаты и на декодировании.