Статьи

Спецификации протокола написаны на Python

Это запись моего выступления на Software Passion Summit в Гетеборге, Швеция. Для получения дополнительной информации см. Пост, который я сделал до конференции.

Написание спецификации на полном языке программирования, таком как Python, имеет свои плюсы и минусы. С другой стороны, Python не предназначен для декларативного языка, поэтому любая попытка сделать его декларативным (помимо простого перечисления собственных типов данных) потребует некоторой настройки и / или инструментов для работы. С другой стороны, имея объявление на языке, на котором вы пишете свои серверы, вы можете использовать саму спецификацию, а не сгенерированную производную этой спецификации, и написание пользовательских (в данном случае минимальных) генераторов для других языков просто, поскольку вы можете ли вы на Python провести самоанализ для прохождения вашей спецификации и шаблонной логики по вашему выбору для генерации источника — это позволяет, например, ориентироваться на терминал J2ME, который просто не будет принимать существующие решения,и где удаление файла jar 150 КБ для реализации протокола не является альтернативой.

Для меня это путешествие началось примерно в 2006 году, когда я начал терять контроль над документацией и версиями протокола для протокола, используемого между терминалами и серверами в решении для управления парком машин Visual Units Logistics . После поиска и отбрасывания нескольких существующих инструментов и после того, как мы вдохновились тем фактом, что мы обычно настраиваем Javascript в Javascript, я начал рисовать (например, чернила на бумаге), как будет выглядеть спецификация протокола в Python. Это транскрипция того, что я придумал в то время:

imei = long
log_message = string
timestamp = long
voltage = float
log = Message(imei, timestamp,
           log_message, voltage)
protocol = Protocol(log, ...)
protocol.parse(data)

Исходя из этого, я создал первую версию реализации протокола. Он выглядел аналогично целевой версии, но страдал от обилия повторений:

