← На главную

Зачем нужен Thrift и основы работы с ним на Scala

Thrift – это такая штука для сериализации данных. Вы описываете схему данных в специальном формате. Из этого описания генерируются классы. Эти классы легко сериализуются и десериализуются. При этом схему можно изменять (например, добавлять-удалять поля в классах) так, что данные, сериализованные по старой схеме, будут успешно десериализованы по новой. Одну и ту же схему можно использовать в проектах на разных языках, и они будут успешно друг с другом взаимодействовать. Плюс к этому еще накручена возможность объявлять исключения и генерировать код для RPC. В этой заметке мы разберемся, как работать с Thrift на языке Scala.

Во-первых, нам понадобятся следующие зависимости:

"org.apache.thrift" % "libthrift" % "0.9.2", "com.twitter" %% "scrooge-core" % "3.20.0",

Во-вторых, в project/plugins.sbt дописываем:

addSbtPlugin("com.twitter" %% "scrooge-sbt-plugin" % "3.16.3")

… а в build.sbt:

com.twitter.scrooge.ScroogeSBT.newSettings

Таким образом мы подключили к проекту Scrooge-плагин и можем генерировать Scala-классы из thrift-файлов командой:

sbt scrooge-gen

Более того, генерация будет происходить автоматически при сборке проекта, в том числе при выполнении команды sbt assembly.

Далее создаем src/thrift/game.thrift следующего содержания:

// include "ololo.thrift" namespace java me.eax.examples.thrift.game // exception Ololo { ... } // service Ololo { ... } enum Weapon { Sword = 1 Bow = 2 } struct WarriorInfo { 1: optional Weapon weapon 2: required i64 arrowsNumber } enum Spell { Fireball = 1 Thunderbolt = 2 } struct MageInfo { 1: required set<Spell> spellbook 2: required i64 mana } union ClassSpecificInfo { 1: WarriorInfo warrior 2: MageInfo mage } struct Hero { 1: required string name 2: required i64 hp 3: required i64 xp 4: ClassSpecificInfo classSpecificInfo }

Мне нужно было придумать пример, демонстрирующий использование required и optional полей, enum’ов, контейнеров типа set и map, а также, как при помощи Thrift получить алгебраические типы. В итоге придумались такие вот классы для RPG-игры. Мне кажется, тут все довольно очевидно, так что двигаемся дальше.

Примеры использования сгенерированных классов:

val spellbook = Set(Spell.Thunderbolt, Spell.Fireball) val mage = Hero( name = "afiskon", hp = 25L, xp = 1024L, ClassSpecificInfo.Mage(MageInfo(spellbook, mana = 100L)) ) val warrior = Hero( name = "eax", hp = 50L, xp = 256L, ClassSpecificInfo.Warrior(WarriorInfo(Some(Weapon.Sword), 0L)) )

Для тестирования сериализации и десериализации мной были написаны тесты с использованием ScalaCheck. Вот один из них:

forAll { (data1: Hero) => val bytes = { val out = new ByteArrayOutputStream() data1.write(new TBinaryProtocol(new TIOStreamTransport(out))) out.toByteArray } val data2 = { val stream = new ByteArrayInputStream(bytes) Hero.decode(new TBinaryProtocol(new TIOStreamTransport(stream))) } data1 shouldBe data2 }

Заметьте, что ScalaCheck откуда-то знает, как генерировать случайных героев. Это потому что для класса Hero мной специально был написан генератор. Генераторы в ScalaCheck пишутся очень просто. Выглядит это примерно так:

implicit lazy val arbHero: Arbitrary[Hero] = Arbitrary( for { name <- Arbitrary.arbitrary[String] hp <- Gen.posNum[Long] xp <- Gen.posNum[Long] classSpecificInfo <- Arbitrary.arbitrary[ClassSpecificInfo] } yield Hero(name, hp, xp, classSpecificInfo) )

Генераторы для ClassSpecificInfo.Mage, ClassSpecificInfo.Warrior и, так сказать, «корневого» ClassSpecificInfo выглядят аналогично.

Обратите внимание, что выше приводится тест для TBinaryProtocol. Всего Thrift поддерживает четыре так называемых протокола сериализации:

  1. TBinaryProtocol – обычный бинарный протокол;
  2. TCompactProtocol – компактный бинарный протокол;
  3. TJSONProtocol – очень сложно читаемый JSON;
  4. TSimpleJSONProtocol – легко читаемый JSON, но без десериализации;

Использование этих протоколов выглядит аналогично. Например:

def heroToJson(hero: Hero): String = { val out = new ByteArrayOutputStream() hero.write(new TSimpleJSONProtocol(new TIOStreamTransport(out))) new String( ByteBuffer.wrap(out.toByteArray).array(), StandardCharsets.UTF_8 ) }

Еще одна интересная фишка Thrift – возможность вручную закодировать список, map или set объектов:

forAll { (data1: List[Hero]) => val bytes = { val out = new ByteArrayOutputStream() val proto = new TBinaryProtocol(new TIOStreamTransport(out)) proto.writeListBegin(new TList(TType.STRUCT, data1.size)) data1.foreach(_.write(proto)) proto.writeListEnd() out.toByteArray } val data2 = { val stream = new ByteArrayInputStream(bytes) val proto = new TBinaryProtocol(new TIOStreamTransport(stream)) val listInfo = proto.readListBegin() val res = { for(_ <- 1 to listInfo.size) yield Hero.decode(proto) }.toList proto.readListEnd() res } data1 shouldBe data2 }

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

А что вы используете для сериализации и десериализации?

Дополнение: Заметки Сериализация и десериализация в/из Protobuf на C++ и Сериализация в языке Go на примере библиотеки codec также могут быть вам интересны.