Как стать автором
Обновить

Знакомство с EXtensible Server Core (exsc)

Время на прочтение 7 мин
Количество просмотров 1.9K
image

Всем привет! Хочу поделиться с общественностью фреймворком, на основе которого в данный момент множество серверов, обслуживают тысячи клиентов в различных серверных системах (по условиям контракта, продукты основанные на данном фреймворке не разглашаются). EXtensible Server Core (exsc) — это фреймворк, написанный на языке C и позволяет в рамках одного приложения, иметь один или несколько серверных потоков. Каждый серверный поток способен обслужить большое количество клиентов. Хотя фреймворк можно использовать в модели типа запрос-ответ, в первую очередь он был рассчитан на поддержание постоянного соединения с большим количеством клиентов и обменом сообщений в реальном времени.

Предыстория которую можно пропустить

Я работаю в компании которая разрабатывает клиент-серверные решения. Изначально все серверные программы были написаны на Qt и ядро было основано на базе QTcpSocket. Однако со временем количество клиентов на некоторых продуктах увеличивалось и стало понятно, что ядро написанное на Qt вскоре перестанет справляться с такими объёмами. Тогда и было принято решение переписать ядро на языке C. Данная статья не претендует на полноту описания фреймврока. Здесь будет описан самый простой механизм его использования. Когда данный фреймворк только разрабатывался — я не нашёл на просторах интернета универсального инструмента с открытым исходным кодом который бы имел похожий интерфейс и возможности.

Описание

В документации часто будет использоваться словосочетание «серверный поток». Серверный поток — это отдельный сервер, который прослушивает свой порт и работает в отдельном потоке. Все подключения которые подключаются к серверному потоку, работают в его же потоке (для каждого соединения не создаются отдельные потоки). Все callback функции вызываются в потоке сервера, однако функции для манипулирования серверным потоком, такие как exsc_send, exsc_sendbyname, exsc_connect и так далее, являются потокобезопасными. Применение нескольких серверных потоков можно использовать, когда для одного сервера не критично максимально быстрое выполнение запросов, а другой сервер должен выполнять запросы в реальном времени. Можно привести такой пример. Один серверный поток работает с базой данных, занимается регистрацией, логином, изменением настроек и так далее. А другой серверный поток выполняет задачи реального времени, выполняя запрос максимально быстро и работая с кешем, это может быть передача важных сигналов от одних пользователей к другим которые критичны ко времени. Пример создания двух серверных потоков который запускаются на разных портах и имеют различные callback функции:

exsc_init(2);
int des = exsc_start(9000, 30, 5, 1024, 20000, exsc_newcon, exsc_closecon, exsc_recv, exsc_ext);
int des_rt = exsc_start(9001, 30, 5, 1024, 20000, exsc_newcon_rt, exsc_closecon_rt, exsc_recv_rt, exsc_ext_rt);


Соединение

В рамках данного фреймворка за соединение отвечает структура exsc_excon. У этой структуры есть следующие поля:

ix — индекс соединения. Это порядковый номер соединения, которое было свободно в момент подключения клиента.
id — идентификатор соединения. Это уникальный номер соединения. В отличие от индекса он не повторяется.
addr — IP адрес клиента
name — имя соединения. Несколько соединений можно назвать одним именем и затем отослать какое-либо сообщение всем соединениям с одинаковым именем (смотрите функцию exsc_sendbyname).

Инициализация ядра

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

void exsc_init(int maxsrvcnt);

Параметр maxsrvcnt сообщает ядру, сколько серверных потоков мы будем использовать в рамках нашего приложения.

Запуск серверного потока

Далее нам нужно запустить серверный поток с помощью функции

int exsc_start(uint16_t port, int timeout, int timeframe, int recvbufsize, int concnt,
               void newcon(struct exsc_excon excon),
               void closecon(struct exsc_excon excon),
               void recv(struct exsc_excon excon, char *buf, int bufsize),
               void ext());

port — порт который будет прослушивать серверный поток.

timeout — указывает сколько времени серверный поток будет ожидать какой-либо активности от клиента. Если в течении этого времени клиент не прислал ни одного сообщения, то серверный поток закрывает такое соединение. Поэтому если мы хотим держать постоянную связь и выставили этот параметр на пример 30 секунд, то необходимо раз в 10-15 секунд присылать какое-либо сообщение типа ping.

timeframe — временные рамки, за которое мы позволяем выполнить запрос.Так на пример, если это значение выставлено на 100 миллисекунд и серверный поток обработал все текущие запросы от пользователей за 10 секунд, то он оставшиеся 90 миллисекунд оставит процессору для выполнения других задач. Таким образом, чем меньше это значение, тем быстрее серверный поток будет обрабатывать запросы, но тем больше он нагрузит процессор.

recvbufsize — размер буфера который серверный поток будет вычитывать за один раз.

concnt — максимальное количество соединений с которым серверный поток работает одновременно.

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

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

