​Современный мир не мыслим без работы в сети. Это может быть сеть предприятия, или Интернет, или какая-либо виртуальная частная сеть. Задачи, связанные с работой по сети, возникают в любой отрасли, практически без исключений. И если бы речь шла только о сайтах, раздающих статический материал, никому бы и в голову не пришло, писать это на С++. 

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

Компоненты системы взаимодействуют между собой по сети, используя различные протоколы. Также предоставляется способ внешнего взаимодействия с системой. Обычно, в качестве способа интеграции с "внешним миром" реализуется протокол на базе REST (Uber, Яндекс.Диск, DigitalOcean, AWS, список почти бесконечен). Реализовать такое взаимодействие можно чуть ли не в автоматическом режиме (Swagger). Код в этом случае будет сгенерирован для любого языка... но не для С++.

Как же быть, если нам нужно использовать C++, для реализации веб-сервиса, поддерживающего REST-API ? Почему нам может понадобиться именно С++ для этой задачи? Об этом и многом другом поговорим далее.

Веб-фреймворки и Веб-сервера

Итак, веб-сервисы и веб-приложения создаются, как правило, не на С++. Любой другой язык предоставляет массу качественных веб-фреймворков: у Python есть Django, Flask, Tornado; у PHP есть Symfony; у Javascript (Node.js) есть Express; у Java есть Spring Web MVC; у Erlang есть N2O и так далее. Это отличные решения, которые, в первую очередь, позволяют программисту не погружаться в протокол HTTP (RFC2616), а сразу сосредоточиться на бизнес-логике своего приложения. Многие веб-фреймворки имеют в своем составе реализацию ORM, что упрощает работу с базами данных. Кроме этого, веб-фреймворки предоставляют:

  • диспетчеризацию URL;
  • систему шаблонов, для удобного создания html-страниц;
  • средства для интернационализация и локализации;
  • авторизацию и аутентификацию;
  • и многое другое.

Но одного веб-фреймворка не достаточно для создания веб-сервиса, понадобится еще, как минимум, веб-сервер. В мире не так много веб-серверов, заслуживающих внимание. Это Apache HTTP server, Nginx, lighttpd, Tornado, Node.js, Yaws, Netty. Веб-серверов существует меньше, чем веб-фреймворков, и новые появляются значительно реже (Caddy). Зачем же нужен веб-сервер, если есть веб-фреймворк?

Веб-сервер - это надежная, быстрая, потребляющая небольшое количество ресурсов программа. Веб-сервер способен поддерживать одновременное подключение тысяч клиентов, и не терять при этом работоспособность. Веб-сервер отлично справляется с такими задачами как: шифрование данных (SSL termination), сжатие данных, обслуживание статических запросов, кеширование, балансировка нагрузки и так далее. Веб-фреймворки не касаются решения этих задач. Эти задачи не являются их зоной ответственности. Фактически, установив и настроив веб-сервер на прием запросов из внешнего мира, мы снимаем огромный "пласт" работы с веб-фреймворка (который "как бы спрятан" за веб-сервером). Например, ему больше не обязательно "быть готовым" встретить тысячи пользователей. Вместо этого, мы можем развернуть 20 веб-фреймворков за одним веб-сервером, и тонко настроить балансировку нагрузки.

То, что веб-сервер необходимо выбирать именно из числа выше названных, подтверждается крупнейшими компаниями - Яндекс, Google, Facebook, Bloomberg, Amazone - какой бы веб-фреймворк не использовался этими компаниями, неизменным остается то, что никто не пишет свой веб-сервер. Обычно, берут либо Nginx, либо Apache. И это первое, что нам нужно уяснить - при разработки веб-приложения на С++, нам также потребуется веб-сервер из числа названных.

Зачем писать веб-приложение на С++

Возможно, вы программист С++ и другие языки вам не знакомы. Возможно, в компании не разрешают писать на чем-либо кроме С++, а сделать свой веб-сервис необходимо по ТЗ. Ситуации бывают разные, и говорить, что С++ для этого совершенно не подходит не правильно.

