Первое знакомство с протоколом HTTP через написание простейшего Web сервера на Java
Думаю что не будет преувеличением утверждать, что знание и понимание сути протокола HTTP необходимо любому, кто решил сколь-нибудь серьезно заняться любым из направлений современной Web разработки. Мой личный опыт говорит о том, что понимание это приходит не сразу. Стыдно сказать, что были времена, когда слова GET и POST были для меня сродни магическим заклинаниям, а о существовании PUT, PATCH и DELETE я даже не подозревал.
Несколько месяцев назад помимо собственно разработки я занялся также преподаванием, и возник вопрос о том, как проще и понятнее рассказать о сути протокола HTTP будущим Java разработчикам. После нескольких дней возни и ряда неудачных попыток сделать презентацию возникла идея, а почему бы не написать простейший HTTP сервер на Java, потому как ни что так хорошо не объясняет суть протокола, как его простейшая, но работающая реализация.
Как оказалось сделать это совсем не сложно. Ниже привожу код, которого будет достаточно для корректного взаимодействия с любым браузером! Все что нам понадобится это ServerSocket и немного стандартного ввода-вывода.
Пробуем запустить этот код. Стоит отметить, что порт, для которого создается ServerSocket должен быть свободным. Если указанный порт занят, то нужно или его освободить, или использовать другой свободный порт.
Каждый раз, когда мы что-то вводим в адресную строку браузера и нажимаем Enter не происходит ничего иного, как отправка текста, начинающегося словом GET и заканчивающегося переводом строки. После слова GET через пробел следует путь к запрашиваемому документу на сервере. Попробуйте ввести в браузере http://localhost:8080/something и посмотреть, как изменится текст запроса в логе.
После того, как текст запроса полностью прочитан сервером, мы отправляем ему простейший ответ, структура которого довольно проста и аналогична структуре запроса. В первой строке версия протокола HTTP и код 200 OK, который сообщит браузеру о том, что запрос был успешно обработан (всем куда лучше знаком код 404, не правда ли 😉 ). Далее следует всего один заголовок Content-Type в котором передается информация о формате передаваемого документа (text/html) и его кодировке (charset=utf-8). После заголовка следует перевод строки (обязательное требование протокола HTTP) и собственно текст, который будет отображен в браузере.
На этом все! Разумеется это далеко не все, что нужно знать о протоколе HTTP и принципах разработки Web серверов, но мне бы не хотелось усложнять данный пример, т.к. главная его задача — продемонстрировать, простейшую коммуникацию по протоколу HTTP. В одном из следующих своих материалов постараюсь развить тему изучения протокола HTTP через его реализацию.
UPD. Гораздо более продвинутый пример подобного сервера можно найти в книге How Tomcat Works: A Guide to Developing Your Own Java Servlet Container by Paul Deck, Budi Kurniawan, глава 1 — Simple Web Server.
Пишем свой вэб сервер на C#
Скачать: [Загрузка не найдена] Скачать: [Загрузка не найдена]
Вступление
Мы будем писать свой простой WebServer, который сможет отправлять ответы на наиболее известные методы HTTP (GET и POST).
HTTP протокол
HTTP протокол связи между сервером и клиентом. Использует в качестве транспорта для отправки и получения протокол TCP/IP.
У HTTP есть несколько методов, но мы реализуем только два из них: это GET и POST.
GET
Что происходит, когда мы нажимаем в адресной строке браузера Enter? (Обычно мы не указываем порт через знак :, хотя для TCP/IP он необходим, так как он задан для HTTP по умолчанию, это порт 80. Если он отличен от 80 то его необходимо указывать через двоеточия)
Это GET запрос, который посылается от нашего браузера к серверу через TCP/IP. Это означает, что браузер запрашивает у сервер содержимое «/» корневой папки «atasoyweb.net».
Мы сами (или браузер) можем добавлять дополнительные заголовки. Но самый упрощенный вариант запроса выглядит так:
Упрощенный вариант POST запроса:
Ответы
Когда запрос поступает на сервер он обрабатывается и возвращается ответ с кодом состояния:
Это заголовок ответа. «200 OK» означает, что все в порядке, запрашиваемый контент, будет возвращен. Есть много кодов состояний, но мы будем использовать только 200, 501 и 404:
Типы контента
Сервер в своем ответе должен указать тип передаваемого контента. Есть много типов контента, их также называют MIME типы (многоцелевые расширения почты интернета).
Вот типы содержимого, которые мы будем использовать в нашем сервере: (Вы можете изменить код и добавить новые типы):
Если сервер укажет неверный тип контента, то содержимое будет неправильно истолковано. Например, если сервер посылает простой текст с помощью «image/png», то клиент попытается показать текст в виде изображения.
Многопоточность
Если мы хотим, чтобы наш сервер, был доступен нескольким клиентам одновременно, мы должны создавать новые потоки для каждого запроса. Таким образом, каждый поток обрабатывает один запрос и завершается после отдачи контента.
Реализация
Теперь мы готовы к реализации нашего простого вэб сервера. Сперва наперво определим переменные которые будем использовать:
Метод Start, запускающий наш сервер
Метод Stop, останавливающий сервер
Наиболее важная часть кода:
Ответы на коды статусов:
Метод, который будет отправлять ответы клиентам:
Свой http-сервер менее чем в 40 строк кода на libevent и C++11