recv — сallback функция, которая будет вызываться когда клиент будет присылать пакеты. В параметры этой функции будет передаваться соединение клиента от которого пришли данные, указатель на данные и размер буфера с данными.

ext — сallback функция, которая будет вызываться каждый проход цикла серверного потока. Эта функция сделана для расширения функционала ядра. На пример сюда можно подвязать обработку таймеров.

Функция exsc_start возвращает дескриптор серверного потока, который понадобится для вызова некоторых функций фреймворка.

Отправка сообщений

За отправку сообщений отвечает функция

void exsc_send(int des, struct exsc_excon *excon, char *buf, int bufsize);

Эта функция потокобезопасная (её можно вызывать её из любого потока). В качестве параметров необходимо передать ей дескриптор серверного потока (который мы получили как возвращаемое значение функции exsc_start), соединение на которое мы хотим отправить сообщение, указатель на буфер с сообщением и размер буфера.

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

void exsc_sendbyname(int des, char *conname, char *buf, int bufsize);

Она аналогична функции exsc_send, за исключением второго параметра, в который передаётся имя подключений которым будет отправлено сообщение.

Задание имени подключения

Для тог, чтобы в будущем как то дополнительно идентифицировать подключение, либо хранить вместе с подключением некоторую информацию о нём, либо отправлять сообщения группе клиентов, используется функция

void exsc_setconname(int des, struct exsc_excon *excon, char *name);

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

Подключение серверного потока к другому серверу

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

void exsc_connect(int des, const char *addr, uint16_t port, struct exsc_excon *excon);

Эта функция потокобезопасная. В качестве параметров нам необходимо передать дескриптор серверного потока, адресс сервера к которому нам необходимо подключиться, потр сервера к которому нам необходимо подключиться и последним параметром мы передаём указатель на соединение с помощью которого мы в дальнейшем сможем вызывать другие функции фреймворка. Стоит отметить что нам нет необходимости дожидаться, пока подключение состоится. Мы можем вызвать функции exsc_connect и exsc_send одну за другой и система сама проследит за тем, чтобы сообщение было отослано сразу после того как сможет подключиться к удалённому серверу.

Пример сервера с комментариями


#include <stdio.h>
#include <string.h>
#include "../exnetwork/exsc.h"

int g_des; // дескриптор серверного потока

// ловим новое подключение
void exsc_newcon(struct exsc_excon con)
{
    printf("the connection was open  %s\n", con.addr);
}

// подключение закрывается
void exsc_closecon(struct exsc_excon con)
{
    printf("the connection was closed  %s\n", con.addr);
}

// принимаем сообщение от клиента
void exsc_recv(struct exsc_excon con, char *buf, int bufsize)
{
    char msg[512] = { 0 };
    memcpy(msg, buf, bufsize);
    printf("receive data from %s\n%s\n", con.addr, msg);

    if (strcmp(msg, "say hello") == 0)
    {
        strcpy(msg, "hello");
        exsc_send(g_des, &con, msg, strlen(msg));
    }
}

// Функция для расширения функционала
void exsc_ext()
{
}

int main()
{
    printf("server_test_0 is started\n");

    exsc_init(2); // инициализируем ядро с расчётом на два серверных потока

    // запускаем серверный поток на порту 7777
    // он будет держать неактивные соединения 30 секунд
    // будет обрабатывать входящие сообщения в рамках 10 миллисекунд
    // размер буфера для приёма задаём 1024 байта
    // максимальное количество подключений ограничиваем до 10000
    g_des = exsc_start(7777, 30, 10, 1024, 10000, exsc_newcon, exsc_closecon, exsc_recv, exsc_ext);

    // тормозим главный поток, чтобы программа не завершилась раньше времени
    // программа завершится когда мы введём команду exit и нажмём клавишу ENTER
    while (1)
    {
        const int cmdmaxlen = 256;
        char cmd[cmdmaxlen];
        fgets(cmd, cmdmaxlen, stdin);
        if (strcmp(cmd, "exit\n") == 0)
        {
            break;
        }
    }
    return 0;
}

Заключение


Ядро exsc осуществляет только низкий уровень взоимодействия с клиентами. Хоть это и самый важный элемент серверной системы, основа на которой всё строится, помимо него нужно строить более верхние уровни, которые будут отвечать за управление подключенями, генерирование сообщений, сборку сообщений (которые вероятно будут приходить за несколько этапов). Если эта статья будет иметь положительный отклик, будет написанна вторая чать, в которой будет развита тема разработки верхнего уровня серверной программы на основе данного ядра.

P.S.
Если кто-то решит разобраться во внутренней логике данного фреймворка или станет применять его в своих проектах и найдёт в нём ошибки, либо узкие места, просьба сообщить об этом. Сообщество для того и нужно, чтобы каждый кто может, внёс свой вклад в open source проекты.

Ссылка на фреймворк
Теги:
Хабы:
+4
Комментарии 10
Комментарии Комментарии 10

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн