Разбираем HTTP/2 по байтам

image

Откройте любую статью с обзором HTTP/1.1. Скорее всего, там найдётся хотя бы один пример запроса и ответа, допустим, такие:

GET / HTTP/1.1 Host: localhost

HTTP/1.1 200 OK Date: Sat, 09 Oct 2010 14:28:02 GMT Server: Apache Content-Length: 38 Content-Type: text/html; charset=utf-8  <!DOCTYPE html> <h1>Привет!</h1>

Теперь откройте статью с обзором HTTP/2 или HTTP/3. Вы узнаете о мультиплексировании запросов, о сжатии заголовков, о поддержке push-технологий, но вряд ли увидите хоть одно конкретное сообщение. Ясно, почему так: HTTP/1.1 — текстовый протокол, тогда как сиквелы к нему бинарные. Это очевидное изменение открывает дорогу ко множеству оптимизаций, но упраздняет возможность просто и доступно записать сообщения.

Поэтому в этой статье предлагаю покопаться в кишках у HTTP/2: разобрать алгоритмы установки соединения, формат кадров, примеры взаимодействия клиента с сервером.

Статья рассчитана как на давно знакомых с HTTP, так и начинающих фронтендеров недавно изучивших HTTP/1.1, и пытающихся осознать, что там с HTTP/2. Естественно, я не могу описывать все подробности, поэтому новичкам в сетевых технологиях для лучшего понимания материала предлагается ознакомиться хотя бы с тем, как работает стек TCP/IP. Для этого очень рекомендую плейлист Networking tutorial на канале Ben Eater. Кроме того, как увидим далее, HTTP/2 на практике работает только поверх защищённого соединения (https), поэтому рекомендуется что-то в общих чертах понимать про TLS.

Спецификация HTTP/2 описана в RFC 9113 — здесь нужно искать все подробности о протоколе. Также пару раз я буду ссылаться на устаревшую спецификацию — RFC 7540. Она не актуальна, но для исторической справки бывает полезна. Плюс на ней были основаны большинство реализаций серверов, так что если вам доведётся с ними работать, возможно пересечётесь с какими-нибудь отголосками прошлого.

Вспомним былое

