В первой части нашей трилогии мы рассказали вам о протоколе 9P/Styx и его устройстве, а также про нашу библиотеку styx2000. Сейчас же мы хотели бы вам показать пример работы с библиотекой на примере пустой папки, которую мы будем раздавать с помощь протоколу 9P/Styx.
Раздача пустой папки ? В чем смысл этого, казалось бы, бессмысленного действия ? Как ни странно, раздача пустой папки поможет показать минимальный набор операций, которые требуется реализовать для работы простого 9P/Styx-сервера, а также позволит пролить свет на процесс взаимодействия клиента и сервера.
Для начала создадим проект dub и добавим в зависимости библиотеку styx2000, в версии main, а не последней стабильной ветки:
dub init empty_folder
После этого, для работы всей сетевой части проекта добавим несколько модифицированную версию сервера, который использовался для создания Gopher-сервера. В данном случае модификация потребуется для того, чтобы сервер не сбрасывал подключение после запросов клиента, а держал бы его открытым. Для этой цели добавляем специальный флаг immediate, который позволит менять поведение сервера в части «удержания» клиента:
module empty_folder.server; private { import std.algorithm : remove; import std.socket; } class GenericSimpleServer(uint BUFFER_SIZE, uint MAXIMAL_NUMBER_OF_CONNECTIONS) { protected { ubyte[BUFFER_SIZE] _buffer; string _address; ushort _port; bool _immediate; Socket _listener; Socket[] _readable; SocketSet _sockets; } abstract ubyte[] handle(ubyte[] request); final void run() { while (true) { serve; scope(failure) { _sockets = null; _listener.close; } } } final void setup4(string address, ushort port, int backlog = 10, bool immediate = false) { _address = address; _port = port; _listener = new Socket(AddressFamily.INET, SocketType.STREAM); _immediate = immediate; with (_listener) { bind(new InternetAddress(_address, _port)); listen(backlog); } _sockets = new SocketSet(MAXIMAL_NUMBER_OF_CONNECTIONS + 1); } final void setup6(string address, ushort port, int backlog = 10, bool immediate = false) { _address = address; _port = port; _listener = new Socket(AddressFamily.INET6, SocketType.STREAM); _immediate = immediate; with (_listener) { bind(new Internet6Address(_address, _port)); listen(backlog); } _sockets = new SocketSet(MAXIMAL_NUMBER_OF_CONNECTIONS + 1); } private { final void serve() { _sockets.add(_listener); foreach (socket; _readable) { _sockets.add(socket); } Socket.select(_sockets, null, null); for (uint i = 0; i < _readable.length; i++) { if (_sockets.isSet(_readable[i])) { auto realBufferSize = _readable[i].receive(_buffer); if (realBufferSize != 0) { auto data = _buffer[0..realBufferSize]; _readable[i].send( handle(data) ); } if (_immediate) { _readable[i].close; _readable = _readable.remove(i); i--; } } } if (_sockets.isSet(_listener)) { Socket currentSocket = null; scope (failure) { if (currentSocket) { currentSocket.close; } } currentSocket = _listener.accept; if (_readable.length < MAXIMAL_NUMBER_OF_CONNECTIONS) { _readable ~= currentSocket; } else { currentSocket.close; } } _sockets.reset; } } }
Экспериментальный 9P/Styx-сервер, который мы будем реализовать, будет производным от класса GenericSimpleServer и потому, единственное, что нужно сделать в нашем случае — это перегрузить метод handle у класса-наследника. В случае любого 9P/Styx-сервера можно создать перегрузку метода handle, который принимая запрос от клиента в виде потока байтов, обрабатывает его и возвращает ответ, также в виде потока байтов. Это выглядит так:
override ubyte[] handle(ubyte[] request) { import std.stdio : writeln; auto r = decode(request); auto q = process(r); writeln(`-> `, r.toPlan9Message); writeln(`<- `, q.toPlan9Message); return encode(q); }
По сути, схема работы достаточно общая и разные реализации будут отличаться только функционалом, который заключен в метод process. Этот метод получает на вход аргумент типа StyxMessage (определен в модуле styx2000.extrautil.styxmessage и представляет собой массив из объектов протокола, которые описывают структуру некоторого сообщения) и выдает результат того же типа. Таким образом, в process осуществляется вся обработка поступающих сообщений 9P/Styx, а методы decode/encode (находятся в модуле styx2000.protomsg) служат для декодирования потока байтов в сообщения 9P/Styx и наоборот. Метод toPlan9Message позволяет отображать сообщения в текстовом виде, точно таком же, в котором выдают отладочную информацию утилиты из комплекта Plan9Port или из самой операционной системы Plan 9. Данный метод не является обязательным и служит для целей наблюдения за работой сервера и находится в модуле styx2000.extrautil.mischelpers.
Осталось разобраться в логике работы метода process, который в данном случае выглядит так:
StyxMessage process(StyxMessage q) { StyxMessage r; STYX_MESSAGE_TYPE type = q[1].toType.getType; ushort tag = q[2].toTag.getTag; switch (type) { case STYX_MESSAGE_TYPE.T_VERSION: r = createRmsgVersion(tag); break; case STYX_MESSAGE_TYPE.T_ATTACH: r = createRmsgAttach(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir)); break; case STYX_MESSAGE_TYPE.T_WALK: string[] name = q[5].toNwname.getName; r = (name != []) ? createRmsgError(tag, `File does not exists`) : createRmsgWalk(tag); break; case STYX_MESSAGE_TYPE.T_STAT: r = makeStat(tag); break; case STYX_MESSAGE_TYPE.T_OPEN: r = createRmsgOpen(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir)); break; case STYX_MESSAGE_TYPE.T_READ: r = createRmsgRead(tag); break; case STYX_MESSAGE_TYPE.T_CLUNK: r = createRmsgClunk(tag); break; default: break; } return r; }
В начале самого метода, который принимает в качестве аргумента объект типа StyxMessage, создается также объект типа StyxMessage, в который будет помещаться сформированный ответ. Далее из переданного объекта (будем далее называть его запросом) извлекается тип сообщения протокола 9P/Styx: сначала выделяется 2 элемент массива (напоминаем, что в начале каждого сообщения протокола есть три обязательных поля — размер, тип сообщения и его тег), затем выделенный объект типа StyxObject (это общий тип для всех объектов протокола в библиотеке styx2000 и находиться он в модуле styx2000.protobj.styxobject) приводится с помощью функции-хэлпера toType (таких хэлперов много и каждый под свой тип объекта, а находятся все они в styx2000.extrautil.casts) к типу Type (находится в модуле styx2000.protobj.type) и далее из полученного результата, получается объект STYX_MESSAGE_TYPE (отвечает за тип сообщения протокола). Данный объект с типом сообщения будет использоваться для обработки всего сообщения, переданного в метод process, а также для идентификации возможных типов сообщений и формировании ответа на каждый конкретный тип сообщения.
Следующим шагом, в методе извлекается тег, точно таким же способом, как ранее извлекался тип сообщения. Далее тип и тег нам нужны будут для правильного формирования ответных сообщений от сервера к клиенту…
В блоке switch мы реализуем разбор минимально необходимого количества сообщений разных типов:
- T_VERSION — сообщение с описанием версии протокола, которое также служит способом согласования связи клиента и сервера. В данном случае мы используем одну из специальных функций-хэлперов из модуля styx2000.extrautil.styxmessage под названием createRmsgVersion, которая формирует корректное сообщение с упомянутым выше типом и генерирует объект типа StyxMessage. По умолчанию у данной функции заданы определенные аргументы и их три: тег, максимальный размер сообщения и версия протокола, но в данном случае, требуется задать только тег, поскольку именно тег идентифицирует конкретное сообщение, а остальные значения стандартные константы (размер — 8192, а версия протокола — константа STYX_VERSION, которая берется из модуля styx2000.protoconst и которая равна строковому значению «9P2000»). В следующих этапах разбора мы также будем применять функции из модуля styx2000.extrautil.styxmessage.
- T_ATTACH — сообщение, которое позволяет серверу понять с каким файловым деревом будет работать клиент, иными словами, это сообщение позволяет прикрепить клиента к нужной файловой иерархии. Данное сообщение формируется с помощью функции-хэлпера createRmsgAttach, которая принимает в качестве аргументов тег, тип идентификатора qid файлового дерева (файл, папка или что-то иное, описано в модуле styx2000.protoconst.qids), версию и путь (точно такие же аргументы нужны для формирования qid). Поскольку, мы раздаем пустую папку, то в функцию-хэлпер мы передаем тег сообщения, тип идентификатора — STYX_QID_TYPE.QTDIR (так как это папка, а не файл), версия — 0 (так как версия говорит о модификации папки или файла), а качестве пути используется 8-байтный хэш в виде единого числа от имени пустой папки (имя папки находится в переменной _dir, об этом мы расскажем чуть позже). Сама функция хэширования hash8 взята из модуля styx2000.extrautil.siphash.
- T_WALK — сообщение, которое позволяет перейти в нужную папку или осуществить продвижение в файловой иерархии (или найти некоторый файл/папку). Для разбора входного сообщения требуется извлечь путь, по которому осуществляется продвижение, а это 6 по счету аргумент во входном сообщении, и извлечение этих данных идет по такой же схеме, как и извлечение типа сообщения или тега. Далее после извлечения пути, проверяется равен ли путь пустому массиву (поскольку наша папка пуста и в ней нет элементов) и если это не так, то формируется сообщение с типом R_ERROR с помощью функции-хэлпера createRmsgError; если же путь и правда оказывается пустым, то формируется сообщение T_WALK с помощью createRmsgWalk. Эта функция принимает в качестве аргумента тег и список qid’ов для каждого элемента пути, но поскольку элементы пути пусты, то передается только тег.
- T_STAT — сообщение, которое позволяет получить информацию о заданном элементе файлового дерева и которое содержит все метаданные по некоторому пути: имя папки или файла, права доступа, размер, время последнего изменения и время последнего доступа, а также наименование группы владельца и т.д. Это сообщение самое сложное для формирования и сам объект протокола представляющий статистику (будем называть так всю информацию об объекте файловой иерархии) с типом Stat содержит множество разных полей, поэтому формированием данного объекта и размещением его в сообщении 9P/Styx занимается функция makeStat. В функции makeStat сначала формируется пустой объект типа StyxMessage, в который затем помещается сформированный объект типа Size (размер сообщения, пока что нулевой), затем помещается объект типа Type (с типом R_STAT) и соответствующим тегом. После того, как начальный заголовок сообщения сформирован, формируется идентификатор файлового дерева (qid), а затем создается объект типа Perm, который описывает права доступа и устанавливает их методом setPerm (из модулей styx2000.protobj.permи styx2000.protoconst.permissions). Модуль styx2000.protoconst.permissions содержит константы, которые описывают права на чтение (READ), запись (WRITE) и исполнение (EXEC) для разных групп владельцев: владельца (OWNER), его группы (GROUP) и остальных (OTHERS). Данные константы описывают точно такую же систему прав доступа, которая принята в Unix-системах, и эти константы точно также складываются (с помощью побитового или |), что и применяется в формировании объекта Perm. Далее формируется объект Stat, в конструктор которого передаются тип и устройство (связано с особенностями архитектуры Plan 9, но экспериментальным способом мы нашли приемлемые значения: тип — 77, а устройство — 4), qid, права доступа, время доступа и время модификации (берутся из внутренней переменной класса _time), размер в байтах (для папок он по соглашению равен 0), наименование файла/папки (берется из внутренней переменной класса _dir), наименование пользователя-владельца (переменная _uid), наименование группы владельца (переменная _gid) и наименование группы остальных пользователей (пустая строка, поскольку для обработки это не имеет значения). Когда формирование объекта Stat закончится, он размещается в сообщении 9P/Styx с типом R_STAT.
- T_OPEN — сообщение, которое позволяет открыть некоторый путь для последующих операций. Данный вид сообщений формируется на основании тега из запроса, типа файлового идентификатора qid, версии и пути, аналогично тому, как это было в сообщении T_ATTACH, но с помощью иной функции-хэлпера createRmsgOpen, который имеет почти такую же сигнатуру как и createRmsgAttach.
- T_READ — сообщение, которое позволяет открыть и считать данные из некоторого файла, или получить содержимое некой папки. В нашем случае, данное сообщение нужно для получения сведений о содержимом папки, но поскольку содержимого нет, то функция-хэлпер createRmsgRead, которая формирует корректное сообщение, принимает лишь один аргумент — тег.
- T_CLUNK — сообщение, которое позволяет объявить некоторый fid (это файловый идентификатор на стороне клиента), что равносильно прекращению дальнейшей работы с файлом/папкой. Именно таким сообщением заканчивается вся работа в сессии (т.н транзакции), и именно это сообщение свидетельствует о конце обмена данными между сервером и клиентом, и единственное что требуется для этого сообщения — это тег. Функция createRmsgClunk, которая формирует сообщение принимает только один аргумент, и других аргументов у нее нет.
Код всего сервера с учетом вышеупомянутой механики выглядит следующим образом (сам код находится в файле emptydir.d):
module empty_folder.emtydir; private { import std.datetime.systime : Clock; import styx2000.extrautil.casts : toNwname, toType, toTag; import styx2000.extrautil.siphash : hash8; import styx2000.extrautil.styxmessage; import styx2000.extrautil.mischelpers : toPlan9Message; import styx2000.protoconst.messages; import styx2000.protoconst.qids; import styx2000.protomsg : decode, encode; import styx2000.protobj; import empty_folder.server; } class EmptyDir : GenericSimpleServer!(8_192, 60) { private { string _dir; string _uid; string _gid; uint _time; StyxMessage makeStat(ushort tag) { StyxMessage s; s ~= new Size; s ~= new Type(STYX_MESSAGE_TYPE.R_STAT); s ~= new Tag(tag); // server identifier for directory Qid qid = new Qid( STYX_QID_TYPE.QTDIR, 0, hash8(_dir) ); // chmod 755 for dir Perm perm = new Perm; perm.setPerm( STYX_FILE_PERMISSION.DMDIR | STYX_FILE_PERMISSION.OWNER_EXEC | STYX_FILE_PERMISSION.OWNER_READ | STYX_FILE_PERMISSION.OWNER_WRITE | STYX_FILE_PERMISSION.GROUP_READ | STYX_FILE_PERMISSION.GROUP_WRITE | STYX_FILE_PERMISSION.GROUP_EXEC | STYX_FILE_PERMISSION.OTHER_READ | STYX_FILE_PERMISSION.OTHER_EXEC ); Stat stat = new Stat( // type and dev for kernel use (taken from some experiments with styxdecoder, see above) 77, 4, qid, // permissions perm, // access time _time, // modification time _time, // conventional length for all directories is 0 0, // file name (this, directory name) _dir, // user name (owner of file) _uid, // user group name _gid, // others group name "" ); s ~= stat; return s; } StyxMessage process(StyxMessage q) { StyxMessage r; STYX_MESSAGE_TYPE type = q[1].toType.getType; ushort tag = q[2].toTag.getTag; switch (type) { case STYX_MESSAGE_TYPE.T_VERSION: r = createRmsgVersion(tag); break; case STYX_MESSAGE_TYPE.T_ATTACH: r = createRmsgAttach(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir)); break; case STYX_MESSAGE_TYPE.T_WALK: string[] name = q[5].toNwname.getName; r = (name != []) ? createRmsgError(tag, `File does not exists`) : createRmsgWalk(tag); break; case STYX_MESSAGE_TYPE.T_STAT: r = makeStat(tag); break; case STYX_MESSAGE_TYPE.T_OPEN: r = createRmsgOpen(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir)); break; case STYX_MESSAGE_TYPE.T_READ: r = createRmsgRead(tag); break; case STYX_MESSAGE_TYPE.T_CLUNK: r = createRmsgClunk(tag); break; default: break; } return r; } } this(string dir, string uid, string gid) { _uid = uid; _gid = gid; _dir = dir; _time = cast(uint) Clock.currTime.toUnixTime; } override ubyte[] handle(ubyte[] request) { import std.stdio : writeln; auto r = decode(request); auto q = process(r); writeln(`-> `, r.toPlan9Message); writeln(`<- `, q.toPlan9Message); return encode(q); } }
Использовать данный сервер просто (файл app.d): достаточно сформировать объект класса EmptyDir, передать в него имя папки, имя пользователя-владельца и наименования группы владельца (т.е это те самые внутренние переменные _dir, _uid, _gid) и установить сетевой адрес/порт для сервера:
private { import empty_folder.emtydir; } void main() { EmptyDir d = new EmptyDir(`test`, `lhs`, `lhs-user`); d.setup4("127.0.0.1", 4444); d.run; }
Испытать сервер можно следующим образом, используя утилиту 9p и удаленный компьютер, на котором будет запущен сервер, раздающий пустую папку:
9p -D -n -a 'tcp!127.0.0.1!4444' ls 9p -D -n -a 'tcp!127.0.0.1!4444' ls -lhd
Примечание автора. Команда 9p — это тривиальный клиент протокола 9p/Styx из комплекта утилит plan9port, который очень удобен для применения в скриптах. Опция -D программы говорит о том, что требуется отладочный вывод сообщений протокола, опция -n — говорит о том, что будет использоваться подключение без аутентификации, опция -a — используется для указания того, что будет использован сетевой адрес, а не локальный файл или сокет.
Все это выглядит следующим образом (компьютер с клиентом 9p):

С точки зрения сервера, точно такая же сессия, выглядит следующим образом:

На этом раздача пустой папки закончилась, и тот же самый подход можно применить и для раздачи файлов по протоколу 9P/Styx, или же для создания полноценных файловых серверов. Мы считаем, что приведенный нами небольшой обзор библиотеки styx2000 и ее модулей, поможет вам в дальнейшем в разработке ваших собственных проектов.
P.S: Все рассмотренные функции-хэлперы в статье имеют довольно простую базу в наименовании и имеют вот такую схему построения названий: create<тип сообщения — T или R>msg<наименование типа сообщения>. Данные функции покрывают все типы сообщений, кроме R_STAT и T_WSTAT, которые имеют сложную структуру и должны обрабатываться вручную (т.к в зависимости от ситуации, эти сообщения надо заполнять по разному). Кроме того, в библиотеке можно найти и иного рода функционал (см. модуль styx2000.extrautil), который может быть использован в ваших проектах.