Есть одна, но очень веская причина разрабатывать back-end веб-сервиса на С++ - любой интерпретируемый язык (будь то Python с его несчастным GIL-ом, Ruby, PHP и другие) проиграет  С++ в быстродействии и потреблении ресурсов. Хорошо написанная, и оптимизированная компилятором, программа на С++ будет работать не хуже аналогичного варианта на С. Если, высокая эффективность для вас не главное требование, то возможно С++ не стоит брать для разработки веб-сервиса. Писать на С++ однозначно придется больше и дольше. Однако, есть вы хотите получить максимум КПД от своих вычислительных мощностей (которых всегда не хватает), то вам придется перейти на компилируемый язык. И С++ тут отличный и хорошо знакомый всем кандидат (языки Go и Rust еще не завоевали такой популярности, а Java и C# могут оказаться избыточными для микросервиса). 

Итак, веб-приложение или веб-сервис будем разрабатывать именно на С++.

Почему не нужно писать свой Веб-сервер на Asio

Я очень много разрабатываю с использованием Asio и добиваюсь очень эффективных результатов. Миллионы сообщений переданных по сети в течении всего одной секунды, сериализованных и  зашифрованных. Asio - это отличная библиотека! Но она не подходит для того, чтобы делать на ней веб-сервер... Почему? Ответ - это слишком низкоуровневый подход. Конечно, возможно у вас получится написать эффективный, быстрый и надежный код. Но сколько на это уйдет времени? И будет ли это решение в итоге быстрее, чем Nginx? И главное, как вы будете решать вопрос функциональных возможностей? Давайте просто сравним, что дает нам Asio, и что предлагает его "Java аналог" - Netty:


Asio C++ Library


Netty framework

Сразу скажу, что поддержка SSL и TLS в Asio хуже, чем у Netty. По сути, вам придется напрямую работать с OpenSSL, так как ASIO предоставляет весьма условные классы-обертки. Здесь можно было бы обойтись и вовсе без Asio, так как вся работы возлагается на OpenSSL. Но шифрование в данном случае не главное.

Обратите внимание на протоколы. Asio заканчивается на уровне TCP. Дальше вам всё нужно будет написать самим. Скорее всего потребуются дополнительные библиотеки, чтобы, например, обеспечить сжатие данных в протоколе HTTP, чтобы поддержать протокол Web-сокетов. В конце концов, чтобы просто получить веб-сервер, соответствующий протоколу HTTP/1.1. Ведь в С++ нет даже стандартной библиотеки для парсинга URL (кто считает это ерундой, тот не парсил арабские линки). Всё это вы либо возьмете из внешних библиотек, либо будете очень долго писать сами, либо (что хуже всего) откажетесь вовсе поддерживать (сказав - мне это не нужно).

Какой бы путь вы не выбрали с Asio, вы потеряете очень много времени, и не приблизитесь к решению именно вашей задачи. Вы просто будете писать свой веб-сервер, который в лучше случае сможет работать также надежно и быстро, как Nginx. И который в лучшем случае поддержит хотя бы 10% от того, что сразу идет в базовой конфигурации любого существующего веб-сервера. Вы удовлетворите только собственное желание "попробовать сделать самому". Пишите на Asio то, что нельзя взять в готовом варианте. Веб-сервер же установите Nginx. На это у вас уйдет 1 минута, и 10 минут, чтобы сконфигурировать его работу.

Почему не нужно брать Веб-сервер на Qt, POCO, Wt C++ или cpp-netlib

К этому моменту мне, возможно, уже удалось отговорить вас писать веб-сервер самостоятельно. Но как насчет Qt? POCOWt? Или cpp-netlib? Последняя библиотека, кстати, использует Asio. Все перечисленные инструменты - это C++, а не Java/Python/C. Они имеют хорошие функциональные возможности, готовые реализации веб-серверов, и даже предлагают себя в качестве веб-фреймворков. Как, к примеру, Wt (у Wt на борту есть Bootstrap 2 и 3, что кажется уже перебором). Казалось бы на С++ есть всё готовое. Бери и используй. Но не спешите...

Вот один из многочисленных бенчмарков HTTP-серверов, опубликованный на Хабре. Смотрим результаты: POCO медленнее Nginx-а всего лишь в 5 раз, cpp-netlib медленнее в 20 раз! И вот вопрос: зачем тянуть в проект большие внешние библиотеки, которые изначально хуже, чем тот же Nginx? Nginx активно развивается на протяжении более десяти лет (как и Apache или lighttpd). Повторить эту работу не просто даже специализированным библиотекам. Отмечу, что все перечисленные C++ библиотеки, также уступают в функциональности тому же Apache. Для Apache сообщество на протяжении длительного времени написало такое количество подключаемых модулей, что их трудно все перечислить

В итоге, если вы пишете свой веб-сервис или веб-приложение на С++, вам лучше использовать в качестве веб-сервера Nginx, Apache или lighttpd. При чем не нужно компилировать и линковать код этих веб-серверов вместе со своим кодом. Эти сервера должны быть установлены "штатно", как и любые другие программы, и работать самостоятельно. Нам же остается только найти решение, как "подружить" программу на С++ с внешним веб-сервером. И решение здесь очень простое.

SCGI и FastCGI - способ подружить Веб-сервер и программу на C++

Если эти технологии вам не знакомы, то прочитайте о них на Википедии. В двух словах - это клиент-серверные протоколы взаимодействия веб-сервера и приложения (в нашем случае С++ приложения). Эти два протокола являются дальнейшим развитием протокола CGI. Однако, CGI работает медленно, и лучше этот подход не рассматривать.

FastCGI-приложения используют Unix Domain Sockets или TCP/IP для связи с веб-сервером. Первый вариант менее гибкий, но более быстрый. Второй вариант, это обычное сетевое клиент-серверное взаимодействие. И здесь (внимание) есть смысл применить свои навыки в Asio, и написать эффективный транспорт для общения с веб-сервером! Потому что, только это от вас и требуется. 

Nginх "встретит" запрос пользователя, расшифрует его, определит куда следует направить дальше этот запрос, и передаст его вашему приложению на С++. Дождется ответа от вашего приложения, сожмет и зашифрует данные, а затем передаст их пользователю. Масштабировать такое решение очень легко - просто запустите столько копий FastCGI-приложения сколько потребуется (на разных машинах или на одной), настройте балансировку в Nginx и всё!

С вашего приложения снимается огромная работа, а в замен вам нужно поддержать на выбор FastCGI или SCGI. Тут есть два подхода - либо взять готовые решения, либо совместить, к примеру, свой транспорт на Asio и "чистый" парсер того же протокола SCGI. Кстати, SCGI реально реализовать одному программисту всего за несколько дней в полном объеме (с нуля).

В любом случае, вашей работы теперь существенно меньше, а результат просто отличный - это и настоящий веб-сервер, через который не страшно смотреть в мир, и простое масштабирование, и высокая скорость работы. Это надежный проверенный способ, о котором много написано статей, в том числе и на Хабре (здесь и здесь).

В итоге, что мы имеем. При разработке веб-сервиса или веб-приложения на С++ нам нужен внешний хороший веб-сервер, а наша программа на С++ должна поддержать протокол взаимодействия с выбранным веб-сервером - FastCGI или SCGI. Двигаемся дальше: как помочь себе в создании веб-фреймворка на С++.

Как помочь себе в создании веб-фреймворка на С++

Я против написания веб-сервера, но, я "за" написание собственного веб-фреймворка. Вот только рутинную работу, нужно на кого-то переложить. А самим написать самое "вкусное" - тот же диспетчер URL, на шаблонах, с созданием своего DSL (как же без этого). А что значит - рутинная работа?

В начале я описывал, что делает веб-фреймворк, и что делает веб-сервер. Задачи этих двух компонентов не пересекаются (обычно). Веб-сервер мы взяли готовый (например, Nginx), а в своей программе поддержали SCGI, но работа на этом не закончилась. Нам нужно добавить себе в программу те возможности, которые обычно предоставляются веб-фреймворками. И здесь целесообразно использовать по максимум готовые решения.

Например, нам потребуется очень много парсить... В начале нужно парсить URL - задача абсолютно стандартная. Решений очень много - от могучих парсеров по типу Google's URL parsing library​, до минималистичных библиотек в один исходный файл. Затем, потребуется парсить передаваемые сообщения - это могут быть и данные форм, и Json-данные, и xml-данные. Но тут уже всё зависит от вашего сервиса. Вы, например, вольны отказаться от использования в REST-протоколе формата XML. И тогда парсер xml вам будет не нужен.

Вторая задача, это "общение" с базой данных. Как и в случае с парсерами, вам нужно будет выбрать решение на С++, исходя из потребностей. Если это "традиционная" база данных, возможно вам подойдет SQLAPI++ Library. Если вы используете какую-либо NoSQL базу, то существуют соответствующие клиенты на С++ и для них (например, для MongoDB).

Несколько слов про ORM. Задумайтесь, так ли он нужен в вашем микросервисе? ORM полезен в том случае, когда ваше приложение достаточно большое и совершает много простых запросов к базе. В этом случае, ORM скрывает всю эту "кухню". Но если ваш микросервис делает всего 10-20 различных запросов к базе, то не стоит создавать весь этот overhead под названием ORM. Это всегда влечет дополнительные накладные расходы. Кроме того, вы лишаете себя возможности оптимизировать каждый из запросов. Лучше напишите эти запросы один раз, и забудьте про ORM. ORM, особенно "самописный", скорее мешает в дальнейшем сопровождении кода, чем помогает. SQL знаком очень многим, а ORM - это обычная "вкусовщина".

Сейчас популярны так называемые "микрофреймворки". В них отсутствует такие возможности как: 

  • система рендеринга html-шаблонов
  • ORM, и в принципе средства доступа к базам данных
  • различные плагины авторизации, аутентификации, роли, работа с email

Всё это легко подключается, если есть такая необходимость. Этому принципу и надо следовать. То что действительно нужно, у вас уже есть - это полноценный безопасный быстрый веб-сервер со всеми его возможностями, библиотеки для разбора URL и JSON, какой-либо драйвер к базе данных. Возможно на этом и следует остановиться. Объединить все компоненты и вашу бизнес-логику в красивое быстрое решение на С++.

Итак. При разработке веб-сервиса или веб-приложения на С++ нам нужен хороший веб-сервер, набор небольших микро-библиотек для парсинга URL, и тех форматов данных, которые будут передаваться, и возможно драйвер для базы данных. Также нам нужно поддержать протокол  FastCGI или SCGI в своей программе на С++. Всё. 

Заключение

Решение, которое вы получите в итоге будет в первую очередь эффективным, надежным и гибким. Все компоненты, от веб-сервера до парсеров, вы сможете в любой момент заменить на более подходящие. Ваши бенчмарки будут показывать отличные результаты, лучше и выше, чем у POCO или cpp-netlib. При этом ваши возможности по масштабированию и расширению функционала не будут ничем ограничены. Нужно интегрировать систему рендеринга html - без проблем, внедрить LDAP аутентификацию - без проблем. Если ничего из этого не нужно - то ваш веб-сервис будет по-настоящему "микросервисом", и никакого лишнего кода.

Предложенный в этой статье подход не является чем-то новым, скорее наоборот. Спецификация на FastCGI появилась кажется еще в 90-х. В это время появились и веб-сервера, которые поддерживали протокол FastCGI. В подтверждение своих слов привожу ссылку на интересную лекцию от Яндекса: Архитектура высоконагруженного сервиса на примере бэкенда Яндекс.Store. Обратите особое внимание на 8-ой слайд. На нем вы увидите всё о чем было написано в этой статье. 

Спасибо за внимание!


P.S.: если будет интересно, я мог бы выложить на github пример небольшого микросервиса построенного по описанному в статье подходу (с конфигурационными файлами для какого-либо веб-сервера). Всё будет написано конечно на С++.