#protocol.py
LOG = 0x023
ALIVE = 0x021
message = Token('message', 'String', 'X')
timestamp = Token('timestamp', 'long', 'q')
signal = Token('signal', 'short', 'h')
voltage = Token('voltage', 'short', 'h')
msg_log = Message('LOG', LOG, timestamp, signal, voltage)
msg_alive = Message('ALIVE', ALIVE, timestamp)
protocol = Protocol(version=1.0, messages=[msg_log,msg_alive])
#usage
from protocol import protocol
parsed_data = protocol.parse(data)
open('Protocol.java’,'w').write(protocol.java_protocol())

Реализация вокруг этого проста; класс Token знает, как анализировать часть сообщения, класс Message знает, какие токены использовать (и в каком порядке), а класс Protocol выбирает правильный экземпляр Message, используя сопоставление байтов маркера с экземплярами Message.

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

Некоторые вещи усложнили создание лучшей версии. Наихудшей проблемой из всех них оказалась я, я и я. В то время я использовал Python пару лет и начал интересоваться более сложными инструментами. Я только что научил себя метаклассам и подумал, что это гениальное применение объектной ориентации — и, найдя новый блестящий молоток, мне не терпелось найти гвоздь.

К сожалению, у меня не было острой необходимости использовать метаклассы, поэтому я изобрел один — я хотел избежать некоторых назначений в спецификации протокола, поэтому я использовал метаклассы, чтобы вырвать метод init (конструктор) и заменить его версией, которая зарегистрировала экземпляр в глобальной карте, а затем вызвал оригинальный метод init. Это неверно, по крайней мере, тремя способами — поскольку это не было универсальным, это можно было бы сделать напрямую в методе init, если бы оно было общим, это было бы работой для декоратора, и это действительно отличный способ запутать код:

__MSG_MAPPING__ = {}
def msg_initizer(cls, old_init):
    def new_init(self, name, marker, *args):
        __MSG_MAPPING__get(cls, {})[name] = self
        __MSG_MAPPING__[cls][struct.pack("!B", marker)] = self
        old_init(self, name, marker, *args)
    return new_init
class RegisterMeta(type):
    def __new__(cls, name, bases, attrs):
        attrs['__init__'] = msg_initizer(cls,
                                         attrs['__init__'])
        return super(RegisterMeta, cls).__new__(cls,
                                      name, bases, attrs)
class Message(object):
    __metaclass__ = RegisterMeta

Это тот код, которым я не горжусьиз, кстати. Худшая часть? Он даже не удалил дублирование, хотя несколько снизил его — и глобальная регистрация сообщений при загрузке протокола действительно испортила любую попытку поддержки нескольких версий. Это была не единственная проблема; Я также пошел за борт и хотел поддержать указание синтаксиса протокола, используя класс Flow, который определял законное упорядочение сообщений. Это могло бы быть хорошей идеей, если бы у нас действительно были такие требования в наших протоколах; поскольку они «проверяют подлинность, делают что угодно», добавление поддержки для этого просто расширило кодовую базу и сделало спецификацию протокола более сложной для чрезвычайно малого выигрыша (тем более, что мы проверяем подлинность различными способами в зависимости от клиента). Добавление оскорбления к травме, это даже более многословно, чем с первой попытки.

#In protocol.py
imei = Token('imei', 'long')
message = Token('message', 'String')
timestamp = Token('timestamp', 'long')
signal = Token('signal', 'short')
voltage = Token('voltage', 'short')
auth = Token('auth', 'String')
Markers({'LOG': 0x023,
    'ALIVE': 0x021,
    'AUTH': 0x028})
Message('LOG', imei, timestamp, signal, voltage)
Message('ALIVE', imei, timestamp)
Message('AUTH', imei, timestamp, auth)
Flow([('AUTH'), ('LOG', 'ALIVE')])
#Usage
protocol = Protocol(version=2.0)
parsed_data = protocol.parse(data) #error if not auth parsed

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

Наконец, и продолжая, я решил применить тщательно отмеренное количество стандартной магии библиотеки, чтобы сделать спецификации более краткими, и убрать вещи, которые нам не нужны. Вместо этого спецификация выглядела примерно так:

#In protocol_4.2.py:
#Tokens
t('message', string)
t('timestamp',  i64)
t('signal', i16)
t('voltage', i16)
#Messages
LOG = ('A log message containing some debug info',
         0x023, timestamp, message, signal, voltage)
ALIVE = ('A message to signal that the terminal alive',
     0x021, timestamp)
#Usage
protocols = load_protocols('protocols')
parsed = protocols[4.2].parse(data)
protocols[4.2].write_java() #Writes to Protocol42.java

В свое время он был даже terser (как и в предыдущем блоге пост ), но эта версия не очень удаются, а версия в производстве очень похож на этот. Во избежание дублирования имен используются два разных метода: токены определяются путем вызова метода t, который создает экземпляр токена и внедряет его обратно в пространство имен вызывающего пользователя, используя предоставленное имя:

#In types.py
from inspect import currentframe
def t(name, data_type):
    """Inserts name = (name, data_type) in locals()
    of calling scope"""
    currentframe().f_back.f_locals[name] = (name, data_type)

Кому-то это может показаться богохульством, но учтите это — реализация чрезвычайно проста по своей концепции, она выполняет свою работу и ее легко объяснить. Другое изменение заключается в том, что сообщения создаются исключительно с использованием inspect для извлечения членов модуля, которые выглядят как сообщения — имя во всех заглавных буквах и кортеж. Стоит отметить, что изначально была обработка ошибок, но я удалил ее, чтобы сделать синтаксический анализ неудачным, вместо того, чтобы принять спецификацию, которая может содержать или не содержать ошибки.

Наконец, исходный код java и html-документация создаются путем обхода экземпляра протокола и подачи информации в простые шаблоны — эксперименты проводились с использованием грамотного программирования с использованием ReST для создания документации, но в итоге это имело тенденцию запутывать, а не наоборот. Это может быть следствием наивной реализации или проблемы не очень хорошо подходит для грамотного программирования, но в любом случае это не стоило того.

В bitbucket есть рабочая и слегка обобщенная версия , и если вы хотите больше узнать об этом (и узнать больше об использовании магии Python), вы можете купить билет на EuroPython — до воскресенья вы будете голосовать за мой предложения (и другие).