Для себя как-то по работе я рассматривал libevent и libev. У каждой есть свои преимущества. Если же есть желание или потребность в скорой разработке небольшого http-сервера, то для меня большой интерес представляет libevent, а с учетом некоторых новшеств C++11 код становится намного компактнее и позволяет создать базовый http-сервер менее, чем в 40 строк.
Материал поста возможно будет полезен тем, кто еще не знаком с libevent и есть потребность в скором создании своего http-сервера, а так же материал может заинтересует людей, у которых такой потребности пока нет и даже если они уже имели опыт создания подобного, интересно узнать их мнение и опыт. А так как пост не содержит ничего принципиально нового, то может быть использован как материал для начала работы в данном направлении, а следовательно попробую поставить пометку «обучающий материал».
Чем хороша libevent в отличии от, например, libev и boost.asio, так это тем, что она имеет свой встроенный http-сервер, и некоторую абстракцию для работы с буферами. А так же имеет немалый набор вспомогательных функций. Можно HTTP протокол и самому разобрать, написав простенький конечный автомат или еще каким-нибудь методом. При работе с libevent это все уже есть. Эта такая приятная плюшка, а можно и на более низкий уровень спуститься и писать свой же парсер для HTTP, при этом работу с сокетами сделать на libevent. Уровень детализации у библиотеки мне понравился тем, что если есть желание сделать что-то быстро, то можно найти в ней более высокоуровневый интерфейс, который как правило менее гибок. При появлении больших потребностей можно постепенно спускаться уровень за уровнем все ниже и ниже. Библиотека позволяет делать многие вещи: асинхронный ввод-вывод, работу с сетью, работа с таймерами, rpc, т. д; можно с ее помощью создавать как серверное, так и клиентское ПО.
Зачем?
Создание собственного небольшого http-сервера может быть обусловлено для каждого его собственными потребностями, желанием или не желанием использовать полнофункциональные готовые сервера по той или иной причине. Предположим у Вас есть некоторое серверное ПО, которое работает по какому-то своему протоколу и решает некоторые задачи и у Вас появилась потребность выдать некоторое API для данного ПО через HTTP протокол. Возможно всего несколько небольших функций по настройке сервера и получению его текущего состояния по протоколу HTTP. Например, организовав обработку запросов GET с параметрами и отдавать небольшой xml с ответом или еще в каком-то формате. В таком случае можно с малыми трудозатратами создать свой http-сервер, который и будет интерфейсом для основного Вашего серверного ПО. Кроме этого если есть необходимость создать свой небольшой специфичный сервис по раздаче какого-то набора файлов или даже создать свое собственное веб-приложение, то можно так же воспользоваться таким самописным небольшим сервером. В общем можно воспользоваться как для построения самодостаточного серверного ПО, так и для создания вспомогательных сервисов в рамках более крупных систем.
Простой http-сервер менее чем в 40 строк
Получилось менее 40 строк, которые способны обрабатывать http-запросы, отдавая в ответ строку «Hello World», а если заменить функцию evbuffer_add_printf на evbuffer_add_file, то можно отправлять файлы. Можно такой сервер назвать базовой комплектацией. Любой авто дилер или риэлтор в большинстве своем мечтают, чтобы их авто и квартиры никогда и ни при каких условиях не уходили в базовой комплектации, а только с дополнительными опциями. А вот нужны ли такие опции потребителю и в каком объеме…
Что может дать такая базовая комплектация по быстродействию можно проверить с помощью утилиты ab для *nix систем с небольшой вариацией параметров.
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 2.289 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 50000
Total transferred: 8500000 bytes
HTML transferred: 3200000 bytes
Requests per second: 21843.76 [#/sec] (mean)
Time per request: 45.780 [ms] (mean)
Time per request: 0.046 [ms] (mean, across all concurrent requests)
Transfer rate: 3626.41 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 3 48.6 0 1001
Processing: 17 42 9.0 43 93
Waiting: 17 42 9.0 43 93
Total: 19 45 49.7 43 1053
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 5.004 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Total transferred: 6300000 bytes
HTML transferred: 3200000 bytes
Requests per second: 9992.34 [#/sec] (mean)
Time per request: 100.077 [ms] (mean)
Time per request: 0.100 [ms] (mean, across all concurrent requests)
Transfer rate: 1229.53 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 61 214.1 20 3028
Processing: 7 34 17.6 31 277
Waiting: 6 28 16.9 25 267
Total: 17 95 219.5 50 3055
Тест проводился на уже не совсем новом ноутбуке (2 ядра, 4Гб оперативной памяти) под управлением 32-х битной операционной системы Ubuntu 12.10.
Многопоточный http-сервер
Нужна ли многопоточность? Вопрос риторический… Можно все IO и в одном потоке организовать, а запросы складывать в очередь и разгребать ее в несколько потоков. В таком случае вышеприведенный сервер можно просто дополнить очередью и пулом потоков для обработки и больше ничего городить не стоит. Если же есть желание или потребность построить многопоточный сервер, то он будет немного длиннее предыдущего, однако ненамного. C++11 с его умными указателями позволяют хорошо реализовывать RAII, как это было приведено с std::unique_ptr в примере выше, а также наличие лямбда-функций немного сокращает код.
Пример многопоточного сервера по своей идеологии аналогичен однопоточному, а некоторые особенности, связанные с многопоточностью его увеличивают примерно в 2 раза по объему кода. Восемьдесят с небольшим строк кода для многопоточного http-сервера на C++ — это не так и много.
В коде можно заметить, что каждый поток создается после некоторого внесенного ожидания. Это небольшой хак, который уже будет исправлен в конечной версии сервера. Пока можно сказать только, что если этого не сделать, то потоки надо будет как-то синхронизировать, чтобы они отработали «странный шаг» по созданию и привязке сокета. Для упрощения пока пусть останется такой хак. Так же в приведенном коде лямбда-функция может показаться спорным решением. Лямбды могут быть хорошим решением при использовании, например, в качестве некоторого предиката при работе со стандартными алгоритмами. В то же время можно задуматься об их использовании и при написании более больших фрагментов кода. В примере выше можно было все вынести в обычную функцию, передать все нужные параметры и получить код в стиле C++03. В то же время использование лямбды дало сокращение в объеме кода. На мой взгляд, когда код невелик, то лямбды могут вполне хорошо в него вписывать даже с не самым коротким ее содержанием и не влиять пагубно на качество кода, конечно не стоит вдаваться в крайности и вспоминать студенческие будни с написанием лабораторной работы в 700 строк в единственной функции main.
Тестирование многопоточного сервера проведено с теми же параметрами, что и предыдущего примера.
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 1.576 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 50000
Total transferred: 8500000 bytes
HTML transferred: 3200000 bytes
Requests per second: 31717.96 [#/sec] (mean)
Time per request: 31.528 [ms] (mean)
Time per request: 0.032 [ms] (mean, across all concurrent requests)
Transfer rate: 5265.68 [Kbytes/sec] received
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 3.685 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Total transferred: 6300000 bytes
HTML transferred: 3200000 bytes
Requests per second: 13568.41 [#/sec] (mean)
Time per request: 73.701 [ms] (mean)
Time per request: 0.074 [ms] (mean, across all concurrent requests)
Transfer rate: 1669.55 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 36 117.2 23 1033
Processing: 3 37 10.0 37 247
Waiting: 3 30 8.7 30 242
Total: 9 73 118.8 61 1089
Конечный вариант сервера
Базовая комплектация приведена, комплектация с небольшим набором опций так же есть. Теперь очередь подошла и для создания чего-то более полезного и функционального, а так же с небольшим тюнингом.
Весьма минимальный объем кода для http-сервера на C++. За все есть плата. И в данном случае такая простота клиентского кода по созданию сервера, оплачена более длинной реализацией, скрытой в предлагаемой обертке над libevent. На самом же деле ненамного увеличилась реализация. Чуть ниже ее фрагменты будут описаны.
Данный интерфейс позволяет получать из входящего запроса его тип, некоторые атрибуты (заголовки), размер тела запроса и само тело запроса при его наличии, а так же формировать ответ с возможностью задать атрибуты (заголовки), код завершения обработки запроса и тело ответа (в данной реализации имеются методы для передачи строки, некоторого буфера или файла в ответ). Каждый метод в его реализации может генерировать исключение типа HttpRequestException.
Если еще раз взглянуть на код сервера, то в коде обработки запросов можно заметить такие строки:
Это формирование заголовка ответа, а данном примере задаются такие поля заголовка, как «Content-Type» и «Server». Не смотря на то, что libevent имеет достаточно широкий функционал, выходящий далеко за потребности HTTP, списка констант полей заголовков в ней нет; есть только неполный список кодов возврата (наиболее часто используемых). Чтобы не возиться со строками, определяющими поля заголовков (например, во избежании опечаток в пользовательском коде), все константы определены уже в предлагаемой обертке над libevent.
Почти аналогичным образом определены и константы для задания типа контента; имеют небольшую модификацию. Было желание реализовать поиск типа контента по расширению файла для удобства при отправке файлов в ответ на запрос.
При желании что-то получить из входящего запроса, например, с какого хоста и с какой страницы был осуществлен переход на запрашиваемый ресурс и, например, есть ли у пользователя «печеньки», можно это все получить из заголовка входящего запроса таким образом:
Аналогичным образом в ответе можно, например, установить пользователю некоторые Cookie, по которым в дальнейшем работать с его сессией и отслеживать при желании его блуждания по Вашему ресурсу (пример работы с заголовками ответа приведен в кода сервера).
Если же есть желание организовать некоторое свое API через HTTP, то это так же легко сделать. Предположим надо создать методы: открытие сессии, получение статистической информации о сервере и закрытие сессии. Пусть для этого строки запроса к Вашему серверу будут выглядеть примерно так:
Ответом на эти строки запросов сервер пользователя может сгенерировать какой-то ответ, например, в формате xml. Это дело разработчика сервера. А вот как работать с такими запросами, получать из них параметры приведено ниже:
Один из путей для примеров выше будет таким /service/login/OpenSession, а параметры это карта из переданных пар ключ / значение. Тип карты параметров:
После разбора всего того, что можно реализовать с помощью предлагаемой конечной версии обертки над libevent можно заглянуть и под капот этой самой обертки.
Функцию обработки запросов можно посмотреть в полной версии, скачав исходные файлы примеров, она стала немного больше, чем в ранее приведенных примерах, и перестала претендовать на лямбду без потери читаемости кода. Так же не стал приводить реализацию интерфейса IHttpRequest, так как она мало интересна своей рутинной работой с буфером libevent. А в остальном если посмотреть на код итоговой версии, он не сильно-то изменился. Небольшая модификация и добавилось немного «тюнинга».
Сервер пользователя не обязан обрабатывать все типы http-запросов. Можно задать список типов запросов, которые сервер должен обрабатывать и для этого libevent имеет функцию evhttp_set_allowed_methods (а по умолчанию обертка задает только тип запросов GET). При задании списка обрабатываемых запросов на все остальные libevent сама будет сообщать о невозможности выполнения такого запроса, тем самым избавив пользователя от дополнительных проверок.
Пытливость ума она бывает разной: нацеленной на созидание и на разрушение. От разрушительной пытливости ума с желанием «завалить» сервер послав ему какой-то непомерно для него большой заголовок http-пакета или сформировав большое тело запроса можно так же проактивно защититься функциями evhttp_set_max_headers_size и evhttp_set_max_body_size. Конечно же отправка больших запросов может быть вызвана не только недобрыми помыслами, а так же и иными причинами. Приведенные методы позволят немного сократить нежелательные аварийные завершения Вашего сервера. Возможно еще что-то предусмотреть, а в остальном уже можно реагировать реактивно, что как правило и происходит…
В конце приведу финальную версию, которая отрабатывает запросы GET (отдает файлы из указанной директории) и выводит на экран с какого хоста был сделан запрос и с какой страницы был осуществлен переход на ресурс, обрабатываемый сервером.
Заключение
Server Software: test
Server Hostname: test
Server Port: 8888
Document Path: /libevent_test_http_srv.zip
Document Length: 23756 bytes
Concurrency Level: 1000
Time taken for tests: 10.012 seconds
Complete requests: 2293
Failed requests: 0
Write errors: 0
Keep-Alive requests: 2293
Total transferred: 60628847 bytes
HTML transferred: 60328370 bytes
Requests per second: 229.02 [#/sec] (mean)
Time per request: 4366.365 [ms] (mean)
Time per request: 4.366 [ms] (mean, across all concurrent requests)
Transfer rate: 5913.65 [Kbytes/sec] received
Две с небольшим тысячи обработанных запросов на получение архива с исходными файлами примеров поста за десять секунд на уже весьма скромной конфигурации железа…
nxweb – HTTP сервер для приложений на Си
Под катом подробная информация о проекте из интервью с разработчиком.
Q: Расскажи о себе, чем занимаешься, где работаешь?
Yaroslav: Я давно занимаюсь разработкой софта. Компания Nexoft. Пишу в основном на Java, но и C/C++ периодически вспоминаю, когда нужно что-то скоростное сделать. Например, движок баннерокрутилки для сайта с высокой посещаемостью.
Q: Значит nxweb создавался с целью – крутить баннеры?
Yaroslav: Ну, не только ради баннеров, но это одна из первых задач. Конкретная задача, по крайней мере, не абстрактная.
Q: Высокая посещаемость – это сколько запросов в секунду?
Yaroslav: Сейчас приходит около 100 запросов в секунду. Часть из них отправляется на Tomcat. Но в каждую HTML страницу вставляется до 20 кодов баннеров. Серверу, в принципе, тяжело, поэтому хочется простой задачей отдачи коротких HTML фрагментов сильно его не обременять. Может, конечно, и на перле это всё работало бы, но мы легких путей не ищем.
Собственно, движок такой у меня был давно, написан как модуль для Apache, но в последнее время все проекты переношу на nginx.
Вообще писать модули под Apache и nginx – не самое приятное занятие. Там куча мишуры, навязанной заботой о переносимости. К тому же, они многопроцессны, а это отдельная беда. С потоками (threads) все намного проще. Общая память – бесценна. Я привык к Java, там именно так. Полез внутрь nginx, уж очень все там громоздко, плюс, опять же, shared memory и пр.
Решил попробовать реализовать свое приложение на микро-движке типа Mongoose, натыкался на него в сети ранее. Стал копать, нашел еще несколько альтернатив, в т.ч. G-WAN. Очень всё в нем понравилось, кроме closed-source. Я пробовал G-WAN именно как сервер приложений, а не как отгрузчик статики. Но довольно быстро стал натыкаться на глюки, найти и исправить которые не представляется возможным из-за закрытости кода.
Итак, написал крутилку на Mongoose, стал тестировать быстродействие и обнаружил, что оно уж больно низкое по сравнению с G-WAN. Это уже потом я догадался 2500 тредов запустить, а поначалу думал, что 8-16 хватит. С таким количеством там вообще никак.
Q: Потому что mongoose использует один OS поток на каждый запрос?
Yaroslav: Да, собственно, 2500 тредов – тоже не решение. Памяти они жрут тонну, и, опять же, появится 2501-й запрос – и привет…
Стал копать глубже, нашел microhttpd и libevent. По производительности, если сравнивать с G-WAN, они выглядели бледно. Пока я со всем этим разбирался, копался в исходниках, пришло понимание, что написать веб-сервер не так уж и сложно. В mongoose, например, и так весь HTTP ответ надо вручную писать разработчику модуля.
libevent выглядел наиболее обещающим, но однопоточным. Решил переделать то же на libev (облегченная альтернатива libevent). Так родился nxweb. Главная первоначальная цель – это веб-приложения на C, а не полнофункциональный сервер. Если уж разрабатываешь что-то на C, значит хочешь, чтобы оно работало предельно быстро, а значит, и платформа должна быть скоростной. А то напишешь под mongoose, и, спрашивается, ради чего старался, если он все на тормозах спустит.
Конкурировать у меня ни с кем цели не было. Хотелось приблизиться к G-WAN, как к эталону скорости. Но стоило опубликовать проект, как тут же пошел поток писем. Стали все меня подзадоривать. Вот и пришлось еще несколько дней потратить, попыхтеть, чтобы G-WAN таки обогнать.
Обогнал я его или нет, пока не ясно. На своем компе я его обогнал. Как говорится, на своей территории. Опять же, наверное не во всех режимах.
Кстати, nginx очень порадовал. Скорость его HTTP стека выше, чем у G-WAN (если правильно его настроить, конечно). Просто nginx не кеширует файлы в памяти, если его не просят. С nginx вообще конкурировать бессмысленно. Даже удивляет, как такой функциональный сервер обеспечивает столь высокое быстродействие. Я тут убедился, что элементарное использование sprintf вместо strcat способно посадить быстродействие на 5-10 тыс. запросов в секунду. Т.е. бой идет уже за каждую лишнюю инструкцию CPU.
Q: Хорошо, расскажи об архитектуре nxweb вкратце.
Yaroslav: Основной поток слушает сокет и рассовывает поступающие коннекты по сетевым потокам (у каждого своя очередь, чтобы избежать мьютексов). Сетевые потоки (их имеет смысл запускать по одному на ядро процессора) обеспечивают HTTP протокол. Декодируют запрос, передают его модулю-обработчику. Воркеры – вещь опциональная, задуманы исключительно для поддержки медленных обработчиков, чтобы они не стопорили сетевой поток.
Сейчас у модулей есть коллбек, который вызывается сервером перед входом в основной цикл. Также есть файл main.c, который можно убрать или заменить. Его функция — исключительно сервисная: открыть лог-файл и передать управление в _nxweb_main(). Он также умеет запускать демона. Причем двойного: один демон следит за тем, чтобы второй (где собственно и крутится nxweb) не остановился. Если внутренний демон падает, то первый перезапускает его автоматически. При желании main.c можно заменить на любой другой. Главное, он должен вызвать _nxweb_main().
Q: Почему каждый сетевой поток не делает accept цикл? Тогда OS сможет распределять коннекты и можно сделать несколько accept-ов параллельно на разных ядрах.
Yaroslav: Я пробовал оба варианта, в т.ч. accept сетевым потоком без участия основного. Разницы в быстродействии не обнаружил, вернулся к единому акцептору, так как мне показалось, что это снижает процент ошибок соединения. У nginx и G-WAN часть соединений подвисает, и нагрузки не несет. См. мой комментарий насчет real concurrency на странице бенчмарок.
Q: Понятно. Первая версия использовала libev, но сейчас в wiki написано, что зависимости от libev нет. Что изменилось?
Yaroslav: Я написал свой libev. Это как раз то, что я имел в виду под «попыхтеть, чтобы обогнать G-WAN». Он заточен под мои нужды и самостоятельным проектом не является. Другими словами, я теперь работаю с epoll напрямую. nxweb совершенно не переносим. Он работает только на Linux да еще на ядре >= 2.6.22. Я использую Edge-Triggered epoll, который libev не поддерживает из-за его непереносимости. Да, libev – супер-классная вещь, я бы на нем и остановился, но народ захотел быстрее. Вот пришлось помучиться.
Q: Именно Edge-Triggered позволил получить последний прирост?
Yaroslav: Не только. Ещё пришлось над кодом поработать. В частности, исключить sprintf, новая система резервирования памяти и т.п. Есть мысль прикрутить всё это назад к libev. Может и не хуже будет, но не знаю, соберусь ли. Сделать переносимый код у меня задача не стоит. Хостинг практически всегда на Linux. А libev – это лишняя зависимость, которую надо инсталлировать, чтобы собрать nxweb. тоже минус.
Q: Игорь Сысоев и авторы libev утверждают, что у epoll есть масса проблем. Ты уже сталкивался с ними?
Yaroslav: Есть надежда, что эти проблемы в современном ядре уже вычищены. Всё-таки много лет прошло. Я тестирую на 2.6.32 и 3.0.0, с проблемами не сталкивался.
Q: Что насчёт резервирования памяти? Ты аллоцируешь какой-то большой пул, чтобы делать меньше сисколов?
Yaroslav: Free-list для коннекшенов и свой аналог obstack для формирования ответов.
Q: Ты используешь собственную реализацию парсера HTTP или брал что-то готовое?
Yaroslav: Реализация собственная. Безусловно, я не поддерживаю абсолютно все нюансы HTTP протокола. Хотя, например, 100-continue или chunked-encoding у меня есть. Опять же, отгрузка статики – это дело модуля. Ядро парсит запрос, вызывает обработчик, получает ответ, оборачивает его в HTTP и отправляет клиенту. Т.е. всякие там ETag и Range – это задача модуля. Сейчас мой модуль sendfile.c формирует лишь основные заголовки, хотя наверное скоро сделаю поддержку Range и If-Modified-Since.
Да, наверное есть готовые парсеры, проверенные на ошибки. Но, с другой стороны, в собственном парсере я быстрее ошибку отловлю, если мне о ней сообщат, чем в чужом. Тут реально счёт идёт на CPU-инструкции. А все самостоятельные проекты парсеров настолько обстоятельны, что ни о какой производительности с ними и речи быть не может.
Q: С другой стороны, я бы не дал никому кроме nginx слушать внешний порт боевого сервера. А за ним все некорректные запросы уже отфильтрованы.
Yaroslav: На данный момент (версия 2.0) nxweb не очень готов к тому, чтобы выставлять его наружу. В основном потому, что реальные приложения состоят не только из C-скриптов. Нужно подвязывать и Java и статику и все остальное. Если только речь не идет о реализации каких-нибудь чатов, где важно держать много тысяч одновременных соединений.
Q: Есть ряд библиотек, которые делают в Си userland потоки, одна из них libtask, используется в mongrel2. У каждой корутины свой стек, поэтому библиотека содержит несколько инструкций на ассемблере для переключения стека. Это позволяет писать обычный последовательный код, без колбеков типа on_request, on_success…
Yaroslav: Да, про корутины слышал. Но если я уже написал все колбеками, то выигрыша в производительности переходом на корутины я не получу. Только лишняя память уйдет на хранение стеков. Да еще и проблемы с gdb – тоже не самое приятное. Тут дело вкуса и привычки. К корутинам надо привыкать. Это особая концепция.
Q: Существенный выигрыш может быть в поддерживаемости кода обработчиков, когда люди захотят не просто request-response, а, например, sleep в середине или в базу сходить. Для простой баннерокрутилки, конечно, никакого смысла в них нет.
Yaroslav: Для слипов и в базу ходить – как раз для этого я и предусмотрел воркеров. И проверил первым делом. Если сконфигурировано 100 воркеров и каждый воркер делает sleep 1 сек, то сервер ровно и без запинок отрабатывает 100 запросов в секунду. Ни G-WAN, ни nginx с этим не справляются. Они либо выдают 3-4 запроса в секунду, либо тупо виснут.
Хотя, при острой необходимости ходить в базу, можно написать для этой цели неблокирующий адаптер. Из сервлета будет один вызов – поставить запрос к БД в очередь и зарегистрировать колбек. После этого возврат и дальше работает колбек. В принципе, тут для удобства написания можно подумать и о корутинах.
Q: Как бы ты описал текущий статус nxweb? Стабильность, ошибки?
Yaroslav: Ну, я уже прикрутил его в качестве бекенда к сайту с довольно большой посещаемостью, т.е. запустил в продакшн. Прошел уже месяц, и ни единого перезапуска/сбоя. Хотя, бекенд – это не самое жесткое боевое крещение.
Полагаю, что статус – альфа. В частности, далеко не все функции nxweb тщательно проверены. Например, chunked request encoding. На тестовых примерах работает, но мало ли что всплывет. Также неясно, какова будет стабильность при некорректном поведении клиента. Например, запросы с ошибками могут спровоцировать сбой.
Еще есть проблема на текущем этапе – я довольно активно меняю h-файлы, структуры данных и пр.
Q: Лично мне как потенциальному разработчику под nxweb нужна возможность полностью управлять обработкой всех запросов и толстая библиотека вспомогательных функций (это уместно в виде модулей), типа сравни урл, sendfile, проставь заголовок, распарси куки, запроксируй на бекенд. Именно так я представляю себе программирование критических участков на nxweb.
Yaroslav: Надо сказать, что это примерно и есть мой подход. Создать базовые функции, с помощью которых можно конфигурировать работу сервера. Но конфигурировать на С, а не с помощью какого-то конфиг-файла. Потому что создать аналог конфига, который есть у nginx, это задача очень тяжкая.
Q: Следующий вопрос, скорее для галочки, что ты думаешь по поводу поддержки Windows?
Yaroslav: На 99% исключено. Я вообще холодно смотрю на переносимость кода. По сути, именно из-за забот о переносимости внутренний интерфейс nginx (да и apache) так заморочен. Приходится отказываться от современных технологий в угоду переносимости кода. Я пока хочу сконцентрироваться только на Linux.
Q: В G-WAN есть довольно удобная фича – автокомпиляция приложений. Конечно, тут есть масса открытых вопросов, типа безопасности, флагов компилятора, обновления на лету. Планируешь ли ты добавить такую же фичу в nxweb?
Yaroslav: Да, мне она очень понравилась. Но не планирую пока. Лишний гемор, который никак не приближает меня к тому, чтобы начать полноценно использовать nxweb для своих нужд.
Q: Хорошо, более насущный вопрос, в какой форме ты планируешь распространять nxweb? Дерево исходников? Библиотека? Статическая/динамическая?
Yaroslav: Пока только в том, каком уже распространяю. Открытый репозитарий, откуда можно скачать исходник и выполнить make. На данный момент полная компиляция проекта занимает секунды. Смысла городить библиотеки пока не вижу.
Q: Допустим, я хочу написать своё hello world приложение. Мои действия?
Yaroslav: hello.c – это шаблон модуля. Далее, в Makefile есть небольшие комментарии о подключении своих модулей. Там же есть специальные переменные SRC_MODULES и INC_MODULES. Кроме того надо добавить ссылку на свой модуль в modules.c.
Согласен, что процесс работы с модулями надо как-то получше обустроить, особенно если их станет больше.
Q: Еще нужна какая-то документация, описание возможных модулей. Примеры решения типичных задач.
Yaroslav: Пока есть пример hello.c, по нему многое понятно. А в остальном, сорри, только исходный код. На bitbucket есть wiki, там какая-то часть документации.
Q: Дальше – где общаться заинтересованным разработчикам. Рассылка/форум. Какое-то место для вопросов и ответов, с историей и поиском.
Yaroslav: На днях я создал две гугл-группы: nxweb и nxweb-ru.
Q: Когда будут стабильные версии, заморозка API?
Yaroslav: Ой, не знаю… Заморозка API нескоро. Хотя, надо сказать, между 1-й и 2-й версией изменения в модулях были минимальны. При том, что внутренности сервера были все переделаны.
Q: Это хороший знак. Какие планы по наращиванию библиотеки полезностей для приложений? Конкретнее: логирование, движок шаблонов, работа с заголовками, куками.
Yaroslav: Заголовки и куки уже парсятся в таблицы и их можно извлекать по именам. Не знаю, что еще с ними можно сделать. access_log – пока не нужен был, в принципе он не сложен, однако существенно замедлит работу. Библиотека полезностей для начала пополнится функциями проксирования, я думаю.
Q: access log как раз не нужен, нужно логирование приложения.
Q: Дальше, по планам. Было бы здорово конфигурировать количество тредов автоматически. Я вижу тут два направления: 1) при запуске определить количество ядер и поставить столько потоков, 2) порождать новые потоки пока LA ниже заданного значения, ну и с верхним ограничением, конечно.
Yaroslav: Автоконфигурация – большой вопрос. nxweb в принципе пока не конфигурируется иначе как путем перекомпиляции. Есть ли смысл делать один параметр автоматически конфигурируемым, не знаю. На самом деле несложно запускать при старте любое автоматически определенное число тредов. Чуть сложнее дозапускать или останавливать треды в процессе, хотя тоже реализуемо.
Из практических целей я пока вижу реализацию проксирования. В первую очередь на Java. Это то, с чем я работаю. Потом – SSL, управляемое кеширование. Может быть, интерфейс к БД.
Сейчас запрос и ответ полностью буфферизуются. Это может быть плохо для некоторых задач (например, upload файла). Поэтому я думаю ввести в модулях понятие обработчиков частично полученных данных, а также отправку данных по частям.
Q: Проксировать и SSL, вроде nginx умеет. Не снаружи же ставить nxweb.
Yaroslav: Если не ставить его снаружи, то всё преимущетсво скорости теряется. Я вчера протестировал nxweb позади nginx: 25 тыс. запросов в секунду. Притом что сам он отдает 160 тыс., да и nginx 130 тыс. умеет. Это для текущей стабильной версии nginx 1.0, у которого нет keep-alive для бекендов. В версии 1.1 получается 50 тыс. запросов в секунду – намного быстрее, но, все равно, более, чем трехкратное замедление.
Yaroslav: Полагаю, что с новыми версиями nxweb появятся и новые use case-ы.