Что значит фраза «HTTP/1.1 — текстовый протокол»? То, что все служебные данные являются простыми строками. Клиент, отправляя запрос, записывает метод, путь и заголовки в виде строки ASCII в своей памяти, и затем отправляет по сети получившиеся байты. Например, запрос из начала статьи в памяти и при передаче по сети, выглядит так (в шестадцатеричной записи. Также далее везде числа с приставкой 0x шестнадцатеричные, c 0b двоичные, # отделяет комментарии):

47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31    # GET / HTTP/1.1 0d 0a                                        # \r\n 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 # Host: localhost 0d 0a 0d 0a                                  # \r\n\r\n

Тело же сообщения — просто какой-то набор байтов, а то, как их следует интерпретировать, указано в заголовке Content-Type. Например, в ответе из начала статьи указано Content-Type: text/html; charset=utf-8. Это значит, что тело представляет собой разметку html, передаваемую в кодировке UTF-8. Если не указать атрибут charset, браузеру ничего не останется, кроме как попытаться угадать кодировку самостоятельно (спойлер: вряд ли угадает).

Пример, как выглядит запрос из начала статьи при передаче по сети

# Строка ответа и заголовки в ASCII 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b           # HTTP/1.1 200 OK 0d 0a                                                  # \r\n 44 61 74 65 3a 20 53 61 74 2c 20 30 39 20 4f 63 74 20  # Date: ... 32 30 31 30 20 31 34 3a 32 38 3a 30 32 20 47 4d 54  0d 0a                                                  # \r\n  53 65 72 76 65 72 3a 20 41 70 61 63 68 65              # Server: Apache 0d 0a                                                  # \r\n 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 33 38  # Content-Length: 38 0d 0a                                                  # \r\n 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20              # Content-Type: 38 74 65 78 74 2f 68 74 6d 6c 3b 20                       # text/html; 63 68 61 72 73 65 74 3d 75 74 66 2d 38                 # charset=utf-8 0d 0a 0d 0a                                            # \r\n\r\n  # Заголовки закончились, дальше идёт тело в UTF-8 (как указано в Content-Type) 3c 21 44 4f 43 54 59 50 45 20 68 74 6d 6c 3e 0a                   # <!DOCTYPE html>\n 3c 68 31 3e d0 9f d1 80 d0 b8 d0 b2 d0 b5 d1 82 21 3c 2f 68 31 3e # <h1>Привет!</h1>

Можно обратить внимание, что в теле 32 символа (включая один перевод строки), но в заголовках написано Content-Length: 38. Это связано с тем, что Content-Length содержит длину тела в октетах, а не символах. А так как тело записано в UTF-8, некоторые символы (а именно 6 кириллических в слове Привет) занимают больше одного октета. То есть HTTP вообще не имеет особого понятия, что написано в теле, с точки зрения протокола это просто какой-то набор байтов. Смысл в этот набор вкладывают приложения (клиент и сервер) с помощью подсказок в заголовках.

HTTP/2

Перейдём к виновнику торжества. Развлекательная программа следующая: сначала рассмотрим основные абстракции, которыми оперирует HTTP/2. Затем посмотрим на то, какие существуют кадры, и какое у них назначение. В заключение, разберём, как происходит обмен сообщениями.

Абстрактные основы

В HTTP/1.1 по большому счёту была одна сущность — сообщение. Клиент и сервер обменивались сообщениями, которые бывали двух видов: запросы и ответы. Внутри одного TCP-соединения они чередовались: клиент отправляет запрос, ждёт на него ответ, затем отправляет следующий.

Одной из задач HTTP/2 же стало мультиплексировать запросы, то есть позволить клиенту отправить второе сообщение-запрос по тому же TCP-соединению, не дожидаясь ответа на первый. В такой схеме клиенту при получении сообщения от сервера нужно как-то узнать, на какой из запросов это ответ. Очевидный способ — клиенту генерировать для каждого сообщения идентификатор и, например, отправлять его в заголовке Request-Id, а серверу указывать этот же идентификатор в заголовках ответа. Однако есть ещё одна проблема: сейчас мы отправляем каждое сообщение целиком. Если первый запрос уже начал отправляться, то второй должен будет дождаться конца передачи. Если первый запрос очень большой и менее приоритетный, чем второй, получается неловкая ситуация. Хотелось бы иметь возможность дробить сообщения на более мелкие куски, тогда можно было бы, чередуя их, отправлять несколько сообщений параллельно.

Из этих соображений в HTTP/2 возникают следующие абстракции:

  • Сообщения (messages) — запросы и ответы, всё как в HTTP/1.1. Состоят из заголовков и (необязательно) тела и трейлеров. Дробятся на более мелкие составные части — кадры.
  • Кадр (frame) — простейшая единица обмена информацией, передаются целиком (нельзя начать передавать второй кадр, пока полностью не передался один). Бывают разных типов, в зависимости от чего чуть отличается их содержимое. Есть служебные кадры, например, для управления соединением и потоками; есть кадры, из которых составляются сообщения.

    Замечание для смышлёных: кадры HTTP никак не связаны с пакетами TCP. Один пакет может содержать несколько кадров, один кадр может начинаться и заканчиваться в разных пакетах.

  • Поток (stream) — последовательность кадров, составляющих один запрос и ответ на него. Тогда как предыдущие абстракции имеют физическое представление (каждый кадр — это последовательность октетов1 в определённом формате, а сообщение — некоторый набор кадров), потоки существуют только в наших сердцах мыслях. В каждом кадре есть поле с идентификатором потока, соответственно все кадры с одинаковым идентификатором считаются принадлежащими одному потоку.

    Новый поток может создать как клиент (отправив запрос с идентификатором, который не использовался ранее), так и сервер (если используется server push). По одному потоку можно передать только одну пару сообщений: после того, как получено сообщение-ответ, поток считается закрытым, и по нему больше нельзя ничего передавать (то есть если одна из сторон получает кадр с идентификатором потока, который закрыт, она либо его игнорирует, либо закрывает соединение с ошибкой).


1 Так как по определению (по крайней мере изначальному) байт не обязательно состоит из восьми бит, а октет — обязательно, то при обсуждении сетевых протоколов принято использовать именно октет. И я буду, чем я хуже.

Раздел кадров

Каждый кадр условно делится на заголовок (header, не путать с заголовками сообщения) и содержимое (payload). Заголовок содержит общую информацию о кадре, и одинаковый у них всех, а содержимое уже зависит от конкретного типа. Формат такой:

HTTP Frame {   Length (24),   Type (8),   Flags (8),    Reserved (1),   Stream Identifier (31),    Frame Payload (..), }

Здесь

  • Length (24 бита/3 октета) — длина кадра в октетах, не включая заголовок. Читатель, слышавший что-то об арифметике, заметит, что длина заголовка — 9 октетов, так что длина всего кадра отличается от значения Lengh ровно на 9.
  • Type (8 бит/1 октет) — тип кадра. Например, значение 0x00 значит, что это кадр DATA, а 0x04 — кадр SETTINGS.
  • Flags (8 бит/1 октет) — набор булевых флагов. Каждый из восьми бит может быть 0 или 1, получается восемь флагов, которые можно устанавливать независимо. Доступные флаги зависят от типа кадра, так что о них будем говорить, обсуждая конкретные типы.
  • Reserved (1 бит) — не используется, всегда равен 0.
  • Stream Identifier (31 бит/почти 4 октета) — идентификатор потока, которому принадлежит кадр. Чтобы создать новый поток, клиент отправляет кадр (определённого типа, увидим позже) со значением Stream Identifier, которое не использовалось ранее. Чтобы отправить кадр по конкретному потоку, туда пишется соответствующий идентификатор. Есть кадры, которые относятся не к конкретному потоку, а к соединению в целом — для них используется зарезервированный идентификатор потока 0.
  • Frame Payload (Length октетов) — содержимое, зависит от типа кадра.

Рассмотрим конкретные типы кадров и их назначения. В скобках после названия указан идентификатор типа (значение поля Type в заголовке). Кадры упорядочены не по возрастанию идентификатора, а по зову сердца.

HEADERS (0x01) и CONTINUATION (0x09)

Вернёмся к сообщениям. В HTTP/1.1 они состояли из трёх основных частей: заголовков (включая строку запроса/ответа) и (необязательно) тела и трейлеров. В HTTP/2 они состоят из двух типов кадров: HEADERS и DATA.

Сначала посмотрим на заголовки и трейлеры. Несмотря на название, и для того, и для того используется кадр HEADERS, который имеет такой формат, самый сложный и основных кадров:

HEADERS Frame {   Length (24),   Type (8) = 0x01,    Unused Flags (2),   PRIORITY Flag (1),        # Устарело   Unused Flag (1),   PADDED Flag (1),   END_HEADERS Flag (1),   Unused Flag (1),   END_STREAM Flag (1),    Reserved (1),   Stream Identifier (31),    [Pad Length (8)],   [Exclusive (1)],          # Устарело   [Stream Dependency (31)], # Устарело   [Weight (8)],             # Устарело   Field Block Fragment (..),   Padding (..2040), }

Первые 9 октетов — стандартный для всех кадров заголовок. Содержимое состоит из так называемого «блока заголовков» (header block) и дополнения (padding). Вообще говоря, в новой спецификации используется термин «блок полей» (field block), а не «блок заголовков». Это связано как раз с тем, что HEADERS используется для передачи не только заголовков, но и трейлеров, поэтому говорить «блок заголовков» не совсем корректно. Но, я считаю, название «блок полей» может сбить с толку, поэтому дальше везде буду говорить только о заголовках и подразумевать, что всё что здесь описано, применимо и к трейлерам.

Флаг PRIORITY, как и значения Exclusive, Stream Dependency и Weight в содержимом, — останки механизма приоритизации потоков, который был предложен в первой итерации HTTP/2. На практике он оказался довольно сложным, и большинство клиентов и серверов либо вообще его не реализовали, либо реализовали очень неоптимально. Поэтому этот механизм и все сопутствующие части кадров в новой спецификации были признаны устаревшими, и предложен новый механизм приоритизации на основе заголовков и специальных кадров PRIORITY_UPDATE, который описан отдельно (RFC 9218). Его разбирать здесь не будем.

Если установлен флаг PADDED, первые 8 бит/1 октет в содержимом указывают длину дополнения (Pad Length), а само дополнение идёт в конце кадра, после заголовков. Дополнение представляет собой просто Pad Length нулевых октетов. Оно используется в качестве меры безопасности в дополнение к шифрованию — если нужно скрыть истинную длину заголовков.

Гвоздь кадра — блок заголовков. Заголовки передаются в сжатом с помощью алгоритма HPACK (RFC 7541) виде. Алгоритм довольно хитрый, так что разбирать здесь его не будем. В примерах я буду записывать заголовки трейлеры самым простым (по совместительству неэффективным) способом, каким позволяет HPACK.

Замечание о легальности этого способа

У меня есть сомнения, можно ли кодировать заголовки, для которых есть сокращённые формы записи (например, заголовок accept в HPACK, вообще говоря, можно кодировать всего одним октетом 0x13 плюс сколько-то байт, чтобы закодировать значение), не используя эти сокращённые формы, то есть полностью выписывая названия заголовка в ASCII.

В спецификации HPACK я не нашёл раздела, где такой способ бы был запрещен или хотя бы не рекомендован. Тем не менее, судя по всему, некоторые реализации его не поддерживают. В моих тестах самописный HTTP/2 сервер на NodeJS (использующий встроенную библиотеку node:http2, которая, в свою очередь, использует C++ библиотеку nghttp2) вполне себе замечательно обрабатывает заголовки, записанные таким образом. А сервера гугла при попытке отправить заголовки в таком виде возвращают ошибку сжатия. Но пока меня не ткнут носом, где написано, что так кодировать нельзя, буду использовать эту простую схему.

Сначала пишется один нулевой октет. Далее один октет — длина названия заголовка (только старший бит длины должен быть 0, но это детали) и само название в ASCII, затем длина значения и само значение тоже в ASCII. Все названия заголовков в HTTP/2 пишутся строчными буквами. Рассмотрим конкретный пример кадра HEADERS для наглядности:

00 00 4b 01             # Длина содержимого 0x00 00 4b=75 октетов, тип HEADERS (0x01) 05                      # Установлены флаги END_HEADERS и END_STREAM (6й и 8й биты, 0x05=0b0000 0101) 00 00 00 01             # Идентификатор потока 1 00                      # Начинаются заголовки. Сначала нулевой октет 07 3a 6d 65 74 68 6f 64 # Длина названия 7 октетов, само название ":method" записано в ASCII 03 47 45 54             # Длина значения 3 октета, значение "GET" 00                      # Начало второго заголовка 07 3a 73 63 68 65 6d 65 # Длина 7, название ":scheme" 05 68 74 74 70 73       # Длина 5, значение "https" 00 05 3a 70 61 74 68       # Название ":path" 01 2f                   # Значение "/" 00                       0a 3a 61 75 74 68 6f 72 69 74 79 # Название ":authority" 09 6c 6f 63 61 6c 68 6f 73 74    # Значение "localhost" 00                       04 68 6f 73 74                   # Название "host" 09 6c 6f 63 61 6c 68 6f 73 74    # Значение "localhost" 

Только что мы невольно лицезрели простейший запрос в HTTP/2, аналогичный запросу HTTP/1.1 из самого начала статьи. Сразу бросаются в глаза заголовки, начинающиеся с двоеточия. Это так называемые псевдозаголовки. Они содержат в том числе данные, которые в HTTP/1.1 передавались в строке запроса. Невероятно, но :method — это метод, :path — путь. :scheme — схема URI, на который поступает запрос, обычно это https (браузеры не поддерживают использование HTTP/2 по http, это ещё обсудим позднее), :authority — имя сервера, на который поступает запрос, грубо говоря то, что в HTTP/1.1 указывалось в заголовке Host. Заголовок host однако, как видно в примере, в HTTP/2 тоже есть, только теперь не обязателен. Чем отличается от :authority позволю себе здесь не обсуждать.

Псевдозаголовки чуть отличаются от обычных заголовков. Например, они обязательно должны идти первыми. Кому интересны подробности, тот почитает (спецификацию)[https://www.rfc-editor.org/rfc/rfc9113#section-8.3-2]. Ответы тоже не обделили псевдозаголовками. Правда он всего один: :status — код статуса. Простейший пример ответа в HTTP/2:

00 00 0d 01    # Длина содержимого 13, тип кадра HEADERS 05 00 00 00 01 # Флаги END_HEADERS и END_STREAM, идентификатор потока 1 00 07 3a 73 74 61 74 75 73 # Псевдозаголовок ":status" 03 32 30 34             # Значение "204"

Постоянно писать байты не удобно, поэтому кадры будем записывать в более простом виде, указывая только тип, установленные флаги и содержимое. После типа кадра через черту будем писать номер потока, по которому кадр отправляется:

# Запрос HEADERS/1   + END_HEADERS   + END_STREAM     :method = GET     :scheme = https     :path = /     :authority = localhost     host = localhost  # Ответ HEADERS/1   + END_HEADERS   + END_STREAM     :status = 204

Вот и вышел простейший обмен сообщениями (предполагая, что стороны уже обменялись всеми необходимыми рукопожатиями. Конкретно о том, что должно произойти до обмена сообщениями, речь пойдёт позже).

Осталось что-то сказать про флаги END_HEADERS и END_STREAM. Бывают ситуации (например, большие файлы куки), когда заголовков нужно отправить очень много, и они не влезают в максимально разрешённый размер кадра (такой есть, им можно управлять с помощью кадров SETTINGS). В таком случае можно разбить их на несколько частей. Первую часть отправить в кадре HEADERS, а оставшиеся в одном или нескольких кадрах CONTINUATION, которые выглядят почти также, только более простые:

CONTINUATION Frame {   Length (24),   Type (8) = 0x09,    Unused Flags (5),   END_HEADERS Flag (1),   Unused Flags (2),    Reserved (1),   Stream Identifier (31),    Field Block Fragment (..), }

Флаг END_HEADERS нужен, чтобы обозначить, где заканчиваются заголовки. Он устанавливается на последнем кадре CONTINUATION, либо на самом кадре HEADERS, если CONTINUATION не требуются. Нужно отметить, что между кадр HEADERS и соответствующие CONTINUATION должны идти подряд: между ними не может быть других кадров, даже из других потоков.

Флаг END_STREAM обозначает последний кадр сообщения. Так как одно сообщение отправляется по одному потоку, то после отправки такого кадра, сторона больше не может передавать данные по этому потоку. Флаг END_STREAM на кадре HEADERS с заголовками означает, что у сообщения нет тела и трейлеров. Если же тело есть, то он выставляется на последнем относящемся к сообщению кадре: это либо DATA, если трейлеров нет, либо HEADERS с трейлерами, если есть.

DATA (0x00)

Используется для передачи тела сообщения. Формат простой:

DATA Frame {   Length (24),   Type (8) = 0x00,    Unused Flags (4),   PADDED Flag (1),   Unused Flags (2),   END_STREAM Flag (1),    Reserved (1),   Stream Identifier (31),    [Pad Length (8)],   Data (..),   Padding (..2040), }

Ситуация с дополнением (padding) точно такая же, как в кадрах HEADERS. С END_STREAM уже разобрались, поэтому без лишних слов посмотрим на более сложный пример сообщения.

HEADERS/1   + PADDED     :status = 200     date = Sat, 09 Oct 2010 14:28:02 GMT     server = apache CONTINUAION/1   + END_HEADERS     content-length = 38     content-type = text/html; charset=utf-8 DATA/1     <!DOCTYPE html>\n DATA/1   + PADDED   + END_STREAM     <h1>Привет!</h1>

Разбор по байтам

# Первый кадр 00 00 45 01             # Длина содержимого 0x45=73 октета, тип HEADERS 08 00 00 00 01          # Флаг PADDED, поток 1 04                      # Длина дополнения 4 октета 00 07 3a 73 74 61 74 75 73 # Псевдозаголовок ":status" 03 32 30 30             # Значение "200" 00 04 64 61 74 65          # Заголовок "date", значение "Sat, 09 Oct...." 1d 53 61 74 2c 20 30 39 20 4f 63 74 20 32 30 31 30 20 31 34 3a 32 38 3a 30  32 20 47 4d 54 00 06 73 65 72 76 65 72    # Заголовок "server"  06 61 70 61 63 68 65    # Значение "apache" 00 00 00 00             # 4 октета дополнения (padding)  # Второй кадр 00 00 3a 09             # Длина содержимого 0x3a=58 октетов, тип CONTINUATION 04 00 00 00 01          # Флаг END_HEADERS, поток 1 00 0e 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 # Заголовок "content-length" 02 33 38                                     # Значение "38" 00 0c 63 6f 6e 74 65 6e 74 2d 74 79 70 65       # Заголовок "content-type", 18 74 65 78 74 2f 68 74 6d 6c 3b 20 63       # Значение "text/html; charset=utf-8" 68 61 72 73 65 74 3d 75 74 66 2d 38  # Третий кадр 00 00 10 00             # Длина содержимого 0x10=16 октетов, тип DATA 00 00 00 00 01          # Флагов нет, поток 1 3c 21 44 4f 43 54 59 50 # Строка "<!DOCTYPE html>\n" в UTF-8 45 20 68 74 6d 6c 3e 0a   # Четвёртый кадр 00 00 1f 00             # Длина содержимого 0x1f=31 октет, тип DATA 09 00 00 00 01          # Флаги PADDED и END_STREAM, поток 1 08                      # Длина дополнения 8 октетов 3c 68 31 3e d0 9f d1 80 d0 b8 d0 # Строка "<h1>Привет!</h1>" в UTF-8 b2 d0 b5 d1 82 21 3c 2f 68 31 3e 00 00 00 00 00 00 00 00          # 8 октетов дополнения

Это сообщение, аналогичное примеру ответа из начала статьи. Только я разбил заголовки и тело на несколько кадров как мне вздумалось, чтобы получилось более интересно.

SETTINGS (0x04)

Используется для установки параметров соединения. Формат такой:

SETTINGS Frame {   Length (24),   Type (8) = 0x04,   Unused Flags (7),   ACK Flag (1),   Reserved (1),   Stream Identifier (31) = 0,    Setting (48) ..., }  Setting {   Identifier (16),   Value (32), }

Потоки не имеют настраиваемых параметров, SETTINGS применяется только к соединению в целом, так что идентификатор потока всегда 0. В содержимом перечисляются параметры, которые нужно изменить. Каждый параметр занимает 48 бит/6 октетов, из которых 2 октета — идентификатор, а остальные 4 — значение. Рассмотрим некоторые параметры в качестве примера. Список не полный, полный список и ссылки на спецификации, где параметры описаны, нужно искать на сайте IANA.

  • SETTINGS_ENABLE_PUSH (идентификатор 0x02) — клиент может установить значение 0 этому параметру, чтобы отключить использование server push. Эту технологию оказалось довольно сложно использовать с толком, поэтому, например, хромиум всегда так делает.
  • SETTINGS_MAX_CONCURRENT_STREAMS (0x03) — максимальное количество потоков, которое другой стороне разрешается использовать одновременно. Этот параметр устанавливается именно для другой стороны: клиент ограничивает количество потоков, которые может создавать сервер, и наоборот. Можно указать значение 0: тогда другая сторона не сможет отправлять новые сообщения, пока этот параметр снова не поменяется.
  • SETTINGS_INITIAL_WINDOW_SIZE (0x04) — элемент управления потоками (flow control). Подробнее в разделе про кадры WINDOW_SIZE.
  • SETTINGS_MAX_FRAME_SIZE (0x05) — максимальный размер кадра в октетах, который собеседнику разрешено отправлять. Значение по умолчанию, оно же минимальное — 16 384=214 октетов.

При отправке SETTINGS, важно знать, получил и применил ли собеседник их новые значения. Для этого каждая клиент/сервер после получения SETTINGS отправляет в ответ ещё один такой кадр, в котором устанавливает единственный доступный для этого типа флаг ACK (acknowledged). Например, сервер отправил кадр SETTINGS и указал, что максимальное количество потоков, которые он готов одновременно обрабатывать (параметр SETTINGS_MAX_CONCURRENT_STREAMS с идентификатором 0x03) равно 100. Как только клиент его получит, он должен запомнить новое значение параметра, и отправить в ответ пустой кадр SETTINGS с флагом ACK. Пока сервер не получил это подтверждение, он не может быть уверен, что клиент узнал о новом значении параметра, поэтому не может рассчитывать, что клиент не откроет больше 100 потоков.

Пример такого обмена:

# Сервер отправляет клиенту SETTINGS/0     SETTINGS_ENABLE_PUSH  = 0     SETTINGS_MAX_CONCURRENT_STREAMS = 100  # Клиент отправляет в ответ SETTINGS/0   + ACK

Разбор по байтам

# Сервер клиенту 00 00 0c 04        # Длина содержимого 0x0c=12 октетов, тип SETTINGS 00 00 00 00 00     # Флагов нет, поток 0 00 02 00 00 00 00  # Параметру SETTINGS_ENABLE_PUSH (идентификатор 0x00 02) установить значение 0 00 03 00 00 00 64  # Параметру SETTINGS_MAX_CONCURRENT_STREAMS (0x00 03) установить значение 100 (0x64)  # Клиент серверу 00 00 00 04        # Длина содержимого 0 (пустой кадр), тип SETTINGS 01 00 00 00 00     # Флаг ACK, поток 0

PING (0x06)

Пока не происходит обмена сообщениями, клиент и сервер периодически обмениваются такими кадрами, чтобы проверить, не закрылось ли соединение, не идёт ли коммунизм. Также с помощью них можно оценивать круговую задержку (round-trip time): сколько времени нужно, чтобы отправить кадр и получить на него ответ. Формат:

PING Frame {   Length (24) = 0x08,   Type (8) = 0x06,    Unused Flags (7),   ACK Flag (1),    Reserved (1),   Stream Identifier (31) = 0,    Opaque Data (64), }

Содержимое состоит из 64 бит/8 октетов произвольных данных (Opaque Data). Сами по себе эти данные ничего не значат, но если одна и сторон получает кадр PING, она должна отправить в ответ ещё один кадр PING с флагом ACK и продублировать содержимое.

Пример обмена пингами

# Клиент отправляет серверу 00 00 08 06    # Длина содержимого 8 октетов, тип PING 00 00 00 00 00 # Флагов нет, поток 0 2f b0 7a ee    # 8 произвольных, ничего не значащих октетов 92 01 8a bc  # Сервер отвечает 00 00 08 06    # Длина содержимого 8 октетов, тип PING 01 00 00 00 00 # Флаг ACK, поток 0 2f b0 7a ee    # Продублировано из предыдущего кадра 92 01 8a bc

WINDOW_UPDATE (0x08)

Во многих сетевых протоколах, в том числе TCP и HTTP, обработка данных происходит не мгновенно по получении. Операционная система получает сетевые пакеты и складывает их в некоторый буфер, из которого приложение потом может их считать сразу или через какое-то время. Из-за этого возникает необходимость в некотором механизме управления потоком (flow control): нельзя отправлять слишком много данных сразу, если они обрабатываются медленно, у собеседника может, например, банально не хватить памяти всё сохранить для последующей обработки.

Самые большие кадры в HTTP — это кадры DATA, поэтому flow control применяется только к ним. Все остальные кадры, в частности HEADERS, обычно не слишком большие, поэтому обрабатываются сразу. За то, чтобы не отправить слишком много заголовков, отвечает механизм flow control, который уже есть в TCP.

У каждого потока и у соединения в целом есть «размер окна» (window size) — максимальное количество октетов, которые собеседнику разрешено отправить в данный момент на данном потоке/соединении. С каждым полученным кадром DATA размер окна уменьшается на размер его содержимого. Если отправить слишком много, размер окна станет слишком маленьким, и собеседник откажется принимать новые кадры. Размер окна свой у клиента и у сервера.

Когда же собеседник обработает полученные ранее данные и решит, что готов получать новые, он отправит кадр WINDOW_UPDATE, в котором увеличит свой размер окна, и позволит отправлять новые кадры. Формат такой:

WINDOW_UPDATE Frame {   Length (24) = 0x04,   Type (8) = 0x08,    Unused Flags (8),    Reserved (1),   Stream Identifier (31),    Reserved (1),   Window Size Increment (31), }

Тело состоит из 4 октетов (только старший бит не используется), которые указывают, на сколько надо увеличить размер окна.

Пример работы WINDOW_UPDATE

Изначальный размер окна задаётся параметром SETTINGS_INITIAL_WINDOW_SIZE, по умолчанию 65 535 октетов. Предположим, сервер отправил кадр размером 65 500 октетов:

00 ff dc 00    # Размер 0xff dc=65 500 октетов, тип DATA 01 00 00 00 01 # Флаг END_STREAM (но не END_HEADERS), поток 1 1a 2b 3c 4d... # Куча данных на 65 500 октетов

И теперь хочет отправить ещё столько же. Но размер окна сервера только что уменьшился на 65 500 и стал 35 октетов, то есть ещё один такой же кадр не поместится. Поэтому сервер не может отправить его сразу же, он должен подождать некоторое время. Когда клиент получит первый кадр и обработает его, он отправит, например, такой WINDOW_UPDATE:

00 00 04 08 00 00 00 00 01 00 00 ff ff

Таким образом, увеличив размер окна для потока 1 ещё на 65 535 октетов. То есть после отправки этого кадра размер окна клиента станет 65 570 октетов. Rогда сервер получит этот WINDOW_UPDATE, он узнает, что размер окна клиента увеличился, и сможет отправить следующий кадр DATA.

Примечание: я не нахожу в спецификации места, где было бы оговорено, нужно ли увеличивать в этом сценарии размер окна для соединения в целом и для потока 1 двумя отдельными WINDOW_UPDATE, или же увеличение окна потока 1 автоматически увеличивает окно всего соединения. Попытался проверить на практике, отправляя кадры на самодельный HTTP/2 сервер на NodeJS, использующий встроенную библиотеку node:http2 (которая, в свою очередь, использует C++ библиотеку nghttp2) и на сервера гугла: в обоих случаях выглядит, как будто достаточно одного кадра, который увеличивает окно только для потока 1.

RST_STREAM (0x03) и GOAWAY (0x07)

Эти кадры позволяют преждевременно закрыть поток/соединение. Например, если клиент на каком-то потоке отправил кадр с ошибкой (не соответствующий спецификации HTTP/2), сервер скорее всего закроет этот поток с помощью RST_STREAM (по-русски reset stream), в котором укажет код ошибки. Либо, например, пока сервер отправляет ответ на запрос, клиент решил, что этот он больше не нужен (скажем, пользователь отменил загрузку картинки в вотсапе). Тогда он может остановить передачу ответа с помощью RST_STREAM. В общем, причин закрывать поток может быть множество.

Аналогично с GOAWAY: он служит для закрытия соединения либо в случае ошибки, либо просто по инициативе одной из сторон. В кадре GOAWAY содержится самый большой идентификатор потока, который отправитель смог получить и каким-то образом обработать — Last-Stream-Id. Таким образом, получатель GOAWAY узнаёт, какие сообщения нужно будет отправлять заново, а какие нет.

Форматы кадров и примеры

RST_STREAM Frame {   Length (24) = 0x04,   Type (8) = 0x03,    Unused Flags (8),    Reserved (1),   Stream Identifier (31),    Error Code (32), }  GOAWAY Frame {   Length (24),   Type (8) = 0x07,    Unused Flags (8),    Reserved (1),   Stream Identifier (31) = 0,    Reserved (1),   Last-Stream-ID (31),   Error Code (32),   Additional Debug Data (..), }

Возможные коды ошибок и спецификации, в которых они определены, надо вновь искать на страничке IANA.

Additional Debug Data — произвольны данные, которые могут помочь при отладке ошибок. Обычно это какая-нибудь строка в ASCII с более подробным описанием ошибки, чем позволяет Error Code, но в принципе там могут быть любые данные.

Пример: клиент закончил передачу запроса на потоке 5, отправив все необходимые кадры и установив в последнем флаг END_STREAM, а затем пытается отправить ещё один кадр HEADERS на том же потоке 5. Сервер закрывает его с ошибкой:

00 00 04 03    # Длина содержимого 4 октета, тип RST_STREAM 00 00 00 00 05 # Флагов нет, поток 5 00 00 00 05    # Ошибка 0x05 — STREAM_CLOSED

Ещё пример: на клиенте некорректно реализован алгоритм HPACK, и сервер не может понять заголовки в запросе из потока 7. В связи с особенностями HPACK, в этом случае сервер должен закрыть соединение. При этом запросы, которые ранее приходили на потоках 1 и 3 уже обработаны, и на них отправлены ответы, а на потоке 5 сообщение начало обрабатываться, но не закончило, и ответ ещё даже не сформирован. Тогда сервер отправляет такой кадр:

00 00 14 07    # Длина содержимого 0x14=20 октетов, кадр GOAWAY 00 00 00 00 00 # Флагов нет, поток 0 00 00 00 05    # Последний обработанный поток — пятый 00 00 00 09    # Ошибка 0x09 — COMPRESSION_ERROR 68 70 61 63 6b 5f 65 72 72 6f 72 0a # Строка "hpack_error" для отладки, записанная в ASCII

После этого он закрывает TCP-соединение, но может продолжить обработку запроса из потока 5 (скажем, это был запрос на перевод денег, который нельзя просто оборвать посередине, чтобы база данных не оказалось в невалидном состоянии).


На этом завершим обзор кадров. Необсуждёнными из спецификации HTTP/2 остались только кадры PRIORITY (устаревшие останки вышеупомянутой системы приоризитации) и PUSH_PROMISE (используется для server push. Позволю себе пропустить его обсуждение, так как технология на данный момент не слишком востребованная, да и интересных подробностей нет: просто сервер отправляет PUSH_PROMISE с описанием запроса, на который он хочет без спросу выдать ответ, а затем сам ответ). Кроме того, упустили кадры, которые были введены в отдельных RFC. Полным списком кадров со ссылками в очередной раз предлагаю искать на страничке IANA.

Как завязать общение с понравившимся сервером

Обсудили кусочки, из которых составляется обмен данными в HTTP/2. Теперь посмотрим на соединение с высоты: как оно начинается и живёт.

Сервера HTTP/1.1 и HTTP/2 по умолчанию используют один и тот же порт — 443. Передаваемые данные у протоколов сильно отличаются, поэтому если клиент не знает заранее, какую версию поддерживает сервер, нужен какой-то механизм, как её выбрать.

Изначально (в старой спецификации) таких было два: один для защищённых соединений (тогда протокол называется HTTP/2 over TLS, сокращённо h2), один для незащищённых (HTTP/2 over cleartext TCP, сокращённо h2c). Однако большинство браузеров в принципе не стали поддерживать возможность использовать h2c, поэтому второй механизм в новой спецификации был признан устаревшим. Но для исторической справки и полноты картины описание, как это должно было работать, приведу в спойлере.

В случае защищённого соединения (с HTTP over TLS, он же h2, схема https) идея очень простая, но только если вы знаете TLS. Этот протокол помимо своей основной цели (шифрование трафика) поддерживает множество расширений — дополнительных полей, которыми сервер и клиент могут обменяться во время установки зашифрованного соединения. Одно из них — согласование протокола прикладного уровня (ALPN, Application-Level Protocol Negotation). Клиент отправляет список поддерживаемых протоколов (например, http/1.1 и h2), а сервер в ответе выбирает, какой из них предпочтителен. К сожалению, разбор TLS по байтам в этой статье не предусмотрен, так что как именно отправляется информация в ALPN как-нибудь в другой раз. Но суть в том, что сервер, увидев, что клиент поддерживает h2, просто сразу с помощью того же ALPN сообщает «будем использовать HTTP/2».

Как могло бы быть в случае незащищённого соединения

Так как TLS в этом случае не используется, а в TCP расширения ALPN не предусмотрено, выбор протокола происходил бы на уровне HTTP. Если бы клиент заранее знал, что сервер поддерживает HTTP/2 (например, вы ручками указали в настройках, либо уже подключались к этому серверу ранее, и браузер сохранил версию протокола в кеш), то клиент сразу бы отправил свои вступительные HTTP/2-кадры, как описано ниже, по которым сервер бы понял, что надо использовать HTTP/2. Если бы клиент ошибся (думал, что сервер поддерживает HTTP/2, а на самом деле нет), сервер бы просто закрыл соединение, не поняв, что от него требуется.

Иначе же, сначала клиент отправил бы запрос HTTP/1.1, в котором, среди прочего, указал заголовки Upgrade и HTTP2-Settings:

GET /hot-asian-girls.php HTTP/1.1 Host: example.com Accept: text/html Accept-Language: en Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings: ...

Значением Upgrade являлся бы идентификатор протокола HTTP/2 over cleartext TCP, то есть h2c. А HTTP2-Settings служил бы, чтобы сразу установить параметры соединения: в нём бы та же последовательность, что и в содержимом кадра SETTINGS, только закодированная в base64. Если бы сервер не поддерживал HTTP/2, он бы просто отправил ответ, как будто это обычный запрос HTTP/1.1. Иначе, он бы отправил ответ со статусом 101:

HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: h2c

За ним сразу последовали бы вступительные кадры HTTP/2 и ответ на запрос клиента. Но сейчас вся эта схема считается устаревшей, и не поддерживается практически ни одним браузером. Здесь приведена только в качестве исторической справки.

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

Что-нибудь содержательное

Первым делом, клиент сразу же отправляет серверу так называемое вступление2 (connection preface). Первая часть вступления — это фиксированная последовательность октетов

50 52 49 20 2a 20 48 54 54 50 2f 32  2e 30 0d 0a 0d 0a 53 4d 0d 0a 0d 0a

О сакральном смысле

Если интерпретировать эти октеты как последовательность кодов символов ASCII, получится следующая строка. Если закрыть глаза, она чем-то напоминает начало запроса HTTP/1.1.

PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

Кто читал спойлер про незащищённое соединение, тому будет понятен смысл. Если бы протокол использовался без TLS, и клиент заранее знал, что сервер поддерживает HTTP/2, то эта строка служила бы индикатором для сервера, что клиент собирается использовать HTTP/2. Если же сервер поддерживал бы только HTTP/1.1, он бы распарсил её как запрос с невалидным методом, неправильной версией, и вообще чёрт знает чем. Тогда бы он просто закрыл соединение, и клиент бы понял, что ошибся с протоколом.

При использовании TLS важность этой строки теряется, поскольку выбор протокола происходит уже на этапе TLS-рукопожатия. Но по историческим причинам, и как дополнительное подтверждение выбора HTTP/2, она остаётся частью протокола.

Вторая часть — (возможно пустой) кадр SETTINGS с изначальными параметрами соединения. После вступления клиент может сразу начать отправлять запросы, не дожидаясь вступления сервера. А у сервера оно тоже есть, правда состоит всего из одного (опять же, возможно пустого) кадра SETTINGS.

Сообщение-запрос выглядит так:

  1. Один кадр HEADERS, в котором указывается идентификатор потока, который не использовался ранее. Идентификаторы выбираются обязательно по возрастанию, причём клиент может открывать только потоки с нечётными номерами. Это нужно, потому что сервер тоже может открывать новые потоки (с помощью кадров PUSH_PROMISE при использовании server push) — он будет выбирать только чётные номера. Таким образом не возникает коллизий.

    Если все заголовки поместились в HEADERS, у него должен быть флаг END_HEADERS. Иначе за HEADERS могут следовать сколько угодно кадров CONTINUATION, и флаг выставляется на последнем. Если у запроса нет тела, то у HEADERS должен быть флаг END_STREAM.

  2. Возможно, один или больше кадров DATA, содержащих тело запроса. Если нет трейлеров, на последнем должен быть флаг END_STREAM.
  3. Возможно, ещё один кадр HEADERS, содержащий трейлеры. Если он есть, то должен быть с флагом END_STREAM. Если трейлеров много, далее также могут следовать кадры CONTINUATION. В этом случае на последнем должен быть флаг END_HEADERS, иначе он должен быть на самом HEADERS. Список трейлеров, так же как и в HTTP/1.1, отправляется в заголовке `Trailers`.

В кадре HEADERS запроса первыми обязательно должны быть псевдозаголовки :method, :path и :scheme (кроме запросов с методом CONNECT, но это детали); псевдозаголовок :authority может отсутствовать.

После того, как сервер получил на потоке кадр с флагом END_STREAM, он может начинать отправлять ответ. Формат сообщения-ответа точно такой же, как и у запроса, только идентификатор потока указывается не новый, а тот, на котором пришёл запрос; и вместо :method, :path, :scheme и :authority, единственный псевдозаголовок, который ответ может и обязан содержать — это :status.

Когда размер окна одной из сторон подходит к концу, она увеличивает его кадром WINDOW_UPDATE. Пока никакие сообщения не отправляются, стороны периодически обмениваются кадрами PING. В любой момент можно изменить параметры соединения кадром SETTINGS. Любая из сторон может закрыть любой поток кадром RST_STREAM. HTTP-соединение прерывается, когда закрывается TCP-соединение. Перед тем как закрыть TCP-соединение, сторона может отправить кадр GOAWAY с объяснением причин (вообще говоря, не обязана, но рекомендуется).

Вот так и живём. Основные детали протокола разобрали, под конец приведу содержательный пример обмена сообщениями. Домашнее задание: написать простой клиент и сервер HTTP/2 на любимом языке.

Содержательный пример

Слева записаны кадры, которые отправляет клиент, справа — сервер. Кадры указаны сверху вниз в порядке, в котором они отправляются. Ещё домашнее задание: расписать этот обмен кадрами по байтам.

PRI * HTTP/2.0\r\n\r\n                  | SM\r\n\r\n                              |  SETTINGS/0                                         |      SETTINGS_MAX_CONCURRENT_STREAMS = 100 SETTINGS/0                              |       SETTINGS_ENABLE_PUSH = 0            |     SETTINGS_NO_RFC7540_PRIORITIES = 1  |                                         | HEADERS/1                               |     + END_STREAM                          |  SETTINGS/0     :method = GET                       |    + ACK     :scheme = https                     |       :path = /                           |     :authority = example.com            |     accept = text/html                  |     accept-language = ru                |     user-agent = My-Browser/1.0         |                                         | CONTINUATION/1                          |   + END_HEADERS                         |     cookie = <2048 октетов>             |                                         | SETTINGS/0                              |   + ACK                                 |  HEADERS/1                                         |    + END_HEADERS HEADERS/3                               |    + END_STREAM   + END_HEADERS                         |      :status = 302   + END_STREAM                          |      location = https://example.com/ru     :method = GET                       |      content-length = 0     :scheme = https                     |      vary = accept-language     :path = /favicon.ico                |     :authority = example.com            |     accept = image/webp                 |     user-agent = My-Browser/1.0         |                                         | HEADERS/5                               |   + END_HEADERS                         |   + END_STREAM                          |  HEADERS/3     :method = GET                       |    + END_HEADERS     :scheme = https                     |      :status = 200     :path = /ru                         |      content-length = 68792     :authority = text/html              |      content-type = image/webp     accept-language = ru                |        user-agent = My-Browser/1.0         |  DATA/3     cookie = <2048 октетов>             |      <65535 октетов>                                         |                                         |  HEADERS/5 WINDOW_UPDATE/3                         |    + END_HEADERS     Window Size Increment = 65535       |      :status = 200                                         |      content-length = 38 WINDOW_UPDATE/5                         |      content-type = text/html; charset=utf-8     Window Size Increment = 65535       |                                           |  DATA/3                                         |    + END_STREAM                                         |      <3257 октетов>                                         |                                         |  DATA/5                                          |    + END_STREAM                                         |      <!DOCTYPE html>\n                                         |      <h1>Привет!</h1>                                         |                                           |                                         |                                         | PING/0                                  |     2f b0 7a ee 92 01 8a bc             |                                         |  PING/0                                         |    + ACK                                         |      2f b0 7a ee 92 01 8a bc                                         | GOAWAY/0                                |     Last-Stream-Id = 0                  |     Error Code = 0x00 NO ERROR          |                                         | # Клиент закрыл TCP-соединение          |                                         |


2 «Вступлением» эту штуку я обозвал сам. Без перевода оставлять не хочу, а в русском языке, кажется, не закрепилось такого термина. Либо я просто не посвящён.


ссылка на оригинал статьи https://habr.com/ru/companies/timeweb/articles/751338/

Делаем классные проекты, а кейсы получаются унылые. В чем проблема?

Рассказываю о 8 причинах, почему в рассказах о работе не выходит показать, какие вы на самом деле классные и профессиональные.

Привет! Меня зовут Паша Молянов, я руковожу контент-агентством «Сделаем». Если бы мне нужно было из всего контент-маркетинга выбрать лишь один формат, я бы выбрал кейсы. Потому что в них всё: и демонстрация результатов, и рассказ о процессе работы, и ваша экспертиза, и то, каким может быть продукт в зависимости от клиентской задачи.

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

Вот самые частые проблемы с кейсами, из-за которых они не получаются ↓

Проблема №1. Завышенные ожидания от кейсов

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

Большинство же кейсов — это рабочие лошадки, которые не взрывают интернет. Например, мы с «Батискафом» написали кейс про оптимизацию налогов. Статья набрала 3200 просмотров, это не очень много. Зато привела четырех клиентов.

Еще одна важная функция кейсов — это быть в нужное время в нужном месте:

  • клиент выбирает между двумя компаниями и отдает предпочтение той, которая опубликовала кейс о работе с его задачей и его сферой

  • на этапе переговоров клиент засомневался, что его задачу смогут решить — а продавец тут же показал ему соответствующий кейс

  • клиент зашел на сайт, увидел 150 успешных кейсов — и само их наличие и количество добавило плюсик в копилку доверия к компании

В этих случаях речи не идет даже о популярности на уровне 3200 просмотров, но свою задачу кейсы выполняют.

При прочих равных кейсы всегда проигрывают по популярности инструкциям, объяснялкам, аналитике, обзорам, набросам и сторителлингам. Если выстреливает один кейс из десяти — это уже круто. Задача кейсов в другом: показать, как вы работаете с конкретным пользовательским сценарием.

Проблема №2. Нет интересной фактуры

Качественно выполненный проект может быть банально неинтересным. Например, наше агентство пишет для клиентов статьи на VC. Подавляющее большинство таких проектов проходят по одному и тому же сценарию: редактор общается с клиентом, достает несколько тем для статей, выбирает с клиентом одну, проводит по ней интервью, пишет и согласует текст, публикует — готово.

Часто дают такие советы по написанию кейсов: рассказывайте о неожиданных ситуациях! как вы разбирались с тем, что идет не по плану! как ошибались! как делали непростой выбор!

Я с этим полностью согласен, но как быть, если этого нет? Высасывать из пальца? Мы с трепетом шли на созвон с клиентом: нам предстояло из пяти заходов для статьи выбрать три… Те самые три захода, которые будут вершить судьбу всей рекламной кампании в ПромоСтраницах!

По «проходным» проектам тоже нужны кейсы — я вообще сторонник того, чтобы по каждому проекту был кейс. Но их можно выпустить в лайт-версии: просто показать результаты работы.

И нам не надо мучиться, и клиент увидит: вот статья, читать интересно, просмотры хорошие — значит, ребята молодцы, можно обратиться.

И нам не надо мучиться, и клиент увидит: вот статья, читать интересно, просмотры хорошие — значит, ребята молодцы, можно обратиться.

Проблема №3. Авторы проектов не хотят заниматься кейсами

В выпуске кейсов больше всего заинтересованы два человека: собственник, которому кейсы нужны для привлечения клиентов, и копирайтер, которому платят за их выпуск.

Для всех остальных это задача под почетным номером 100500.

Проблема в том, что ни собственник, ни даже копирайтер не могут сами написать кейс. Обязательно нужен тот (или те, это вообще проблема в квадрате), кто работал над проектом.

А этот человек обычно занят тем, что уже работает на другом проекте. Чтобы написать кейс, ему нужно вспомнить ход предыдущего проекта, собрать материалы, дать интервью копирайтеру, согласовать текст перед публикацией. Задача большая, поэтому кейсы буксуют. Копирайтер обивает пороги, его кормят завтраками, а когда перестают, то дают интервью «по-быстрому».

Я пока не придумал элегантное решение этой проблемы. Есть только то, которое работает со скрипом — но хотя бы работает.

  1. Включить создание кейсов в должностные обязанности. Чтобы нельзя было сказать: «Это не моя работа»

  2. Придумать понятную мотивацию для команды. У нас в «Сделаем» редакторы получают гонорар за кейс, «Нескучные финансы» рассказывали, что сделали определенное количество кейсов условием для перехода на следующий грейд.

  3. Назначить ответственного за кейсы — человека, который будет всех тюкать. Важно, чтобы этим человеком был тот, которому не получится сказать «Давай когда-нибудь потом», «Мой кейс не надо публиковать», «Скрины пришлю через 4 недели» или просто заигнорить. У нас этим занимается директор по маркетингу. Надеюсь, он сможет это делегировать =)

Проблема №4. Копирайтер не разбирается в теме

Проект классный, эксперт дал интересную фактуру, кейс должен получиться бомбовый — а на выходе что-то скомканное и дилетантское: по верхам, с упущенными нюансами (а в них вся мякотка!) и даже с фактическими ошибками.

Хочется показать в кейсах свою экспертность, а текст такой, будто вы только вчера начали работать.

Так бывает, когда копирайтер только начинает работать с новой темой, в которой не разбирается. Тут главное не ставить на копирайтере крест, как только получили первый «дилетантский» кейс. Нужно смириться, что первые пять-десять кейсов придется вымучивать: подбирать разговорчивых экспертов, ходить на созвоны вместе с копирайтером, по нескольку раз редактировать кейсы, скидывать материалы для обучения.

В начале работы оценивайте чисто копирайтерские навыки: структуру, логику, формулировки, навыки визуализации, умение проводить интервью. А экспертность к такому копирайтеру придет позже, если помочь ему погрузиться в тему. Ведь для него она совсем новая.

Если вы к этому не готовы, ищите копирайтеров с опытом в вашей теме. Это сложно, но тогда и кейсы сразу будут классными.

Проблема №5. Не история, а набор фактов

Даже самый интересный проект можно превратить в унылый нечитаемый кейс, если просто фигачить в читателя фактами: провели аудит, внедрили одну штуку, исправили другую штуку, провели план-факт через месяц, результаты вот, спасибо до свидания.

В таких кейсах не хватает связок, которые склеивают факты в единую историю ↓

🌚 Мы начали работу с диагностики и увидели, что у одного из продавцов проседает конверсия в продажу → Нам предстояло увеличить конверсию в продажу, но в компании не было подробной аналитики. Без нее непонятно, над чем конкретно работать. Поэтому мы начали работу с диагностики: две недели просто замеряли результаты отдела продаж как они есть, безо всяких изменений. Когда мы подбили результаты, увидели, что у одного из продавцов проседает конверсия в продажу.

🌚 Чтобы увеличить конверсию, мы разработали для продавца индивидуальную программу обучения → Обычно конверсия в продажу западает, когда продавец плохо знает продукт или не соблюдает технологию продаж. Чтобы это проверить, мы прослушали несколько десятков звонков Ильи — но ничего такого там не было. Но мы обратили внимание на его манеру речи: спокойную, тихую, местами даже монотонную.

Автору проекта все эти связки могут показаться очевидными, потому что он привык принимать такие решения. А вот человеку со стороны с их помощью будет лучше понимать ситуацию, а значит и читать ему будет приятнее.

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

Проблема №6. Не хватает визуализации

Текст классно передает факты, но плохо справляется с формированием картинки в голове читателя. Поэтому кейсы со слабым визуалом получаются чересчур абстрактными.

Настроили рекламную кампанию? Покажите рекламные креативы и плейсменты, в которых их использовали.

Внедрили финансовый учет? Сделайте скриншоты отчетов и покажите, как выглядят в них привычные показатели.

Закончили долгосрочный проект и постепенно пришли к желаемым результатам? Сделайте график, чтобы можно было за секунду увидеть динамику.

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

Проблема №7. Кейс обо всем — и ни о чем

Так бывает, когда для клиента сделали комплексный проект или работали с ним долго и много дел успели переделать. Вроде бы логично рассказать в кейсе обо всех этих делах — но получается такой кейс или слишком большим, или слишком поверхностным.

Лучше выпустить несколько кейсов по одному клиенту: отдельный про оптимизацию расходов, отдельный про систему мотивации для сотрудников, отдельный про систему финансового планирования.

А если важно показать комплексность услуг, из этих кейсов можно собрать мегакейс. Мы так делаем со своими самыми крупными и долгими проектами: собираем кейсы по ним на одной странице и при необходимости показываем потенциальным клиентам.

Проблема №8. Кейсы просто лежат на сайте

Если у компании много кейсов на сайте, то это уже прекрасно. Они будут выполнять свое базовое предназначение: потенциальный клиент зайдет на сайт и убедится: ребята опытные, задачи как у меня уже решали. Но можно делать больше ↓

Анонсируйте кейсы в своих соцсетях. Пусть аудитория видит, что компания тут не просто умную теорию рассказывает, а занимается суровой практикой.

Делайте ресайзы под соцсети. Большой детализированный кейс можно упростить и сделать из него историю на 30 секунд чтения — такая с большей вероятностью соберет охваты.

Вставляйте кейсы в прогревочные имейл-цепочки. Ведь что может прогреть лучше, чем кейсы? А ничего не может.

Используйте кейсы повсюду в контент-маркетинге. Пишете полезную статью? Аргументируйте тезисы через истории из практики. Нужно привести пример на вебинаре? Сошлитесь на один из своих кейсов. Делаете подборку статей по теме? Добавьте в нее и кейсы тоже.

Интегрируйте кейсы в отдел продаж. Пусть продавцы включают кейсы в свои презентации, подбирают нужные кейсы перед созвоном с клиентом, ссылаются на кейсы при аргументации, изучают по кейсам пользовательские сценарии и методику работы компании. А то обидно, когда кейсы выпускаются, а отдел продаж за ними совсем не следит.

Несколько слайдов из презентации нашего агентства

Несколько слайдов из презентации нашего агентства

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


ссылка на оригинал статьи https://habr.com/ru/articles/751552/

Как взломать антиплагиат? — Безопасность и уязвимости NLP -классификаторов. Часть 1

Всем привет! Меня зовут Артём Семенов, я занимаюсь пентестами в компании RTM Group.

Известная поговорка гласит: «Словом можно ранить, а словарём – убить». Это особенно актуально для темы, которую мы сегодня рассмотрим, ведь для атак мы будем использовать либо слово, либо огромный текст. В начале 2023 года ChatGpt произвёл фурор. Эта языковая модель может генерировать машинные тексты и писать стихи, которые максимально приближены к «авторскому» (человеческому) стилю. Казалось бы, при помощи систем антиплагиата проблему подделок можно было бы решить, но уже известно, что и их реально взломать – автор диплома, написанного с помощью ChatGPT, в своём твиттере описывает, как это сделать.

В этой статье я расскажу о том, какие недостатки содержат классификаторы, и как этим может воспользоваться реальный злоумышленник. Мы рассмотрим теоретические аспекты, а также проведем анализ безопасности текстовых классификаторов, – как распространёнными методами, так и нестандартными способами, с целью обхода систем классификации текста, а также обмана систем определения машинного текста. 

Дисклеймер

Данная статья носит исключительно образовательный характер и не призывает использовать описанные методы для нарушения академической честности. Наша цель — определить недостатки систем антиплагиата и возможности для их исправления специалистами в соответствующих областях.

 

Как мы готовились к проведению исследования?

В качестве систем для тестирования нашего текста мы взяли самые распространённые системы классификации естественного языка:

·      Классификатор от OpenAI

·      Антиплагиат

·      https://detectgpt.com/

·      Zerogpt

·      https://detectgpt.ericmitchell.ai/

 

Этот текст мы возьмём для тестирования:

Экономика — это наука, которая исследует, как люди, компании и государства используют ресурсы для производства, распределения и потребления товаров и услуг в определенной стране или регионе. Она имеет широкий диапазон интересов, включающий макро и микроэкономику, теорию и практику, национальную и международную экономику.

Макроэкономика исследует экономические тенденции на уровне всей страны или региона, такие как инфляция, безработица и рост ВВП. Микроэкономика фокусируется на решении проблем на уровне отдельных компаний и потребителей, таких как ценообразование и производство.

Экономика играет важную роль в жизни людей. Она определяет стабильность и процветание страны, влияет на уровень безработицы, доход и покупательскую способность граждан. Также экономика может бывать сильной или слабой, и это влияет на финансовый рынок и инвестиции.

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

Одним из важных аспектов экономики является управление ресурсами, такими как финансы и трудовые ресурсы. Оптимизация использования ресурсов может привести к увеличению производительности, повышению экономического роста, сокращению затрат и улучшению жизни людей.

Кроме того, экономика может иметь влияние на природную среду, так как производство и потребление товаров и услуг означает использование ресурсов и энергии. Из этого следуют вопросы экономической устойчивости, которые подразумевают балансирование потребности людей в ресурсах с сохранением природных ресурсов для будущих поколений.

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

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

Одним из важных направлений в экономике является международная экономика. Она исследует экономические отношения крупных государств и международные организации, а также транснациональные корпорации, участвующие во внешнеторговых и инвестиционных операциях. Международная экономика имеет большое значение для глобальной экономической стабильности, развития торговых отношений и улучшения жизни людей в различных странах.

Еще одним важным аспектом экономики является роль государства в экономике. Государство влияет на экономические процессы при помощи налоговой политики, фискальной политики и монетарной политики. Роль государства в экономике может быть различной, в том числе варьироваться от свободного рынка до государственного планирования. Большинство стран выбирают среднюю линию, совмещая элементы свободной рыночной экономики и государственного регулирования.

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

Этот текст сгенерирован при помощи GPT3.5. Мы взяли эту модель за основу, потому что она является самой популярной. Кроме того, сегодня существует огромное количество детекторов, позволяющих определить созданный ею текст. После генерации текста было принято решение сразу прогнать его через сервисы. Мы получили следующие результаты:

Результат, полученный при проверке текста при помощи Antiplagiat.ru

Результат, полученный при проверке текста при помощи Antiplagiat.ru

Стоит сразу отметить, что сервис также может определять тексты, написанные с использованием языковой модели. Эту возможность сервис получил в мае. Антиплагиат отметил весь текст, как написанный машиной.

 

Результат, полученный при проверке текста при помощи https://www.zerogpt.com/

Результат, полученный при проверке текста при помощи https://www.zerogpt.com/

 

Результат, полученный при проверке текста при помощи классификатора от OpenAI (https://platform.openai.com/ai-text-classifier)

Результат, полученный при проверке текста при помощи классификатора от OpenAI (https://platform.openai.com/ai-text-classifier)

Классификатор от OpenAI распознал этот текст как «возможно сгенерированный при помощи ИИ». И это кажется невероятно странным, потому что мы точно знаем, что текст является машинным. Возможно, он сомневается из-за того, что он слабо обучен на других языках. Это мы проверим позже.

Результат, полученный при проверке текста при помощи классификатора от detectgpt от Эрика Митчелла (https://detectgpt.ericmitchell.ai/)

Результат, полученный при проверке текста при помощи классификатора от detectgpt от Эрика Митчелла (https://detectgpt.ericmitchell.ai/)

Данный ресурс определил, что 3694 токена, которые использовались при генерации текста, являются токенами GPT.

В ходе написания статьи было обнаружено, что множество из доступных детекторов машинного текста не определяют текст на русском или других языках. Им доступен лишь английский, поэтому было принято решение попросить ChatGPT перевести текст, который мы сгенерировали раннее. Вот что получилось:

Economics is a science that studies how people, companies, and governments use resources to produce, distribute, and consume goods and services in a particular country or region. It has a wide range of interests, including macro and microeconomics, theory and practice, national and international economics.

Macroeconomics examines economic trends at the level of an entire country or region, such as inflation, unemployment, and GDP growth. Microeconomics focuses on solving problems at the level of individual companies and consumers, such as pricing and production.

Economics plays an important role in people’s lives. It determines the stability and prosperity of a country, affects the level of unemployment, income and purchasing power of citizens. Also, the economy can be strong or weak, which affects the financial market and investments.

Overall, economics is a key element in the life of people and human society as a whole. Understanding economic processes and phenomena helps to make informed decisions on financial matters, manage businesses, and even personal finances.

One important aspect of economics is resource management, such as finances and labor resources. Optimizing resource usage can lead to increased productivity, economic growth, cost reduction and improvements in people’s lives.

In addition, economics can have an impact on the natural environment, as production and consumption of goods and services means using resources and energy. This leads to questions of economic sustainability, which involve balancing people’s resource needs with preserving natural resources for future generations.

Modern economics also includes new technologies, such as digital and global economics. These technologies allow for quick information exchange, creation of new business models and products, which in turn leads to the creation of new markets and opportunities for economic development.

Thus, economics plays an important role in people’s lives and determines the prosperity of a country as a whole. Understanding economic processes helps to make sensible decisions and successfully manage resources in a dynamic world.

One important area in economics is international economics. It studies economic relations between large countries and international organizations, as well as transnational corporations involved in foreign trade and investment operations. International economics is of great importance for global economic stability, development of trade relations and improvement of people’s lives in different countries.

Another important aspect of economics is the role of the state in the economy. The government influences economic processes through tax policy, fiscal policy, and monetary policy. The role of the state in the economy can vary from a free market to state planning. Most countries choose a middle ground, combining elements of a free market economy with government regulation.

Economics is also related to many other areas, such as sociology, politics, ecology, innovation, and technology. Studying economics helps to understand many aspects of people’s lives, as well as creates opportunities to develop new business models, technologies, and products that can make people’s lives better.

После перевода мы попросили определить текст на критерий «машинности».

Результат, полученный при проверке текста при помощи классификатора от DetectGpt (https://detectgpt.com/)

Результат, полученный при проверке текста при помощи классификатора от DetectGpt (https://detectgpt.com/)
Результат, полученный при проверке текста при помощи классификатора от OpenAI (https://platform.openai.com/ai-text-classifier)

Результат, полученный при проверке текста при помощи классификатора от OpenAI (https://platform.openai.com/ai-text-classifier)

Большинство из доступных детекторов также сообщили о том, что текст сгенерирован ИИ. Однако, антиплагиат.ру пока что не научился определять машинные тексты на английском.

Как образовалось направление классификации текста.

 

Потребность в классификации текста возникла ещё в конце 20-го века, когда с массовым распространением интернета и развитием отрасли информационных технологий стало возможным создание, накопление и обработка огромного объёма информации, включая текст.

Со временем было реализовано множество алгоритмов, однако они также могут быть применимы при классификации других типов данных. Рассмотрим некоторые из них:

1. Naive Bayes — вероятностный классификатор, основанный на предположении о независимости признаков. Он базируется на теореме Байеса, описывающей вероятность того, что событие А произойдет при условии, что случится событие B.  Каждый объект рассматривается как набор характеристик или признаков, а каждый признак описывается вероятностной моделью. Алгоритм определяет вероятность того, что объект относится к определенному классу, основываясь на вероятности каждого признака по данному классу. 

2. Метод опорных векторов (SVM): он делит текст на несколько категорий с помощью гиперплоскости, разграничивающей наборы данных. Этот метод является самым популярным для классификации текста. Но он имеет серьёзный недостаток, поскольку крайне неэффективен при работе с большим объёмом текста. Для этого могут потребоваться огромные вычислительные мощности.

3. Метод k-ближайших соседей (k-NN): используется для классификации на основе схожести текстов с другими наборами данных. Он работает путем поиска k ближайших соседей текста и определения наиболее часто встречающегося класса в этом множестве.

Этот метод имеет несколько недостатков. Во-первых, он чувствителен к выбору значения k и к типу метрики расстояния. Во-вторых, он может быть вычислительно дорогостоящим для больших наборов данных, поскольку требует измерения расстояния до каждого образца в обучающем наборе данных. В-третьих, он может испытывать сложности при работе с высоко размерными данными из-за «проклятия» размерности.

4. Древовидные алгоритмы классификации, включая метод случайные леса (Random Forests), применяются для многоклассовой классификации. С их помощью создаются решающие деревья на основе заданных правил для каждой категории и используются для классификации новых данных.

Однако, у Random Forests также есть некоторые недостатки. Во-первых, случайные леса могут быть довольно сложными и непрозрачными. Их часто называют «черными ящиками», поскольку они могут делать прогнозы без простого объяснения того, как пришли к этому прогнозу. Во-вторых, они могут быть вычислительно дорогостоящими и требовать много памяти, особенно при работе с большими наборами данных. В-третьих, они могут быть чувствительными к шуму и выбросам в данных.

Теперь перейдём к обзору теоретических и практических методов атак с использованием текста.

Атаки на уровне слов

Методы атаки на уровне слов генерируют состязательные примеры путем незначительных изменений отдельных слов в исходном тексте. Обычно это делается через:

1.    Замену слов в тексте синонимами, чтобы трансформировать форму, но сохранить близкое значение. Например, вместо «отличный» пишут «превосходный»;

2.    Перестановку близких по смыслу слов. Например, меняют местами «большой» и «огромный»;

3.    Вставки, удаления или замены определенных маркеров. Например, вставляют «не» перед глаголом или убирают «очень» из прилагательного;

4.    Замену предлогов и союзов. Поскольку эти части слов слабо влияют на семантику предложения, их легко заменить другими, не изменив смысла, но сбив ML-модель;

5.    Расстановку запятых. Добавление, удаление или перемещение запятых может повлиять на структуру предложения и его смысловую разметку, что приводит к ошибке ML-модели.

Основная идея атак на уровне слов — вносить минимальные локальные изменения, которые незначительно влияют на человеческое понимание текста, но могут сбить ML-модель. Это позволяет генерировать более естественно звучащие и грамматически корректные состязательные примеры.

В статье Phrase-level Textual Adversarial Attack with Label Preservation  был предложен метод для генерации текстов, создающий состязательные примеры через внесение изменений на уровне словосочетаний.

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

Эксперименты проводились на наборах данных Yelp Reviews, AG News, MNLI и QNLI. Предложенный метод показал лучшие результаты по сравнению с существующими методами текстовых состязательных атак по таким метрикам, как частота успешных атак, perplexity и сохранение меток.

Основными недостатками предложенного метода являются:

1.    Медленная скорость генерации состязательных примеров. Метод использует достаточно тяжеловесные языковые модели (BART, RoBERTa) для генерации возможных замен фраз и оценки сохранения меток, что требует значительных вычислительных ресурсов. Авторы указывают, что для генерации 100 состязательных примеров требуется около 160 минут на GPU NVIDIA GTX 1080 Ti;

2.    Невозможность полностью сохранить смысл исходного текста. Как показывает человеческая оценка, даже при использовании фильтра сохранения меток предложенный метод не может гарантировать 100% сохранение смысла текста после атаки. Это свойственно практически всем существующим методам текстовых состязательных атак;

3.    Зависимость от размеченных данных. Метод использует языковые модели, обученные на размеченных по классам данных, для оценки вероятности сохранения метки. Это ограничивает применение метода на датасетах с небольшим количеством размеченных данных;

4.    Уязвимость метода к усилению защиты моделей. Хотя он показывает хорошие результаты атаки на стандартные BERT-модели, предварительно обученные с использованием методов защиты от атак словарного уровня, его эффективность против моделей с более сильной защитой остается неизученной.

 

Незаметные атаки

Недавно на arxiv была опубликована статья Bad Characters: Imperceptible NLP Attacks . В ней исследователи предложили ряд атак методом чёрного ящика. Каждая из них является незаметной для человеческого глаза, но помогает обмануть NLP-системы по обработке естественного языка. Атаки, описанные в статье, работают, в том числе, на моделях с открытым исходным кодом, разработанных Microsoft, Facebook и IBM. Рассмотрим подробнее некоторые из них.

Атаки при помощи невидимых символов

Данный сценарий подразумевает использование символов, которые по спецификации не имеют глифа при отрисовке, для внесения изменений во входные данные модели. Например, символы U+200B (ZERO WIDTH SPACE), U+200C (ZERO WIDTH NON-JOINER) и U+200D (ZERO WIDTH JOINER) в Unicode.

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

Атаки при помощи омоглифов

Ее суть состоит в использовании символов, которые отображаются одинаково или очень похоже, для внесения изменений во входные данные модели. Например, латинская A и кириллическая А могут быть взаимозаменяемыми в тексте, что позволяет обмануть машинный классификатор. У атаки при помощи омоглифов есть большое преимущество. Оно заключается в том, что на классификацию такого текста может потребоваться гораздо больше ресурсов. Чаще всего использование глифов может привести к некорректному срабатыванию классификатора, из-за которого злоумышленник получит высокие баллы.

Очевидный недостаток этого метода состоит в том, что шрифт, которым написан текст, может быть различным для букв-омоглифов. Это может быть заметно человеческому глазу. И то, что на некоторые буквы нет омоглифа –«Э» или «П» — прямо подтверждает этот факт. Но будем честны: и слов, которые состоят только из одной буквы «Э» или буквы «П», также нет. [КЮ6] 

Изменение порядка

Заключается в применении символов управления направлением для изменения порядка отображения символов без изменения их кодирования. Это позволяет манипулировать порядком символов, передаваемых модели.

Удаления

Метод «Удалений» может быть использован при атаках на NLP-модели. Он основан на применении небольшого количества управляющих символов в Юникоде, таких как backspace (BS), delete (DEL) и carriage return (CR), которые могут привести к удалению соседнего текста или перезаписи его содержимого.

Атаки с использованием этого метода не зависят от шрифта или платформы, но их сложнее использовать на практике, так как большинство систем не копируют обрабатываемый текст в буфер обмена.

Авторы исследования реализовали сайт для того, чтобы можно было сгенерировать текст, который будет заведомо вредоносным: https://imperceptible.ml/generator. Им мы и воспользуемся, чтобы проверить недостатки классификаторов машинного текста.

Результаты

Метод с символами нулевой ширины

Классификатор от OpenAI (русский текст):

Антиплагиат (русский текст):

(помечает как странный)

DetectGpt (английский текст):

Не может определить текст. Возможно, это перебои сервиса.

Zerogpt (русский текст): текст полностью написан человеком (0% машинности).

detectgpt.ericmitchell (английский текст): Your text is 3571 GPT-2 tokens! To not 🤯 our GPUs, keep it under 256 tokens.

 

Метод с омоглифами

Классификатор от OpenAI (русский текст):

Антиплагиат (русский текст): 95 процентов уникальности.

DetectGpt (английский текст): 90 процентов уникальности.

Zerogpt (русский текст): текст полностью написан человеком (0% машинности).

detectgpt.ericmitchell (английский текст): Your text is 2213 GPT-2 tokens! To not 🤯 our GPUs, keep it under 256 tokens.

 

Метод с изменением порядка

Классификатор от OpenAI (русский текст):

 

Антиплагиат (русский текст): 90 процентов уникальности (однако сообщил о том, что текст странный»).

DetectGpt (английский текст): 90 процентов уникальности.

Zerogpt (русский текст): текст полностью написан человеком (0% машинности).

detectgpt.ericmitchell (английский текст): Your text is 32696 GPT-2 tokens! To not 🤯 our GPUs, keep it under 256 tokens.

Стоит заметить, что текст, преобразованный таким методом, может доставить трудности при чтении человеком. Также было обнаружено, что на английских текстах Zerogpt выдаёт результат в 46%.

Это говорит о том, что ресурс также плохо обучен на русском языке. На Reddit удалось найти комментарии о том, что Zerogpt – полностью случайный, а также что Zerogpt определяет библию, как текст, который написан при помощи GPT. Это является хорошим подтверждением несостоятельности NLP- классификаторов на данный момент.

Однако, удалять Zerogpt из ресурсов, которые мы тестируем, будет нецелесообразно, ибо он является одним из популярных инструментов, а значит ему доверяют.

Метод с удалениями

При использовании такого метода текст становится нечитаемым для человека, поэтому его нельзя считать эффективным для нас.

В ходе анализа методов, приведённых выше, нам удалось обнаружить, что некоторые эффективны при атаке на классификаторы. К сожалению, большинство классификаторов сейчас не могут определять подобные атаки.

В следующей части статьи мы рассмотрим не менее интересные методы, а также способы защиты от подобных атак. А пока вместо заключения предлагаем ознакомиться с интересным фактом о GPT.

 

Это интересно

Для обучения ChatGPT и GPT-4 использовалось множество книг, однако точный список обучающих данных является неизвестным, поскольку эти модели закрытые. Наши эксперименты показали, что ChatGPT и GPT-4 хорошо знают следующие книги:

  • Классические литературные общедоступные произведения (до 1923 года), такие как «Алиса в Стране чудес» Льюиса Кэрролла, «Красное Пятно» Натаниеля Готорна и «Гордость и предубеждение» Джейн Остин

  • Современные научно-фантастические романы, такие как «Гарри Поттер» Дж. К. Роулинг, «Хищные игры» Сьюзен Коллинз, «Властелин колец» Дж. Р. Р. Толкина и «1984» Джорджа Оруэлла

  • Современные бестселлеры, такие как «Пятьдесят оттенков серого» Э. Л. Джеймс.

Модели плохо знают глобальную англоязычную литературу. Это говорит о том, что данные, использованные для обучения этих моделей, использовались предвзято.   


ссылка на оригинал статьи https://habr.com/ru/articles/751560/

Мобильное приложение для nopCommerce: наш опыт кроссплатформенной разработки

Допустим, перед вашей командой стоит задача разработать мобильное приложение для крупного eCommerce проекта. Современные технологии развиваются столь стремительно, что порой непросто отдать предпочтение той или иной технологии. Какие-то из них набирают обороты, некоторые уже находятся на пике своего развития, а есть и те, которые постепенно угасают, уступая место более продвинутым решениям. 

Команда nopCommerce должна была не просто создать проект мобильного приложения, но и сформировать задел на будущее, чтобы нам не пришлось удерживать устаревший проект на плаву, а можно было бы легко и просто интегрировать передовые технологии в области разработки мобильных приложений в наше готовое решение.  

В этой статье мы разберем основные шаги, которые прошла команда nopCommerce, создавая свое мобильное приложение и поделимся с вами накопленным опытом кроссплатформенной разработки.

Выбор инструментов разработки (Flutter)

Сперва необходимо определиться с выбором платформы/фреймворка, на котором предстоит разрабатывать проект. 

Мы анализировали варианты в том числе и с точки зрения бизнеса. На самом деле выбор стоял между двумя направлениями:

  1. Создание нативного приложения, а по факту это должно быть два приложения: одно для Android и второе для iOS. По сути, это вариант “Качественно и Дорого”, поскольку скорость выведения на рынок готового решения определенно больше, чем у конкурирующего подхода (то же самое касается и скорости выхода обновлений). Это направление больше подходит для очень крупных компаний, которые могут себе позволить содержать две команды разработчиков (Swift и Kotlin). Экономические издержки этого направления самые высокие, но если стоит задача сделать максимально адаптированное приложение для каждой из платформ, то это определенно лучший вариант.

  1. Кроссплатформенный подход. Главная ценность этого подхода — ориентирование на Android и iOS из одной и той же кодовой базы. Этот вариант позволяет выкатить на рынок готовый продукт в максимально короткие сроки. Однако существует некоторая особенность касательно функциональных возможностей в сравнении с нативным подходом, поскольку так или иначе такие приложения получают поддержку нативных фич с некоторым запозданием. Тем не менее, на сегодняшний день этот разрыв, если и существует, то никакого решающего значения при выборе подхода не несет, особенно в eCommerce направлении, поскольку существует огромное множество сторонних библиотек и плагинов, покрывающих любые функциональные потребности. Однозначно этот подход является самым экономически эффективным вкупе с минимальным сроком выхода на рынок. Именно эти факторы подталкивают к небывалому развитию фреймворков, которые играют на этом поле. Для нас выбор был очевидным!

Теперь, давайте разберемся, какой именно фреймворк взять на вооружение. Наша команда выбрала Flutter и вот почему:

  1. Кривая обучения и порог вхождения в понимание платформы должен быть простым и быстрым — это на самом деле очень важный показатель, он говорит о зрелости и технологичности платформы.

  1. Интеграция с популярными редакторами — крайне важно, чтобы было удобно не только писать код, но и производить отладку, просматривать дерево виджетов, анализировать утечки памяти. Flutter имеет отличную интеграцию с VSCode, который хорошо знаком разработчикам .NET. 

  1. Горячая перезагрузка (Hot reload) — еще одна особенность, которая реально экономит время и нервы разработчика, ускоряя процесс разработки. В нативной разработке ничего подобного нет.

  2. Производительность — по этому показателю Flutter показывает отличные результаты. По заявлениям разработчиков, фреймворк поддерживает частоту до 60 кадров в секунду, это позволяет делать приложения по скорости близкими к нативным. Мы это проверили и провели собственные испытания производительности (подробнее об этом чуть ниже).

Производительность

Перед началом разработки мы решили исследовать производительность отрисовки графического интерфейса, поскольку в нашем приложении основные расходы ресурсов идут именно на эти цели. За основу мы взяли очень простую задачу — отрисовку с анимацией рекламного слайдера. Отслеживать будем потребление памяти, FPS, загрузку CPU с помощью инструмента GameBench

Для тестирования был реализован один и тот же интерфейс нативно на Android, на React Native и на Flutter. Время теста в каждом случае было фиксировано. Для кэширования изображений использовались библиотеки на каждой платформе. Условия тестирования максимально одинаковые. Давайте посмотрим, что из этого вышло.

Интерфейс для тестирования разных фреймворков

Интерфейс для тестирования разных фреймворков

В каждом показателе на диаграмме (кроме FPS) лучше считается наименьший результат.

Диаграмма

Сравнительная производительность трех фреймворков

FPS — во всех случаях результат очень близкий, Flutter свои заявленные значения выдает стабильно. Визуально разницы конечно не заметно.

Memory — тут мы видим уже более наглядную разницу, Flutter потребляет вдвое больше памяти, чем нативная реализация. Однако еще хуже дела обстоят с React Native.

CPU — и снова React Native тут выглядит явным аутсайдером, причина тому — использование JS Bridge между JS и Native code.

Battery — конечно, нативная реализация лучше всего оптимизирована под систему Android, кроме того, из предыдущих результатов логичный вывод о потреблении энергии напрашивается сам по себе, и Flutter в этом показателе занимает заслуженное второе место.

Даже на таком простом тесте мы видим разницу в подходах, хотя повторюсь, что для eCommerce проекта производительность не играет решающей роли. 

Из альтернатив нативной разработке — Flutter по итогу является предпочтительным вариантом.

Архитектура приложения

Когда вы планируете разработку сложных приложений, то правильный выбор архитектуры имеет решающую роль при дальнейшем развитии проекта, его масштабируемости и в конце концов позволяет грамотно структурировать код, что делает работу команды над таким проектом более прозрачной и слаженной. 

Хорошая архитектура должна пронизывать весь проект, помогая справиться с его сложностью, но никак не усложнять код надуманными логическими уровнями или чрезмерным обобщением. Иначе процесс разработки будет очень трудоемким, а сопровождение кода будет почти невозможным.

Архитектура нашего приложения построена по общепринятым стандартам, описанным в документации Android. Но поскольку она не учитывает специфики Flutter, нам необходимо модифицировать ее.

Давайте рассмотрим ее более детально. По сути, она состоит из трех уровней.

Архитектура Android приложения

Архитектура Android приложения

Поскольку в проекте используется система управления состоянием Riverpod, архитектура была расширена и выглядит так:

Архитектура мобильного приложения nopCommerce

Архитектура мобильного приложения nopCommerce

Как видите, она во многом повторяет каноничную архитектуру Android, но в ней немного перераспределены уровни. Рассмотрим подробнее изменения применяемые в нашей архитектуре.

Domain Layer — на этом уровне описаны доменные модели, которые могут быть изменены сервисами на уровне приложений (Application Layer), и они же заполняются репозиториями на уровне данных (Data Layer). Важно отметить, что эти модели должны описывать объекты бизнес логики, с которыми вы работаете через репозитории.

Application layer — по своему предназначению он схож с Domain Layer из Android архитектуры. На этом уровне сосредоточены классы-сервисы описывающие методы, которые будут использовать контроллеры. Это очень сильно помогает в том случае, когда одной и той же логикой управляет несколько виджетов или логика зависит от нескольких репозиториев. Таким образом, мы получаем более лучшее разделение задач. Стоит заметить, что прикладной уровень не нужно использовать повсеместно, контроллеры могут запрашивать данные и напрямую из репозиториев. Если сервисы просто перенаправляют вызовы методов из контроллеров в репозиторий — то прикладной уровень избыточен.

Клиент API (интеграция с Web API)

Когда речь заходит о взаимодействия с API, то всегда возникает вопрос о реализации клиента доступа и поддержанию его в актуальном состоянии. Но все становится чрезвычайно просто, если при создании API вы позаботились о том, чтобы оно было описано по спецификации OpenAPI 3. Это открывает возможность хранить все описание вашего API в схеме формата json. И как следствие — позволяет генерировать клиента в нужном нам языке, используя это формальное описание с помощью инструмента OpenAPI Generator.

Поскольку мы пишем приложение на Flutter, то клиента нам предстоит генерировать на языке Dart при помощи dart-dio генератора (Documentation for the dart-dio Generator). 

Схема генерации API клиента

Схема генерации API клиента

Когда клиент будет готов, он будет сгенерирован в виде пакета, остается только указать в приложении, что мы будем его использовать. Таким образом, мы быстро получили все методы поддерживаемые API. Остается лишь “научить” репозитории работать с этими методами.

В дальнейшем, когда методы API поменяют сигнатуру, или произойдут иные структурные изменения, вам достаточно будет просто перестроить клиента заново. Как видите, тут все очень просто!

Безопасность

Для защиты всех передаваемых данных в приложении используется “токен на предъявителя” (Bearer Token). Токен выдается только авторизованному пользователю и далее используется при обмене запросами между клиентом (мобильным приложением) и сервером.

Обмен между клиентом и сервером через токены

Обмен между клиентом и сервером через токены

Токен на стороне сервера можно отозвать, после чего все клиенты вынуждены будут пройти процедуру авторизации повторно и получить новый токен.

Возникает логичный вопрос — как будет храниться токен на стороне клиента? Ведь использование токена не требует от предъявителя доказательства владения, и тут очень важно держать эту информацию под контролем. Для этих целей во Flutter будем использовать защищенное локальное хранилище.

Локальное хранилище служит для сохранения данных пользователей до тех пор, пока приложение не будет удалено. Но для разных платформ используются разные технологии:

  • для Android используется шифрование AES. Секретный ключ зашифрован с помощью RSA алгоритма, ключ от которого хранится в KeyStore.

  • для IOS используется KeyChain (связка ключей) — это безопасное хранилище, используемое для доступа к криптографическим ключам приложения.

Таким образом, на любой платформе используются лучшие практики безопасности, делая токен защиты недоступным для чужих глаз. Эти меры безопасности помогают сохранить доверие пользователей к продукту, что мы считаем очень важным.

UI/UX

В основе интерфейса всегда лежит концепция, пронизывающая все приложение, которая является фундаментом для построения удобного и функционального дизайна. В основе нашего приложения лежит система Material Design 3 или как ее принято называть — Material You.

На сегодняшний день это самая передовая версия системы дизайна от Google. В ней переосмыслены визуализации компонентов, появились новые цвета и новая анимация — все это уже доступно для использования во Flutter. 

Вместе с тем Material Design предлагает системные (эталонные) токены для хранения значений стилей, типографики и анимации. Это открывает перед нами возможность использования одних и тех же значений стиля в файлах дизайна и коде.

Наша задача — сделать приложение так, чтобы все его компоненты выглядели одинаково в любом месте использования и самое главное — реализовать единое управление стилями этих компонентов. С этой задачей нам как раз помогают справиться токены дизайна (Design tokens). Давайте кратко посмотрим, как этот принцип работает.

Все, что нам нужно для создания Design Tokens, это Material Theme Builder. С его помощью можно сгенерировать тему с цветами и стилями по умолчанию. А уже на ее основе создать свою пользовательскую тему. 

Существует два способа указания цвета, на которых будет базироваться вся схема, обычно это цвета бренда:

  • указать базовые цвета вручную

  • использовать “динамический цвет” — цвета будут выбраны на основе предоставленного изображения (например логотипа компании).

После того как базовые цвета выбраны, на их основе будет сгенерирована вся палитра токенов согласно концепции Material You, но уже на основе ваших пожеланий. Это чрезвычайно просто!

Генерация палитры токенов

Генерация палитры токенов

Теперь появляется возможность управлять токенами для разных режимов приложения (Dark mode/Light mode), что делает пользовательский интерфейс невероятно гибким,  и позволяет менять стиль всего приложения буквально на лету, простым и логичным способом.

Управление режимами приложения

Управление режимами приложения

Итак, какие преимущества мы получили от использования токенов дизайна? 

  • Больше не нужно изучать руководства по дизайну конкретных платформ, достаточно загрузить токены и применять стили Material Design как в проектировании, так и в разработке. Это своего рода дизайн-документация вашего проекта.

  • возможность быстрого перестроения приложения под новый стиль основанный на Material Design

  • легкость поддержания и обновления стилей

Заключение

Если вы находитесь в процессе создания мобильного eCommerce приложения или подумываете над началом разработки, мы надеемся, что это руководство будет вам полезно. 

Мы постарались описать наш путь разработки мобильного приложения “крупными мазками”, останавливаясь лишь на наиболее крупных и важных этапах. Но если вы хотите больше узнать больше о получившемся MVP приложении для nopCommerce, обязательно посетите наш официальный сайт. 

 Чтобы не пропустить наши будущие обновления, ознакомьтесь с документацией по мобильному приложению.


ссылка на оригинал статьи https://habr.com/ru/articles/751430/

Миграция с Docker на CRI-O в Kubernetes

Для запуска контейнеров платформа Kubernetes использует Container runtime, т. е. движок для контейнеров. Долгое время основным движком считался Docker, но им не ограничивался — Kubernetes также поддерживает такие движки, как CRI-O и containerd. Однако начиная с версии Kubernetes 1.20, было объявлено, что в будущих версиях поддержка Docker (а если быть точнее, то dockershim) будет полностью прекращена. Начиная с версии Kubernetes 1.24, Docker был полностью удален из Kubernetes. Одной из причин такого решения было то, что в Docker отсутствует поддержка интерфейса CRI (Container Runtime Interface). В качестве замены можно перейти на CRI-O, который позиционирует себя как легковесную исполняемую среду для контейнеров в Kubernetes. В данной статье мы рассмотрим миграцию с движка Docker на CRI-O.

Технические характеристики кластера

Смена движка будет произведена на 4-узловом кластере Kubernetes, который состоит из 2x master (управляющих) нод и 2x worker (рабочих) нод:

Версии используемых программных компонентов:

  • Версия Kubernetes: 1.20.4

  • Версия Docker: 24.0.2

  • Версия CRI-O: 1.23

  • Операционная система: Ubuntu 20.04.6 LTS

Перед выполнением процедуры по смене контейнерного движка необходимо сделать резервную копию образов Docker, например перенесите их реестр (Nexus, Jfrog и т. д.), так как при удалении Docker все его данные (включая такие пользовательские, как образы) будут удалены.

Одно из самых важных требований во время смены движка для контейнеров — это наличие минимум 2х master (управляющих) нод кластера. При использовании одной master ноды есть риск того, что кластер не будет запущен, так как на master ноде расположены основные компоненты системы.

Вывод узлов из кластера

Начинать будем с worker (рабочих) нод, далее перейдем к master нодам. Для данной задачи нам понадобятся такие команды:

  • kubectl cordon — используется для блокировки ноды. После выполнения данной команды новые объекты типа pod не будут развёртываться на данной ноде. 

  • kubectl drain — используется для удаления подов и их перемещения на другие узлы кластера.

1) Блокируем первую ноду с именем k8s-worker2:

kubectl cordon k8s-worker2

2) Удаляем и переносим запущенные pod’ы:

kubectl drain k8s-worker2 --ignore-daemonsets

Опция —ignore-daemonsets предназначена для игнорирования размещённых на ноде объектов использующих DaemonSet.

3) Проверяем статус ноды:

kubectl get nodes

Как видно, у ноды k8s-worker2 появился статус SchedulingDisabled, что означает, что на данном узле выключена возможность по развертыванию pod’ов.

Следующие шаги выполняем на той же ноде, которую мы переводили в статус SchedulingDisabled (в данном случае это рабочая нода с именем k8s-worker2).

4) Останавливаем сервис kubelet и проверяем его статус:

sudo systemctl stop kubelet && sudo systemctl status kubelet

Дожидаемся, когда статус ноды перейдет в NotReady. Только после этого переходим к следующим шагам.

5) Останавливаем сервис docker и проверяем его статус:

sudo systemctl stop docker && sudo systemctl status docker

6) Удаляем пакеты docker с сервера:

sudo systemctl stop docker && sudo systemctl status docker

Команда apt purge удаляет также конфигурационные файлы. 

Установка CRI-O

Следующий шаг — установка движка CRI-O. Так как пакеты CRI-O отсутствуют в репозиториях ОС, мы воспользуемся репозиториями от разработчиков операционной системы openSUSE. 

1) Создадим переменные, содержащие версию используемого дистрибутива Ubuntu (20.04) и версию CRI-O, которую мы будем использовать (в данном примере это будет версия 1.23):

OS=xUbuntu_20.04 VERSION=1.23

2) Добавляем репозиторий от разработчиков openSUSE: 

echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list

3) Далее добавляем репозиторий, содержащий пакеты с CRI-O:

echo "deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list

4) Скачиваем ключ от репозитория:

curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key add –

5) Устанавливаем CRI-O и необходимые пакеты:

sudo apt update && sudo apt install cri-o cri-o-runc cri-tools -y

Настройка CRI-O

1) Для настройки crio необходимо задать cgroup драйвер — мы будем использовать systemd. Для этого в файле kubelet необходимо добавить следующую строку:

echo "KUBELET_EXTRA_ARGS=--cgroup-driver=systemd" | sudo tee /etc/default/kubelet

2) Загружаем необходимые сетевые модули из ядра ОС:

cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf overlay br_netfilter EOF

3) Активируем загруженные модули:

sudo modprobe overlay sudo modprobe br_netfilter

4) Применяем настройки. Команду ниже необходимо выполнять от имени пользователя root:

cat <<EOF >  /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 net.ipv4.ip_forward                 = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF

5) Перезапускаем параметры ядра:

sudo sysctl --system

 6) Прописываем адрес сокета с crio. Команду ниже необходимо выполнять от имени пользователя root:

echo "runtime-endpoint: unix:///var/run/crio/crio.sock" > /etc/crictl.yaml

7) Добавляем crio в автозагрузку и запускаем:

sudo systemctl enable crio && sudo systemctl start crio

8) Проверяем, что crio работает, выполнив команду для проверки запущенных контейнеров:

sudo crictl ps

9) Открываем для редактирования конфигурационный файл kubelet:

sudo nano /etc/default/kubelet

и прописываем следующие параметры:

KUBELET_EXTRA_ARGS=--feature-gates="AllAlpha=false" --container-runtime=remote --cgroup-driver=systemd --container-runtime-endpoint='unix:///var/run/crio/crio.sock' --runtime-request-timeout=5m

Сохраняем изменения и выходим из файла.

10) Запускаем сервис kubelet:

sudo systemctl start kubelet

11) Возвращаемся на master ноду и включаем ранее выключенный узел:

kubectl uncordon k8s-worker2

12) Проверяем, что нода запущена (статус READY):

kubectl get nodes

А также проверим, что движок с Docker был изменен на crio:

kubectl get -o wide

Далее выполняем все предыдущие шаги на остальных нодах кластера. Мастер ноды будут подниматься намного дольше, чем рабочие. После того как все ноды кластеры были переведены на новый движок, проверяем, что везде используется crio (столбец CONTAINER_RUNTIME):


НЛО прилетело и оставило здесь промокод для читателей нашего блога:

— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS


ссылка на оригинал статьи https://habr.com/ru/companies/first/articles/751396/