1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/sogou-workflow

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
tutorial-10-user_defined_protocol.md 16 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 01.03.2025 10:20 552780a

Пример простого пользовательского протокола клиент/сервер

Пример кода

message.h
message.cc
server.cc
client.cc

Описание user_defined_protocol

В этом примере мы создаем простой протокол связи и используем его для реализации сервера и клиента. Сервер принимает сообщение от клиента, преобразует его в верхний регистр и отправляет обратно.

Формат протокола

Сообщение протокола состоит из 4-байтового заголовка и тела сообщения. Заголовок представляет собой целое число в сетевой последовательности, указывающее на размер тела. Формат запросов и ответов одинаков.

Реализация протокола

Для использования пользовательского протокола требуется предоставление методов сериализации и десериализации. Эти методы являются виртуальными функциями класса ProtocolMessage. Также рекомендуется реализовать перемещающие конструкторы и оператор присваивания для удобства использования.

В ProtocolMessage.h представлены следующие интерфейсы:

namespace protocol
{

class ProtocolMessage : public CommMessageOut, public CommMessageIn
{
private:
    virtual int encode(struct iovec vectors[], int max);

    /* Вы должны реализовать одно из этих 'append' функций, а первая с аргументом 'size_t *size' рекомендована. */
    virtual int append(const void *buf, size_t *size);
    virtual int append(const void *buf, size_t size);

    ...
};

}

Функция сериализации encode

  • Функция encode вызывается перед отправкой сообщения и вызывается только один раз для каждого сообщения.
  • Внутри функции encode пользователю необходимо сериализовать сообщение в массив vector, количество элементов которого не должно превышать max. На данный момент значение max равно 2048.
  • Структура struct iovec определяется системными вызовами readv и writev.
  • При успешном выполнении функция encode должна вернуть значение между 0 и max, которое указывает на количество использованных vector.
    • Для протокола UDP следует учитывать ограничение на общую длину до 64кБ и использование не более 1024 vector (Linux позволяет использовать максимум 1024 vector за одну операцию writev).
  • Возвращаемое значение -1 указывает на ошибку. При возникновении ошибки errno должен быть установлен. Если возвращаемое значение больше max, будет выдано сообщение об ошибке EOVERFLOW.
  • Чтобы повысить производительность, содержимое iov_base указателей в vector не копируется. Поэтому они обычно указывают на члены объекта сообщения.

Функция десериализации append

  • Функция append вызывается каждый раз при получении нового блока данных. Она может быть вызвана несколько раз для одного сообщения.
  • Аргументы buf и size представляют собой данные блока и его размер соответственно. Пользователю необходимо скопировать эти данные.
    • Если реализован интерфейс append(const void *buf, size_t *size), можно установить значение *size, чтобы сообщить фреймворку, сколько байтов было прочитано. Полученный размер - прочитанный размер = оставшийся размер, который будет получен снова при следующем вызове append. Эта возможность делает парсинг протокола более удобным, хотя пользователи могут также полностью скопировать все данные самостоятельно.
  • Функция append возвращает 0, если сообщение еще не завершено, и продолжает передачу. Возврат 1 указывает на окончание сообщения. Возврат -1 указывает на ошибку, и errno должен быть установлен.
  • Главная цель функции append — сообщить фреймворку, что сообщение закончилось. Не выполняйте сложный парсинг протокола внутри функции append.

Установка errno

  • Возврат -1 или другого отрицательного значения функций encode или append указывает на ошибку, и errno должен быть установлен для передачи причины ошибки.
  • Если произошла ошибка системного вызова или библиотечной функции (например, malloc), libc уже установит errno, поэтому нет необходимости делать это повторно.
  • Некоторые типичные ошибки, связанные с незаконными сообщениями, такие как EBADMSG для ошибочного содержимого сообщения и EMSGSIZE для слишком большого сообщения.
  • Пользователи могут выбрать значения errno, превышающие стандартные значения, для представления своих собственных ошибок. Обычно значения больше 256 доступны.
  • Не используйте отрицательные значения errno, так как фреймворк использует отрицательные значения для представления SSL ошибок.

В нашем примере сериализация и десериализация очень просты.
Заголовочный файл message.h объявляет классы request и response:

namespace protocol
{

class TutorialMessage : public ProtocolMessage
{
private:
    virtual int encode(struct iovec vectors[], int max);
    virtual int append(const void *buf, size_t size);
    ...
};

using TutorialRequest = TutorialMessage;
using TutorialResponse = TutorialMessage;

}

Классы request и response представляют один и тот же тип сообщения. Они используются через using.
Обратите внимание, что request и response должны иметь возможность создания без параметров, то есть должны существовать конструкторы без параметров или вообще отсутствовать.
Кроме того, при повторной попытке соединения объект response будет использоваться заново.

