OpenResty — это улучшенный NGINX с поддержкой динамических запросов и ответов

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

После предыдущего введения вы должны понять концепцию 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-запросов? Пожалуйста, оставьте свои комментарии и поделитесь этой статьёй с коллегами и друзьями, чтобы мы могли общаться и улучшаться вместе.