OpenResty — это улучшенный NGINX с поддержкой динамических запросов и ответов
API7.ai
October 23, 2022
После предыдущего введения вы должны понять концепцию OpenResty и как её изучать. В этой статье мы рассмотрим, как OpenResty обрабатывает запросы и ответы клиентов.
Хотя OpenResty является веб-сервером на основе NGINX, он принципиально отличается от NGINX: NGINX управляется статическими конфигурационными файлами, в то время как OpenResty управляется Lua API, что обеспечивает большую гибкость и программируемость.
Позвольте мне рассказать о преимуществах Lua API.
Категории API
Во-первых, нам нужно знать, что API OpenResty разделены на следующие основные категории.
- Обработка запросов и ответов.
- Связанные с SSL.
- Общий словарь (shared dict).
- Cosocket.
- Обработка трафика на четырёх уровнях.
- Процессы и воркеры.
- Доступ к переменным и конфигурации NGINX.
- Строки, время, кодирование и другие общие функции и т.д.
Здесь я предлагаю вам также открыть документацию Lua API OpenResty и сверить её с списком API, чтобы увидеть, насколько вы можете связать это с данной категорией.
API OpenResty существуют не только в проекте lua-nginx-module, но и в проекте lua-resty-core, такие как ngx.ssl, ngx.base64, ngx.errlog, ngx.process, ngx.re.split, ngx.resp.add_header, ngx.balancer, ngx.semaphore, ngx.ocsp и другие API.
Для API, которые не находятся в проекте lua-nginx-module, вам нужно отдельно их подключать для использования. Например, если вы хотите использовать функцию split, вам нужно вызвать её следующим образом.
$ resty -e 'local ngx_re = require "ngx.re" local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5}) print(res) '
Конечно, это может вас запутать: в проекте lua-nginx-module есть несколько API, начинающихся с ngx.re.sub, ngx.re.find и т.д. Почему только API ngx.re.split требует предварительного подключения перед использованием?
Как мы упоминали в предыдущей главе о lua-resty-core, новые API OpenResty реализованы в репозитории lua-rety-core с использованием FFI, поэтому неизбежно возникает ощущение фрагментации. Я надеюсь, что в будущем эта проблема будет решена путём объединения проектов lua-nginx-module и lua-resty-core.
Запрос
Теперь давайте рассмотрим, как OpenResty обрабатывает запросы и ответы клиентов. Сначала рассмотрим API для обработки запросов, но существует более 20 API, начинающихся с ngx.req, так с чего же начать?
Мы знаем, что HTTP-запросы состоят из трёх частей: строка запроса, заголовки запроса и тело запроса, поэтому я представлю API в этих трёх частях.
Строка запроса
Первая часть — это строка запроса, которая содержит метод запроса, URI и версию HTTP-протокола. В NGINX вы можете получить это значение с помощью встроенной переменной, а в OpenResty это соответствует API ngx.var.*. Давайте рассмотрим два примера.
- Встроенная переменная
$scheme, которая представляет название протокола в NGINX, это либоhttp, либоhttps; в OpenResty вы можете использоватьngx.var.schemeдля получения того же значения. $request_methodпредставляет метод запроса, напримерGET,POSTи т.д.; в OpenResty вы можете получить то же значение черезngx.var.request_method.
Вы можете посетить официальную документацию NGINX, чтобы получить полный список встроенных переменных NGINX: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.
Возникает вопрос: зачем OpenResty предоставляет отдельный API для строки запроса, если вы можете получить данные в строке запроса, возвращая значение переменной, такой как ngx.var.*?
Результат содержит множество факторов:
- Во-первых, не рекомендуется многократно читать
ngx.varиз-за низкой производительности. - Во-вторых, из соображений удобства для программы,
ngx.varвозвращает строку, а не объект Lua. Это затрудняет обработку, когда вы получаетеargs, которые могут возвращать несколько значений. - В-третьих, с точки зрения гибкости, большинство
ngx.varдоступны только для чтения, и только несколько переменных доступны для записи, такие как$argsиlimit_rate. Однако нам часто нужно изменять метод, URI и args.
Поэтому OpenResty предоставляет несколько API, специально предназначенных для манипуляции строкой запроса, которые могут перезаписывать строку запроса для последующих операций, таких как перенаправление.
Давайте рассмотрим, как получить номер версии HTTP-протокола через API. API OpenResty ngx.req.http_version делает то же самое, что и переменная NGINX $server_protocol: возвращает номер версии HTTP-протокола. Однако возвращаемое значение этого API — не строка, а числовой формат, возможные значения которого — 2.0, 1.0, 1.1 и 0.9. Если результат выходит за пределы этих значений, возвращается nil.
Теперь рассмотрим метод получения запроса в строке запроса. Как упоминалось, роль и возвращаемое значение ngx.req.get_method и переменной NGINX $request_method одинаковы: в строковом формате.
Однако формат параметра текущего метода HTTP-запроса ngx.req.set_method — не строка, а встроенные числовые константы. Например, следующий код перезаписывает метод запроса на POST.
ngx.req.set_method(ngx.HTTP_POST)
Чтобы убедиться, что встроенная константа ngx.HTTP_POST действительно является числом, а не строкой, вы можете вывести её значение и увидеть, что вывод будет 8.
resty -e 'print(ngx.HTTP_POST)'
Таким образом, возвращаемое значение метода get — строка, а входное значение метода set — число. Это нормально, когда метод set передаёт запутанное значение, так как API может завершиться с ошибкой и выдать ошибку 500. Однако в следующей логике проверки:
if (ngx.req.get_method() == ngx.HTTP_POST) then -- do something end
Такой код работает нормально, не выдаёт ошибок, и его трудно обнаружить даже во время проверки кода. Я сам однажды допустил подобную ошибку и до сих пор помню её: я уже прошёл два раунда проверки кода и неполные тестовые случаи, чтобы попытаться её покрыть. В конечном итоге аномалия в рабочей среде привела меня к этой проблеме.
Нет практического способа решить такую проблему, кроме как быть более внимательным или добавить ещё один уровень инкапсуляции. Когда вы проектируете свои бизнес-API, вы также можете учитывать и поддерживать согласованный формат параметров методов get и set, даже если это потребует некоторого снижения производительности.
Кроме того, среди методов для перезаписи строки запроса есть два API, ngx.req.set_uri и ngx.req.set_uri_args, которые можно использовать для перезаписи URI и args. Давайте рассмотрим эту конфигурацию NGINX.
rewrite ^ /foo?a=3? break;
Итак, как мы можем решить это с помощью эквивалентного Lua API? Ответ — следующие две строки кода.
ngx.req.set_uri_args("a=3") ngx.req.set_uri("/foo")
Если вы читали официальную документацию, вы обнаружите, что ngx.req.set_uri имеет второй параметр: jump, который по умолчанию равен "false". Если вы установите его как "true", это будет эквивалентно установке флага команды rewrite на last, а не на break, как в примере выше.
Однако мне не очень нравится конфигурация флагов команды rewrite, так как она нечитаема и не распознаваема, и гораздо менее интуитивна и поддерживаема, чем код.
Заголовки запроса
Как мы знаем, заголовки HTTP-запросов имеют формат ключ : значение, например:
Accept: text/css,*/*;q=0.1 Accept-Encoding: gzip, deflate, br
В OpenResty вы можете использовать ngx.req.get_headers для разбора и получения заголовков запроса, а тип возвращаемого значения — таблица.
local h, err = ngx.req.get_headers() if err == "truncated" then -- можно выбрать игнорирование или отклонение текущего запроса end for k, v in pairs(h) do ... end
По умолчанию возвращаются первые 100 заголовков. Если количество превышает 100, будет выдана ошибка truncated, и разработчик должен решить, как с этим справиться. Вам может быть интересно, почему это сделано именно так, и я упомяну об этом позже в разделе о уязвимостях безопасности.
Однако следует отметить, что OpenResty не предоставляет специального API для получения определённого заголовка запроса, то есть нет формы ngx.req.header['host']. Если у вас есть такая потребность, вам нужно полагаться на переменную NGINX $http_xxx, чтобы достичь этого. Затем в OpenResty вы можете получить её через ngx.var.http_xxx.
Теперь давайте рассмотрим, как мы должны перезаписывать и удалять заголовки запроса. API для обеих операций довольно интуитивны:
ngx.req.set_header("Content-Type", "text/css") ngx.req.clear_header("Content-Type")
Конечно, официальная документация также упоминает другие способы удаления заголовка запроса, такие как установка значения заголовка на nil и т.д. Однако я всё же рекомендую использовать clear_header для единообразия и ясности кода.
Тело запроса
Наконец, рассмотрим тело запроса. По соображениям производительности OpenResty не читает тело запроса активно, если вы не включили директиву lua_need_request_body в nginx.conf. Кроме того, для больших тел запросов OpenResty сохраняет содержимое во временный файл на диске, поэтому весь процесс чтения тела запроса выглядит следующим образом.
ngx.req.read_body() local data = ngx.req.get_body_data() if not data then local tmp_file = ngx.req.get_body_file() -- io.open(tmp_file) -- ... end
Этот код содержит блокирующую операцию ввода-вывода для чтения файла с диска. Вы должны настроить параметр client_body_buffer_size (по умолчанию 16 КБ на 64-битных системах), чтобы минимизировать блокирующие операции; вы также можете настроить client_body_buffer_size и client_max_body_size одинаковыми и обрабатывать всё полностью в памяти, в зависимости от размера вашей памяти и количества одновременных запросов.
Кроме того, тело запроса можно перезаписать. Два API ngx.req.set_body_data и ngx.req.set_body_file принимают строку и локальный файл на диске в качестве входных параметров для выполнения перезаписи тела запроса. Однако такие операции встречаются редко, и вы можете ознакомиться с документацией для получения более подробной информации.
Ответ
После обработки запроса нам нужно отправить ответ клиенту. Как и сообщение запроса, сообщение ответа также состоит из нескольких частей: строка состояния, заголовки ответа и тело ответа. Я представлю соответствующие API в соответствии с этими тремя частями.
Строка состояния
Основное, что нас интересует в строке состояния, — это код состояния. По умолчанию возвращаемый HTTP-код состояния — 200, что соответствует встроенной константе OpenResty ngx.HTTP_OK. Но в мире кода всегда обрабатываются самые исключительные случаи.
Если вы обнаружили, что запрос является злонамеренным, то вам нужно завершить запрос:
ngx.exit(ngx.HTTP_BAD_REQUEST)
Однако в кодах состояния HTTP OpenResty есть особая константа: ngx.OK. В случае ngx.exit(ngx.OK) запрос завершает текущую фазу обработки и переходит к следующей, а не возвращается непосредственно клиенту.
Конечно, вы также можете выбрать не завершать запрос, а просто перезаписать код состояния с помощью ngx.status, как написано ниже.
ngx.status = ngx.HTTP_FORBIDDEN
Вы можете посмотреть их в документации, если хотите узнать больше о константах кодов состояния.
Заголовки ответа
Что касается заголовков ответа, есть два способа их установки. Первый — самый простой.
ngx.header.content_type = 'text/plain' ngx.header["X-My-Header"] = 'blah blah' ngx.header["X-My-Header"] = nil -- удалить
Здесь ngx.header содержит информацию о заголовках ответа, которую можно читать, изменять и удалять.
Второй способ установки заголовков ответа — ngx_resp.add_header из репозитория lua-resty-core, который добавляет заголовок, вызывается следующим образом:
local ngx_resp = require "ngx.resp" ngx_resp.add_header("Foo", "bar")
Разница с первым методом заключается в том, что add_header не перезаписывает существующее поле с тем же именем.
Тело ответа
Наконец, рассмотрим тело ответа. В OpenResty вы можете использовать ngx.say и ngx.print для вывода тела ответа.
ngx.say('hello, world')
Функциональность этих двух API идентична, единственная разница в том, что ngx.say добавляет перевод строки в конце.
Чтобы избежать неэффективности конкатенации строк, ngx.say / ngx.print поддерживают строки и массивы в качестве параметров.
$ resty -e 'ngx.say({"hello", ", ", "world"})' hello, world
Этот метод пропускает конкатенацию строк на уровне Lua и оставляет её обработку функциям на C.
Итог
Давайте подведём итог сегодняшнего материала. Мы представили API OpenResty, связанные с сообщениями запросов и ответов. Как вы можете видеть, API OpenResty более гибкие и мощные, чем директивы NGINX.
Следовательно, достаточно ли API Lua, предоставляемых OpenResty, для удовлетворения ваших потребностей при обработке HTTP-запросов? Пожалуйста, оставьте свои комментарии и поделитесь этой статьёй с коллегами и друзьями, чтобы мы могли общаться и улучшаться вместе.