Server и Client Определение

Request и Response классы позволяют нам создать сервер и клиент, работающие через наш Tutorial протокол. В предыдущих примерах мы рассматривали типы данных для HTTP протокола:

using WFHttpTask = WFNetworkTask<protocol::HttpRequest,
                                 protocol::HttpResponse>;
using http_callback_t = std::function<void (WFHttpTask *)>;

using WFHttpServer = WFServer<protocol::HttpRequest,
                              protocol::HttpResponse>;
using http_process_t = std::function<void (WFTutorialTask *)>;

Аналогично, для нашего Tutorial протокола типы данных будут такими:

using WFTutorialTask = WFNetworkTask<protocol::TutorialRequest,
                                     protocol::TutorialResponse>;
using tutorial_callback_t = std::function<void (WFTutorialTask *)>;

using WFTutorialServer = WFServer<protocol::TutorialRequest,
                                  protocol::TutorialResponse>;
using tutorial_process_t = std::function<void (WFTutorialTask *)>;

СерверСервер, который работает как общепринятый HTTP-сервер, начинается с поддержкой IPv6 в приоритетном порядке, однако это не влияет на IPv4-клиентов. Также, мы ограничиваем размеры запросов максимум до 4 КБ. Полный код можно найти в файле server.cc.

Клиент

Клиент получает данные от стандартного ввода, отправляет запрос на сервер и получает результат. Этот процесс повторяется с помощью WFRepeaterTask, прекращая цикл, когда входные данные пользователя пусты. Кроме того, проверяем, чтобы ответы от сервера не превышали 4 КБ для обеспечения безопасности.

Основная задача клиента — создание задачи для специального протокола. Это можно найти в файле WFTaskFactory.h:

template<class REQ, class RESP>
class WFNetworkTaskFactory
{
private:
	using T = WFNetworkTask<REQ, RESP>;

public:
	static T *create_client_task(TransportType type,
								 const std::string& host,
								 unsigned short port,
								 int retry_max,
								 std::function<void (T *)> callback);

	static T *create_client_task(TransportType type,
								 const std::string& url,
								 int retry_max,
								 std::function<void (T *)> callback);

	static T *create_client_task(TransportType type,
								 const ParsedURI& uri,
								 int retry_max,
								 std::function<void (T *)> callback);

	static T *create_client_task(TransportType type,
								 const struct sockaddr *addr,
								 socklen_t addrlen,
								 int retry_max,
								 std::function<void (T *)> callback);
}

Пример конфигурации:

};

В данном примере TransportType указывает протокол транспортного уровня, доступны следующие значения: TT_TCP, TT_UDP, TT_SCTP и TT_TCP_SSL.

Различия между четырьмя интерфейсами незначительны; в нашем примере URL пока не требуется, мы используем доменное имя и порт для создания задач.

Если пользователю требуется использовать Unix Domain Protocol для доступа к серверу, следует использовать последний интерфейс, передавая sockaddr.

Актуальный вызов кода представлен ниже. Мы производим наследование от класса WFTaskFactory, но это наследование не обязательно.

namespace protocol;

class MyFactory : public WFTaskFactory
{
public:
    static WFTutorialTask* create_tutorial_task(const std::string& host,
                                                 unsigned short port,
                                                 int retry_max,
                                                 tutorial_callback_t callback)
    {
        using NTF = WFNetworkTaskFactory<TutorialRequest, TutorialResponse>;
        WFTutorialTask* task = NTF::create_client_task(TT_TCP, host, port,
                                                       retry_max,
                                                       std::move(callback));
        task->set_keep_alive(30 * 1000);
        return task;
    }
};

Как видно, используется класс WFNetworkTaskFactory<TutorialRequest, TutorialResponse> для создания клиентских задач.

Далее через метод set_keep_alive() задачи поддерживается соединение на 30 секунд после завершения связи, в противном случае будет использоваться короткое соединение.

Остальные части клиента содержат информацию, которая уже была представлена в предыдущих примерах. Подробнее см. client.cc.

Как создаются запросы для встроенных протоколов

Система поддерживает встроенные протоколы, такие как http, redis, mysql, kafka и dns. Можно ли создать задачи для этих протоколов аналогичным образом? Например:

WFHttpTask* task = WFNetworkTaskFactory<protocol::HttpRequest, protocol::HttpResponse>::create_client_task(...);

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

Аналогично, если создать MySQL-задачу таким образом, она может просто не запуститься из-за отсутствия процесса аутентификации.

Запрос к Kafka может требовать сложной взаимодействия с несколькими брокерами, что также невозможно реализовать данным способом.

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

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/sogou-workflow.git
git@api.gitlife.ru:oschina-mirror/sogou-workflow.git
oschina-mirror
sogou-workflow
sogou-workflow
master