На официальном сайте www.d2school.com используется da4qi4 в качестве веб-сервера разработки (nginx + da4qi4 + redis + mysql).
Эффект от работы сайта на мобильном телефоне:
Стиль некрасивый, но это связано только с плохим UX-дизайнером, а не с используемым бэкендом.
Приоритет использования зрелых и открытых проектов C/C++ при разработке da4qi4. Ключевые компоненты:
Примечание:
Используя разработку на C++, основанную на асинхронной структуре, цель состоит в том, чтобы иметь хорошую отправную точку для производительности, и разработчикам не нужно слишком беспокоиться о производительности. Конечно, производительность также не может быть плохой, потому что плохая производительность неизбежно повлияет на удобство использования продукта.
В настоящее время проводится сравнение только с Tomcat. Поскольку Tomcat, похоже, использует модель «Per Connection Per Thread», это сравнение будет немного предвзятым; однако, учитывая широкое применение Tomcat в реальных системах, сравнение с ним полезно для демонстрации применимости da4qi4 с точки зрения производительности.
Тестовая среда:
Конфигурация Tomcat:
Количество параллельных запросов | Среднее время отклика (мс) | Медиана времени отклика (мс) | Время отклика 99% пользователей (мс) | Минимальное время отклика (мс) | Максимальное время отклика (мс) | Частота ошибок | Пропускная способность (с) | Получено байт в секунду (КБ) | |
---|---|---|---|---|---|---|---|---|---|
Tomcat | 1 000 | 350 | 337 | 872 | 1 | 879 | 0 | 886,7 | 273 |
da4qi4 | 1 000 | 1 | 1 | 20 | 0 | 24 | 0 | 1233 | 286,6 |
Кроме того, официальный сайт www.d2school.com изначально использовал сервер с 1 МБ пропускной способностью, 1 ядром ЦП и 1 ГБ ОЗУ для запуска (то есть одновременно запускал MySQL и Redis). Позже из-за медленной компиляции в сети было сделано ограниченное обновление.
Позже будет проведено больше сравнений с другими веб-серверами. В целом, текущая разработка da4qi4 не ставит целью значительное повышение производительности.
Как известно, язык программирования C++ сложен, и он очень подходит для программистов на C++. Поэтому некоторые проекты с открытым исходным кодом на C++ хотя и технически превосходны, но их легко отпугнуть обычных программистов на C++. Например, они любят использовать «шаблоны» ... da4qi4 строго контролирует этот «мастерский» импульс в коде, стараясь сделать код как можно более простым и понятным, особенно для внешних интерфейсов. Он следует принципу KISS и не позволит вам испытать какое-либо «удивление» (когда-нибудь видели программу, которая была написана так просто и элегантно?). В запросе представлен текст технической направленности из области разработки и тестирования программного обеспечения. Основной язык текста запроса — китайский язык.
Перевод на русский язык:
В любом режиме программирования, поддерживаемом C++ в широком диапазоне, включая «процедурный», «объектно-ориентированный», «объектный» и «обобщённый», вам достаточно быть знакомым с процедурным подходом и немного разбираться в объектно-ориентированном подходе, чтобы уверенно использовать эту библиотеку.
0.5 Следуйте за отечественным производственным окружением
Какой версией C++ пользоваться? Какой версией boost пользоваться? Какой версией OpenSSL пользоваться? Какой версией CMake пользоваться?
Есть одно правило: основные облачные провайдеры уже предоставляют готовые версии, мы используем их — это означает, что вам нужно только скомпилировать написанный код, и вы сможете выполнить онлайн-сборку и развёртывание. Не нужно компилировать boost, не нужно компилировать OpenSSL, не нужно загружать и компилировать новую версию CMake...
Alibaba Cloud, Tencent Cloud, Baidu Cloud, Huawei Cloud, Qiniu Cloud... Независимо от того, какой провайдер вы выберете, если вы запросите сервер Ubuntu 18.04 (или более поздней версии) на их платформе, вы сможете легко скомпилировать и развернуть его онлайн, превратив его в WEB-сервер с работающим «большим устройством INSIDE», который будет предоставлять услуги вашему пользователю. Минимальные требования к конфигурации сервера: 4 ГБ памяти, 1 МБ пропускной способности, 1 ядро процессора.
Часть 1. Быстрое понимание
1.1 Пустой WEB-сервер
Нам нужен файл C++, назовём его main.cpp, содержимое которого следующее:
#include "daqi/da4qi4.hpp"
using namespace da4qi4;
int main()
{
auto svc = Server::Supply(4098);
svc->Run();
}
Менее чем за 10 строк кода мы создали пустой, казалось бы, бездействующий WEB-сервер.
Скомпилируйте и запустите его, а затем введите в адресной строке браузера: http://127.0.0.1:4098 и нажмите Enter. Браузер отобразит страницу с надписью:
Not Found
Хотя он и кажется бездействующим, работа этого WEB-сервера полностью логична: мы не предоставили ему никаких ресурсов или ответов на операции, поэтому любой доступ к нему возвращает страницу 404 «Not Found».
1.2 Hello World!
Теперь реализуем следующую функцию: когда пользователь обращается к корневому пути сайта, он должен отвечать «Hello World!».
1.2.1 Ответ на указанный URL
Для этого нам нужно указать в коде, как реагировать на посещение корневого пути сайта. Ответ может быть функцией-указателем, std::function, методом класса или лямбда-выражением, представленным в C++11. Начнём с последнего:
#include "daqi/da4qi4.hpp"
using namespace daqi4;
int main() {
auto svc = Server::Supply(4098);
svc->AddHandler(_GET_, "/", [](Context ctx) {
ctx->Res().ReplyOk("Hello World!");
ctx->Pass();
});
svc->Run();
}
В приведённом выше примере используется метод AddHandler() класса Server, который принимает три параметра:
Эти три параметра и имя метода вместе выражают следующее: если пользователь посещает корневой путь сайта с помощью метода GET, фреймворк вызовет лямбда-выражение для ответа.
Скомпилируйте и запустите. Теперь перейдите в браузер по адресу http://127.0.0.1:4098, и вы увидите:
Hello World!
В качестве сравнения ниже представлена реализация той же функции с использованием свободной функции:
#include "daqi/da4qi4.hpp"
using namespace daqi4;
void hello(Context ctx) {
ctx->Res().ReplyOk("Hello World!");
ctx->Pass();
}
int main() {
auto svc = Server::Supply(4098);
svc->AddHandler(_GET_, "/", hello);
svc->Run();
}
Чтобы сэкономить место в тексте, в последующих демонстрациях используется лямбда-выражение для выражения операций HTTP-ответа. На самом деле, лямбда не является обязательной, и класс фактически редко используется — это основное требование простого WEB-сервера: по возможности избегайте состояния; свободная функция более «естественна» и не имеет состояния по сравнению с методами класса (или так называемыми методами).
1.2.2 Возврат HTML
Приведённый выше код возвращает чистый текст в браузер. Далее следует вернуть HTML-контент. Из соображений демонстрации мы сделали одну «вонючую» вещь: напрямую написали HTML-строку в коде. Вскоре мы покажем, как это сделать правильно: использовать статические файлы или на основе шаблонов веб-страниц для настройки содержимого веб-страницы; но сейчас давайте изменим одиннадцатый ряд кода, вызывающий функцию ReplyOK(), которая изначально была «Hello World!», а теперь станет строкой HTML:
……
ctx->Res().ReplyOk("<html><body><h1>Hello World!</h1></body></html>");
……
1.3 Обработка запросов
Далее мы хотим, чтобы содержимое запроса и ответа могло немного измениться, и между ними существует определённое соответствие. Конкретно: в URL запроса добавляется параметр, предположим, name=Tom, тогда мы надеемся, что сервер вернёт «Hello Tom!».
Это требует использования «Request/Запрос» и «Response/Ответ»:
#include "daqi/da4qi4.hpp"
using namespace daqi4;
int main() {
auto svc = Server::Supply(4098);
svc->AddHandler(_GET_, "/", [](Context ctx) {
std::string name = ctx->Req("name");
std::string html = "<html><body><h1>Hello " + name + "!</h1></body></html>";
ctx->Res().ReplyOk(html);
ctx->Pass();
});
svc->Run();
}
Важно: здесь мы используем лямбда-выражения для удобства демонстрации, но реальная система не может поместить весь код в функцию main(). Поэтому определённо есть отдельные функции. Использование самых основных функций в языке программирования не стыдно, потому что мы стремимся к практичности, а не к тому, чтобы показать «Я знаю лямбды!». (См.: 0.4 Простота превосходит навыки)
Скомпилируйте и запустите. Перейдите в браузере по адресу «http://127.0.0.1:4098/?name=Tom», и вы получите «Hello Tom!» с HTML-форматированием.
1.4 Введение приложения
Скомпилируйте и запустите. Перейдя в браузере по адресу «http://127.0.0.1:4098/?name=Tom», вы получите «Hello Tom!» с форматированием HTML.
Сервер представляет собой WEB-сервис, но одна и та же система WEB-серверов часто может быть разделена на несколько разных групп пользователей.
Например, написание онлайн-магазина, где основными пользователями являются покупатели, которые приходят в магазин за покупками онлайн, а вторая группа пользователей — продавцы и администраторы магазина. Это различие также можно назвать: один сервис, несколько приложений. В рамках большого устройства приложение представлено Application.
На данный момент мы ещё не продемонстрировали случай, когда на одном сервере работает несколько приложений, так зачем нам начинать представлять Application? Приложение отвечает за реализацию основного поведения в фоновом режиме. В предыдущем примере, хотя в коде виден только сервер, на самом деле сервер помогает нам создать экземпляр объекта по умолчанию, а затем использовать этот объект по умолчанию для реализации функций, показанных в примере.
Ниже мы получаем этот «Application | Приложение» через «Server | Сервисный» объект и заменяем первый для реализации последней функции примера.
#include "daqi/da4qi4.hpp"
using namespace daqi4;
int main() {
auto svc = Server::Supply(4098);
auto app = svc->DefaultApp(); // Получаем автоматически созданный объект приложения по умолчанию
app->AddHandler(_GET_, "/", [](Context ctx) {
std::string name = ctx->Req("name");
std::string html = "<html><body><h1>Hello " + name + "!</h1></body></html>";
ctx->Res().ReplyOk(html);
ctx->Pass();
});
svc->Run();
} **Перевод текста на русский язык:**
Кроме того, что объектом реализации «AddHandler()» раньше был svc, а теперь стал «app», в остальном практически ничего не изменилось. Код соответствует тому, который использовался до явного введения Application. Но зачем нам нужно вводить Application? Помимо вышесказанного, это делается для подготовки к будущему, когда один сервер будет соответствовать нескольким приложениям. С точки зрения проектирования и эксплуатации есть ещё одна цель: разделить ответственность между сервером и приложением.
**Приложение отвечает за более высокоуровневую логику, акцентируя внимание на конкретной бизнес-логике, в то время как сервер отвечает за базовую логику сервера, фокусируясь на сетевых функциях.** Следующий раздел будет посвящён журналам, которые являются типичным проявлением разделения труда между ними.
#### 1.5 Запуск журналов
Конечно, при работе веб-сервера могут возникать различные проблемы. В таких случаях возможность вывода и хранения журналов во время работы становится необходимой функцией. Кроме того, самое важное заключается в том, что если вы создаёте серверную программу и она работает без какого-либо вывода на экран в течение полугода, это может выглядеть непрофессионально и повлиять на вашу зарплату...
В соответствии с разделением труда между сервером и приложением, журналы делятся на две основные части: серверные и прикладные.
* Серверные журналы: глобально уникальные, регистрируют состояние базовой сетевой инфраструктуры и связанных с ней периферийных сред выполнения (кэш/Redis, база данных/MySQL).
* Прикладные журналы: каждый приложение имеет свой собственный журнал регистрации, записывающий его работу.
Серверные журналы создаются автоматически с помощью фреймворка; прикладные журналы, естественно, соответствуют каждому приложению. Программа может создавать разные стратегии ведения журналов для серверного и прикладного уровней. Фактически, если существует несколько приложений, можно настроить разные стратегии журналов для каждого из них. Если не создать журнал регистрации для конкретного приложения, оно будет работать на полной скорости, не выводя никаких журналов — звучит круто, но не стоит быть слишком уверенным в своём коде.
```C++
#include "daqi/da4qi4.hpp"
using namespace da4qi4;
int main()
{
// Инициализация серверного журнала, необходимо указать, где должен храниться файл журнала и какой уровень детализации требуется
log::InitServerLogger("Ты хочешь / файл журнала сервера / хранить / каталог /", log::Level::debug);
auto svc = Server::Supply(4098);
log::Server()->info("Сервис успешно загружен."); // принудительный вывод одной записи в журнале сервера
auto app = svc->DefaultApp();
// Теперь инициализируем журнал приложения
app->InitLogger("Ты хочешь / каталог хранения / текущего приложения /");
app->AddHandler(_GET_, "/", [](Context ctx) {
std::string name = ctx->Req("name");
std::string html = "<html><body><h1>Hello " + name + "!</h1></body></html>";
ctx->Res().ReplyOk(html);
ctx->Pass();
});
svc->Run();
log::Server()->info("До свидания!"); // принудительно вывести ещё одну запись в журнале сервера
}
Все функции журнала находятся в пространстве имён «log::». Эта конфигурация журнала не только выводит информацию на терминал (консоль), но и автоматически сохраняет её в файле в указанном каталоге, причём серверный и прикладной журналы являются отдельными файлами. Файлы имеют стандартные ограничения по размеру и количеству. Обычно, при запуске программы на Linux-сервере, она выполняется в фоновом режиме и перенаправляет вывод экрана на текущий запуск в определённый файл.
Журнал поддерживает следующие уровни контроля вывода: трассировка (trace), отладка (debug), информация (info), предупреждение (warn), ошибка (err), критическая ошибка (critical). В примере используется уровень по умолчанию «info» для «app->InitLogger()».
Ниже представлен снимок экрана с примером запуска журнала.
Он выглядит как фоновая программа, и вы можете пригласить своего руководителя, чтобы он оценил вашу работу.
Пришло время решить проблему непосредственного написания HTML-кода в программе.
Содержимое веб-страницы, которое видит пользователь, частично определяется на этапе проектирования системы, а частично должно быть известно только после того, как пользователь посетит сайт. Например, в приведённом выше примере заранее известны: шрифт страницы, а также «Привет, _ _ _ _ !»; в то же время, то, что должно быть заполнено в нижнем подчёркивании, станет известно только тогда, когда пользователь перейдёт по ссылке.
Вот простой HTML-шаблон, подходящий для этого примера:
<!DOCTYPE html>
<html lang="zh">
<head>
<title>Главная страница</title>
<meta content="text/html; charset=UTF-8">
</head>
<body>
<h1>Привет, {=_URL_PARAMETER_("name")=}!</h1>
<p>Ваш браузер: {=_HEADER_("User-Agent")=}</p>
<p>Вы обращаетесь к этому сайту через: {=_HEADER_("Host")=}</p>
</body>
</html>
«Привет,» перед определённым форматом {= URL_PARAMETER(«name») =} будет распознан программой и заполнен значением параметра «name».
Объяснение:
Предположим, этот файл хранится в «вашем / каталоге шаблонов / веб-страниц». В следующем коде «app->SetTemplateRoot()» будет использоваться путь к этому каталогу.
#include "daqi/da4qi4.hpp"
using namespace da4qi4;
int main() {
log::InitServerLogger("Вы хотите / каталог файлов журнала / сервера /", log::Level::debug);
auto svc = Server::Supply(4098);
log::Server()->info("Сервис успешно загружен.");
auto app = svc->DefaultApp();
// Две новые строки:
app->SetTemplateRoot("ваш / каталог шаблонов / веб-страниц /"); // корневой каталог шаблона файла
app->InitTemplates(); // загрузка и компиляция шаблона файла в байт-код
app->InitLogger("вы хотите / каталог текущего приложения /");
// Следующая строка позволяет серверу обнаруживать изменения в шаблонах (включая новые) каждые 5 секунд
svc->EnableDetectTemplates(5); // 5 секунд, в реальных проектах рекомендуется устанавливать более длительные интервалы, например 10 минут
svc->Run();
log::Server()->info("До свидания!");
}
Теперь, используя Firefox для доступа к URL с параметром «name», например http://127.0.0.1:4098?name=da4qi4, вы получите следующий HTML-контент:
<h1>Привет, da4qi4!</h1>
<p>Ваш браузер: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0</p>
<p>Вы обратились к этому сайту через: 127.0.0.1:4098</p>
Небольшая подсказка: «Почему код стал короче?» Вы должны заметить, что после использования шаблонного ответа «AddHandler()» больше не виден. Это связано с тем, что в этом примере нет реальной бизнес-логики: пользователь посещает URL-адрес с параметром, и сервер на основе заранее определённого шаблона отображает этот параметр без изменений. Конечно, реальная система не может быть такой простой (иначе зачем нам нужны разработчики серверных программ?), но при быстрой разработке системы в процессе начальной разработки такая ситуация очень распространена, и без изменения исходного кода или перезапуска службы можно сразу увидеть новый или изменённый контент веб-страницы.
Шаблоны, предоставляемые фреймворком, могут не только заменять данные, но и поддерживать базовые условные операторы, циклы, пользовательские функции и т. д., подобно сценарию.
Важно: в большинстве случаев мы пишем программы на C++ для высокопроизводительного получения данных из различных источников (базы данных, кэш, файлы, сеть и т.д.) различными способами (синхронно, асинхронно) и их обработки. Однако интерпретаторы шаблонов в программах на C++ обычно используются для простых задач и не должны быть слишком сложными, поскольку в контексте языка C++, который является мощным языком, интерпретатор шаблонов «язык» не может сравниться с ним ни по функциональности, ни по производительности.
Далее мы рассмотрим пример, содержащий бизнес-логику. Эта бизнес-логика очень сложна и сильно зависит от вычислительной мощности процессора... Мы создадим сумматор. Пользователь вводит в адресную строку браузера:
http://127.0.0.1:4098/add?a=1&b=2
Браузер отобразит результат сложения a и b. Очевидно, бизнес-логикой является вычисление суммы двух целых чисел, и наш мощный и избыточный по вычислительным ресурсам язык C++ наконец-то может пригодиться. Поддержка передачи больших сообщений по частям (что также является требованием WebSocket).
Поддержка групповой рассылки.
Сохранение концепции и дизайна, аналогичных HTTP, например, контекста: Context.
При подключении через WebSocket можно легко получить информацию о Cookie, HTTP-заголовках и URL до обновления соединения (upgrade).
Пример 1. Сначала покажем пример объектно-ориентированного подхода.
using namespace da4qi4;
class MyEventsHandler : public Websocket::EventsHandler
{
public:
bool Open(Websocket::Context ctx) { return true; } //разрешить подключение ws
void OnText(Websocket::Context ctx, std::string&& data, bool isfinish)
{
ctx->Logger()->info("Получено: {}.", data);
ctx->SendText("Прочитано!");
}
void OnBinary(Websocket::Context ctx, std::string&& data, bool isfinish)
{
//здесь data - двоичные данные, например изображение, которое можно сохранить...
}
void OnError(Websocket::Context ctx
, Websocket::EventOn evt //на каком этапе произошла ошибка, чтение или запись?
, int code //код ошибки
, std::string const& msg //сообщение об ошибке
)
{
ctx->Logger()->error("Ошибка. {} - {}.", code, msg);
}
void OnClose(Websocket::Context ctx, Websocket::EventOn evt)
{
ctx->Logger()->info("Соединение Websocket уже закрыто.");
}
};
#include "daqi/da4qi4.hpp"
#include "daqi/websocket/websocket.hpp" //импорт определений websocket
using namespace da4qi4;
class MyEventsHandler : public Websocket::EventsHandler
{
//см. выше
};
int main()
{
auto svc = Server::Supply(4098);
auto app = svc->DefaultApp();
app->InitLogger("log/");
//в определённом URL приложения подключите обработчик ответов websocket
app->RegistWebSocket("/ws", UrlFlag::url_full_path,
[](){ return new MyEventsHandler; }
);
svc->Run();
}
Теперь пусть ваш фронтенд-разработчик напишет код на JavaScript в HTML-странице, подобный этому:
var ws = new WebSocket("ws://127.0.0.1:4098/ws");
ws.onopen = function(evt) {
this.send("Hello WebSocket.");
}
ws.onmessage = function (evt) {
console.log(evt.data);
}
……
И клиент, и сервер готовы к работе.
Пример 2. Если логика бэкенда действительно проста, то создание класса может показаться излишним. В этом случае вы можете использовать простые функции или лямбды для быстрого реагирования.
Метод заключается в определении переменной «Websocket::EventHandleFunctor», которая является предопределённым объектом:
#include "daqi/da4qi4.hpp"
#include "daqi/websocket/websocket.hpp" //импорт определений websocket
using namespace da4qi4;
/*не нужно определять класс*/
int main()
{
auto svc = Server::Supply(4098);
auto app = svc->DefaultApp();
app->InitLogger("log/");
Websocket::EventHandleFunctor functor;
functor.DoOnText = [] (Websocket::Context ctx, std::string&& data, bool isfinished)
{
ctx->Logger()->info("Получено: {}.", data);
ctx->SendText("Прочитано!");
}
app->RegistWebSocket("/ws", UrlFlag::url_full_path, functor);
svc->Run();
}
Поддержка cookie.
Поддержка кэширования в браузере (клиенте).
Поддержка Redis кэша.
Поддержка сессий.
Статические файлы.
Обнаружение обновлений и горячая загрузка шаблонов файлов.
Компоненты HTTP/HTTPS клиента (уже на основе этого реализованы C++SDK для входа в систему с помощью сканирования QR-кода WeChat и коротких сообщений Aliyun, см. ниже).
Поддержка POST-ответов.
Загрузка и скачивание файлов.
Ограничение доступа.
JSON.
API-интерфейсы для вывода чистых данных, которые могут быть интегрированы с AJAX на стороне клиента.
Полная интеграция фреймворков: (a) на основе исходного кода; (b) на основе динамической библиотеки; (c) на основе статической библиотеки.
Распространённые кодировки (UTF-8, UCS, GBK, GB18030).
……
Доступ к базе данных.
Интеграция с nginx (для обеспечения горизонтального масштабирования с балансировкой нагрузки).
Асинхронный клиент для отправки коротких сообщений Aliyun.
Асинхронный клиент для входа в WeChat с помощью сканирования QR-кода.
Инструмент шифрования данных на основе OpenSSL.
Общие инструменты обработки строк.
……
Хотя используемые компоненты поддерживают кроссплатформенность, текущая версия фреймворка поддерживает только компиляцию в Linux. Большинство реальных проектов работают в Linux.
Веб-сервисы развёртываются на Linux, и для этого есть причины: (1) вышеупомянутые внешние компоненты: nginx, mysql, redis устанавливаются в Linux одной командой, что намного проще, чем в Windows. (2) Если вы используете очень популярный в настоящее время Docker, это ещё более верно. (3) На самом деле, большинство популярных фреймворков для разработки веб-приложений сначала предоставляют версию для Linux, а затем рассматривают возможность выпуска версии для Windows, некоторые даже отказываются выпускать версию для Windows (например, автор Redis отказался от предложения Microsoft предоставить пакет исправлений, который также позволяет запускать Redis в Windows).
В будущем фреймворк будет предоставлять версию для Windows.
Большинство облачных провайдеров в Китае предлагают Ubuntu Server версии 18.04 LTS. Если у вас есть сервер Ubuntu 2019 года или более поздней версии, вы можете создать фреймворк на нём. Для этого вам потребуется только библиотека iconv для преобразования китайских символов, которую необходимо загрузить и скомпилировать вручную, в остальном все остальные зависимости можно получить из репозитория Ubuntu.
Далее предполагается, что система работает под управлением Ubuntu 18.04 Server. Однако, поскольку разработка обычно выполняется на настольных компьютерах, при установке компонентов необходимо временно повысить права пользователя, т. е. добавить «sudo» перед соответствующими командами. Например, на рабочем столе:
sudo apt install git
На сервере:
apt install git
sudo apt install build-essential
sudo apt install cmake
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )