[ AmberSkyNet VR ]

Сейчас популярны всякие MMORPG и SecondLif'ы... И мы сделаем в нашем универсальном движке возможность работы с сетью. Так же учтём возможность подключения различных сетевых протоколов через плагины. Но менеджер сервера будет один. Это упростит возможность контроля сетевых соединений (например, можно сделать единый блэк-лист на все сетевые соединения). А потом реализуем в качестве плагина к нему возможность обработки IRC на клиенте...

INetSupervisor

Так.. Подумаем какие функции нам потребуются от сетевого менеджера.. Протоколы поверх TCP/IP могут быть разные, но сущность в общем то одна и та же - открыть/закрыть сокет, принять/отправить байты. Поэтому функции работы с сокетами можно реализовать в одном месте - в сетевом менеджере, а обработку сетевых протоколов, которые используют TCP/IP сделать через обработчики-плагины к нему...

class INetSupervisor {
public:

virtual ~INetSupervisor(){}

// функции обработчиков
virtual INetProtocolHandler *OpenServerListener(std::string _HandlerName,
			 std::string _Protocol, int port, std::string Init_string)=0;
virtual INetProtocolHandler *OpenClientListener(std::string _HandlerName,
			 std::string _Protocol, std::string Server_Name, int port,
			 std::string UserName, std::string Init_string)=0;
virtual INetProtocolHandler *FindHandler(std::string _HandlerName)=0;
virtual void CloseListener(std::string _HandlerName)=0;
virtual bool AddCustomListener(INetProtocolHandler* handler, std::string Server_Name, int port)=0;
// функция проверки всех сокетов на готовность
virtual void Check()=0;

// интерфейс для INetProtocolHandler'ов

// открываем "приёмный" сокет для серверных сокетов
// получаем на выходе true - не удалось принять
virtual bool AcceptSocket(unsigned int Open_Socket_Id,unsigned int handler_id)=0;
virtual int SendMsg(unsigned int Open_Socket_Id, char *buffer, int buf_size)=0;
virtual void CloseListener(int num)=0; //сокет знает свой num и может закрыть себя
};

Пройдёмся по функциям...

~INetSupervisor() - виртуальный деструктор. Пригодится для корректного удаления наследуемых от интерфейсного класса сетевых менеджеров...

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

OpenClientListener - открываем "слушатель"-клиент. На вход подаём имя, имя протокола, имя сервера в сети (можно и IP), порт к которому будет произведено подключение, имя пользователя и строку инициализации... Может быть впоследствии имя пользователя уберём... Т.к. не всегда оно нужно, например - при закачки файла через HTTP с открытого ресурса...

FindHandler - функция поиска обработчика по имени. На выходе получаем указатель на обработчик если он у нас уже был открыт при помощи функций OpenServerListener или OpenClientListener.

AddCustomListener - функция предназначена для добавления "пользовательского слушателя", какого-то специфического протокола.. Либо какой-нибудь сетевой обработчик проткола может вызвать эту функцию чтобы открыть еще один клиентский сокет для себя (мне пригодилось для поддержки перекачки файлов по IRC - на клиент по уже открытому сокету приходит информация к какому IP и порту обращаться чтобы скачать файл. Формат обмена сообщений для скачки файла достаточно специфический и нужды описывать его отдельным плагином нет ).

Check - функция проверки сокетов на готовность к передаче и приёму. Если из сокета что-то можно прочитать - менеджер читает эту информацию и вызывает сетевой обработчик чтобы он эту информацию интерпретировал..

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

SendMsg - при помощи этой функции сетевые обработчики записывают некоторые данные в сокет, т.е. передают информацию по сети внешним программам.

CloseListener - функция закрытия "слушателя". Например, если у нас обработчик следил за тем, как качается файл, то после скачки он больше не нужен и может отправиться на покой, вызвав функцию CloseListener... :)

INetProtocolHandler

Теперь опишем интерфейсный класс обработчика. Сетевые обработчики будут являться связью между прикладной программой и сетевым менеджером. Наследуем его от IBaseObject, т.к. нам очень пригодятся "универсальные" функции SetParam, GetParam. Его интерфейс будет попроще, чем интерфейс сетевого менеджера...

class INetProtocolHandler: public IBaseObject {
public:

virtual void Init( std::string Init_String )=0;

//эту функцию вызывает менеджер если сокет готов к отправке/приёму данных
virtual unsigned int HandleMessage(unsigned int handler_id, char *buffer, long int buf_size)=0;

//выполнить обработчику команду
// true - не удалось отправить или команда не поддерживается
virtual bool Send(std::string To, std::string Operation, std::string Value)=0;

//ID данного обработчика в менеджере сети
unsigned int SupervisorId;
};

//определим тип callback-функции для обработчика.
typedef void (*CALLBACK_PROTOCOL_HANDLER)( INetProtocolHandler* , void* );

Init - функция инициализации обработчика. Вызывается из менеджера сети при создании либо там, где создаётся пользовательский обработчик, наследуюемый от INetProtocolHandler. На вход подаётся строка инициализации... например вида "Параметр=Значение;Параметр=Значение;...Параметр=Значение".

HandleMessage - данная функция вызывается из менеджера сети, если сокет, который "слушает" данный обработчик готов передать какие-то данные или принять их. На вход подаётся handler_id, указатель на буфер с данными и его длина. На выходе - получаем новое значение handler_id, который менеджер сохраняет до следующего вызова обработчика.
handler_id - это некое число, которое показывает в каком состоянии находится сокет. Например handler_id может показывать стадию соединения - пока нет связи с сервером (handler_id=0), связь установлена (handler_id=1), авторизация пройдена (handler_id=2).
В менеджере сети для каждого сокета запоминается свой handler_id, поэтому один обработчик может "слушать" несколько сокетов, обрабатывая каждый из них в зависимости от его handler_id. Например - обработать полученную из сокета информацию как текстовое сообщение (handler_id=0), или воспринять как сокет загрузки ресурса (ну, скажем от 100 до 120).. Причём имена загружаемых ресурсов, процент скачки и другая информация могут хранится в списке внутри обработчика, в этом случае handler_id будет представлять собой номер записи в списке.
Значения handler_id могут быть разными для разных обработчиков и используются исключительно внутри них.

Send - функция выполнения команды обработчиком. На вход подаётся кому обработчик должен передать команду (To), какая это команда (Operation) и дополнительные параметры (Value). Какие именно команды и кому можно передавать - различаются в зависимости от типа обработчика.

Параметр SupervisorId хранится обработчиком чтобы использовать его в вызовах функций менеджера сети. Таким образом, параметр передающийся в функции указывает менеджеру сети от какого именно обработчика пришел вызов функции. Скажем, если обработчик попросит себя удалить (функция INetSupervisor::CloseListener) - то менеджер сети удалит его и уберет указатель на уже несуществующий обработчик из списка своих указателей на экземпляры созданных им обработчиков протокола.

Еще определим формат вызова так называемой "калбэк-функции" (т.е. функции обратного вызова) обработчика. Для чего это надо? Сейчас поясню.. Обработчики могут быть вполне автономные (скажем, обработчик, реализующий файл-сервер), но, например, если мы хотим реализовать чат, нам потребуется обмен данными обработчика и внешней программы - именно во внешнюю программу мы будем передавать полученные по сети фразы от других людей и из внешней же программы мы мы будем получать фразы, которые нам надо отправить другим людям. Поэтому неплохо было бы сделать так, чтобы при получении из какой-либо фразы обработчик вызывал функцию во внешней программы, в которую бы передавал полученную фразу и указатель на себя (для возможности вызова функций именно этого обработчика). Указатель на калбэк-функции (а их может быть и несколько, в зависимости от типа обработчика и того, что он принял из сокета) устанавливается через универсальную функцию IBaseObject::SetPtrParam("имя калбэк-функции", "указатель на функцию" );

В случае вызова калбэк-функции обработчик передаст в нее указатель на себя и указатель на некую структуру, формат которой будет зависеть от типа обработчика.

Реализация менеджера сети CNetSupervisor

Реализацию менеджера сети я сделаю через SDL_net - кросс-платформенную обёртку для работы с сетью. Менеджер будет хранить внутри себя список открытых сокетов из элементов такой структуры:

struct COpenSocket{
bool ServerSocket;
TCPsocket socket;
INetProtocolHandler *handler;
unsigned int handler_id;
};
Где ServerSocket - указатель на то, что сокет был создан как сокет сервера; socket - переменная, в которой хранится номер сокета для использования в SDL_net; handler - указатель на "слушателя", т.е. на обработчика данного сокета; handler_id был описан выше...

При вызове функции OpenServerListener менеджер сети прибавляет к имени протокола строчку "_Server" и просит Engine создать обработчик сетевого протокола с таким типом ( ENGINE->CreateObject() ). Engine при помощи системы плагинов либо создаёт экземпляр класса такого типа и возвращет указатель на него, либо не создаёт и возвращает NULL.
Если такой объект создан, то менеджер сети при помощи библиотеки SDL_net открывает серверный сокет, вызывает у вновь созданного обработчика сетевого протокола функцию Init, передавая ему строку инициализации и запоминает указатель на него а так же сокет в списке открытых сокетов

При вызове функции OpenClientListener происходит аналогичный процесс, только к имени проткола прибавляется строчка "_Client" и открывается сокет, при помощи которого будет происходить соединение с удалённым сервером...

При выполнении функции AddCustomListener нет обращения к системе плагинов, т.к. указатель на уже созданный обработчик передаётся во входном параметре функции. Если ServerName равно "-" - значит открываем серверный сокет, если какое-то другое значение - то менеджер сети создаёт сокет для связи с машиной в сети с именем или IP равным ServerName.

При вызове функции Check() мы определяем число готовых к передаче/приёму сокетов и проверяем поочерёдно каждый сокет в списке, вызывая функцию HandleMessage у "слушателей" активных сокетов...

Реализация клиента IRC в виде плагина к менеджеру сети

Протокол IRC описан в rfc1459, rfc2812... поддержку CTCP-команд и в частности работу с DCC ( Direct client-to-client, сообщения клиент-клиент, без участия сервера ) пришлось искать в других местах. Реализация в AmberSkyNet не совсем полная, но в общем вроде кое-что работает..

В одном хидере nh_irc.h описываются несколько обработчиков сетевых протоколов. NH_IRC - "главный" обработчик, который собственно отправляет и получает сообщения с IRC-сервера. Он же создаёт обработчики NH_IRC_DCC_CHAT, NH_IRC_DCC_GETFILE и NH_IRC_DCC_SENDFILE если получает DCC-запрос с сервера. CTCP-запросы обрабатывает сам.
Конечно можно было бы обрабатывать сообщения в зависимости от handler_id одним менеджером, как описано вверху, но все-таки писать отдельный класс под конкретный формат протокола полегче...

Теперь о том, каким образом связываются между собой наше приложение и IRC-обработчики. Можно например было написать обработчик, "знающий" про менеджер мира и при считывании сообщения с IRC-сервера как-нибудь взаимодейтствовал бы с ним. Например - добавлял/удалял аватаров, писал бы собщения над головами пользователей..
Но делать такое на уровне обработчика сетевого проткола не совсем удобно - мы теряем в гибкости нашего приложения. Поэтому оставим обработчику только работу с сетевыми сообщениями, а конкретную реализацию отображения сетевых сообщений в наш трёхмерный мир оставим нашей прикладной программе. Оставляя неизменным сетевой обработчик IRC-протокола мы можем создать в виде прикладной программы и 2d-чат, и 3d, и даже текстовый :) При этом перекомпилировать сетевой плагин нам не придётся.

Обработчику сетевого протокола можно установить в прикладной программе указатель на функцию, которую он будет вызывать если произошло событие, которое может заинтересовать программу. Например - пришло сообщение, появился пользователь, пользователь ушел, итп.. Это делается при помощи вызова в прикладной программе функции SetPrtParam с именем Hanlder, вот так:

void myCustomHandler(INetProtocolHandler *handler, void *info){
...
}
...
...
myHanler->SetPtrParam("Handler", myCustomHandler);

Т.к. наша функция myCustomHandler может обрабатывать вызовы несольких сетевых обработчиков, то одним из её входных параметров является указатель на INetProtocolHandler. Это сделано для того, чтобы функция могла в зависимости от входных событий реагировать на них, вызывая в обработчике сетевого протокола функцию Send. Например:

// кто-то ушёл из линии
if ( Action == "PART" ) {
    std::string mess="Славный рыцарь "+Nick+" покинул нас... :(";
    handler->Send("PRIVMSG", (*_it), mess.c_str() );
    };

Второй параметр функции myCustomHandler представляет собой ссылку на некую структуру, в которой хранятся дополнительные параметры вызова функции. Например - имя линии, ник сказавшего человека, сообщение, итп.. Я не уверен корректно ли выдавать из DLL указатель на std::map, но это вроде работает. Если некорректно - что ж, переделаем на использование ITextListMap.

ITextListMap и его реализация CTextlistMap

ITextListMap - это класс собрания "текстовых карт", т.е. текстовых списков. Данный класс позволяет добавлять в текстовый список фразу ( Add ), перематывать указатель на начало списка ( Begin ), на конец ( End ), получать текущее значение из списка с одновременной перемоткой указателя вверх ( GetDecString ) или вниз ( GetIncString ). При выполнении функций GetIncString и GetDecString СНАЧАЛА запоминается значение, которое будет возвращено а ПОТОМ происходит перемотка указателя вверх или вниз. Аргументом всех этих функций является имя списка. Если такого списка нет (т.е. в него еще ничего не было помещено), возвращается значение ASN_NOT_FOUND_PARAM, которое определено в файле ASN_types.h

typedef std::list _textlist;

class ITextListMap {
public:
virtual _textlist *GetList(const std::string& MapName, int start=0, int num=30)=0;
virtual void Add(const std::string& MapName, std::string Line)=0;
virtual void Begin(const std::string& MapName)=0;
virtual void End(const std::string& MapName)=0;
virtual std::string& GetIncString(const std::string& MapName)=0;
virtual std::string& GetDecString(const std::string& MapName)=0;
};

Реализация этого интерфейса CTextlistMap использует нормальные std::map и std::list для своей работы. Если на момент вызова функции Add текстовой карты с таким именем еще нет- функция создаёт новый текстовый список, в основе которого лежит std::list и запоминает его в своём списке текстовых карт std::map.

Я использовал это для реализации класса CNodeGUI_TextBox, который просто отрисовывает на сцене затекстурированный квадрат, заполняя его текстом из текстового списка.

Потом я думаю можно будет увязать "текстовые карты" с файлами и загружать/записывать информацию в них.

Изменения в исходниках

А на очередную версию в SVN ссылку не даю - после этого шага произошли еще некоторые изменения в движке. Об этом речь пойдет в следующем шаге...

Powered by: SourceForge.net Logo