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

Для начала пара слов о том, зачем это вообще было нужно и почему я не использовал одну из уже существующих клиентских библиотек.

В процессе работы над серверной частью игрового проекта у нас часто возникали служебные задачи, такие как получение текущих данных напрямую из приложения, graceful остановка игрового сервиса (нельзя просто так взять и вырубить игровой сервер, пока на нем есть люди). В самом начале для работы со всеми служебными частями приложения было написано небольшое GUI приложение, посредством которого эти все проблемы и решались. Но с течением времени все чаще нам хотелось работать с этими служебными вещами через консоль, да еще и юниксовую. При этом не хотелось перекомпиливать все при малейших изменениях кода. По этой причине C#, Java и C++ были отметены. Оставался еще вариант с JavaScript и node.js, но меня достаточно давно манил Python и очень хотелось с ним познакомиться. А тут такой удачный случай. Поэтому решено было написать библиотеку именно на этом языке.

Так как протокол Photon не выложен в открытом доступе (ну или во всяком случае я его найти не смог), пришлось декомпилировать исходники клиентской библиотеки, написанной на Java, изучить их и переписать все это дело на Python. Java была выбрана потому, что этот язык мне ближе, так как последние несколько лет я занимался разработкой именно на этом языке. Как в последствии выяснилось после беседы с разработчиками Photon, лучше было взять библиотеку на C#, так как она более свежая.

Изначально я хотел написать все на Python версии 2.7 (на момент написания библиотеки актуальная версия была 2.7.9), но столкнувшись с трудностью в реализации некоторых вещей, которые уже были реализованы «из коробки» в 3.4, решил все же идти в ногу со временем.

С исходниками библиотеки можно ознакомиться на гитхабе.

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

Для начала реализуем интерфейс PeerListener:

class Connection:
    def __init__(self, connected=False):
        self.connected = connected

class SimpleListener(PeerListener):
    def __init__(self, connection):
        super().__init__()
        self.connection = connection

    def debug_return(self, debug_level, message):
        print("[{}] - {}".format(debug_level.name, message))

    def on_status_changed(self, status_code):
        print("[Status changed] - {}".format(status_code.name))
        if status_code is StatusCode.Connect:
            self.connection.connected = True

    def on_operation_response(self, op_response):
        print(op_response)

    def on_event(self, event_data):
        print(op_response)

Пока не будем никак обрабатывать OperationResponse и Event'ы. Будем просто выводить их в консоль. Так же тут используется маленький класс Connection, просто как holder для статуса соединения. Он нам пригодится чуть позже.

Теперь напишем сервисный поток, который будет дергать метод service у нашего пира примерно 10 раз в секунду (рекомендация разработчиков):

class ServiceThread(threading.Thread):
    def __init__(self, pp):
        threading.Thread.__init__(self)

        self.pp = pp
        self._run = False

    def run(self):
        self._run = True

        while self._run:
            self.pp.service()

            time.sleep(100.0 / 1000.0)

    def stop(self):
        self._run = False

Ну и конечно же main функция:

def main():
    connection = Connection()

    pp = PhotonPeer(enums.ConnectionProtocol.Tcp, SimpleListener(connection))
    pp.set_debug_level(DebugLevel.All)

    pp.connect(your_ip, your_port, your_app_name)

    service_thread = ServiceThread(pp)
    service_thread.start()

    while connection.connected is False:
        pass

    # Put your code here   

    service_thread.stop()
    service_thread.join()

    pp.disconnect()

Целиком исходник тут
import threading
import time

from photon import enums

from photon.enums import DebugLevel, StatusCode
from photon.listener import PeerListener
from photon.peer import PhotonPeer


class Connection:
    def __init__(self, connected=False):
        self.connected = connected


def main():
    connection = Connection()

    pp = PhotonPeer(enums.ConnectionProtocol.Tcp, SimpleListener(connection))
    pp.set_debug_level(DebugLevel.All)

    pp.connect(your_ip, your_port, your_app_name)

    service_thread = ServiceThread(pp)
    service_thread.start()

    while connection.connected is False:
        pass

    # Put your code here   

    service_thread.stop()
    service_thread.join()

    pp.disconnect()


class ServiceThread(threading.Thread):
    def __init__(self, pp):
        threading.Thread.__init__(self)

        self.pp = pp
        self._run = False

    def run(self):
        self._run = True

        while self._run:
            self.pp.service()

            time.sleep(100.0 / 1000.0)

    def stop(self):
        self._run = False


class SimpleListener(PeerListener):
    def __init__(self, connection):
        super().__init__()
        self.connection = connection

    def debug_return(self, debug_level, message):
        print("[{}] - {}".format(debug_level.name, message))

    def on_status_changed(self, status_code):
        print("[Status changed] - {}".format(status_code.name))
        if status_code is StatusCode.Connect:
            self.connection.connected = True

    def on_operation_response(self, op_response):
        print(op_response)

    def on_event(self, event_data):
        print(op_response)


if __name__ == "__main__":
    main()


Отдельно хотелось бы остановиться на сериализации целочисленных типов. Для тех, кто успел заглянуть в исходники, подобный код может показаться немного странным:

def _serialize_byte(out, value, set_type):
    if set_type:
        out.extend(bytearray([98]))

    value = ((value + ((1 << 7) - 1)) % (1 << 8)) - ((1 << 7) - 1)
    out.extend(struct.pack('>b', value))

Это пример сериализации байта, для остальных типов все аналогично. Ответ прост — подобный код здесь для совместимости с результатом сериализации в Java.

Ну и напоследок о текущих ограничениях:
  • Поддерживает только TCP протокол
  • Не поддерживает encrypted соединение
  • Не поддерживает сбор статистики траффика

Добавить UDP протокол и справить 2 последних пункта было бы замечательно, но пока нет на это времени. Но исходники открыты, как говорится — все карты в руки. Хочется надеяться, что мы не единственные, кому это понадобилось и наш труд окажется кому-нибудь полезен.

P.S. Если кто-то найдет ошибки/неточности в коде и/или не оптимально написанные участки (чего я не исключаю ввиду не очень длительного знакомства с Python) — буду безмерно благодарен за указание на них.

Комментарии (2)


  1. tisov
    31.03.2015 11:50

    Интересно, но маловато


    1. mlogarithm Автор
      31.03.2015 11:59

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

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