Перейти к содержимому

Лекция 13. Сетевое взаимодействие. Часть 2: надёжность, оптимизация, безопасность

Введение

В предыдущей лекции мы разобрали основы сетевого взаимодействия: протоколы, форматы данных, работу с fetch и REST API в Expo/React Native. Но в реальном приложении одного «умения отправить запрос» недостаточно. Мобильная сеть нестабильна: пользователь заходит в метро, переключается с Wi-Fi на сотовую сеть, теряет сигнал. Сервер может временно не отвечать, а злоумышленник в публичной сети — попытаться перехватить трафик. Эта лекция о том, что отличает учебный код от рабочего приложения:

  • как классифицировать сетевые ошибки и правильно на них реагировать;
  • как грамотно повторять запросы (retry с экспоненциальной задержкой);
  • как кэшировать данные, чтобы приложение оставалось отзывчивым;
  • как оптимизировать сетевой трафик (батчинг, дедупликация, пагинация, сжатие, отмена запросов);
  • как защитить передаваемые данные и токены;
  • какие современные технологии (Edge Computing, GraphQL, gRPC) меняют подход к сетевому слою.

1. Классификация сетевых ошибок

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

1.1. Три класса ошибок

КлассПримерыПриродаПравильная реакция
Временные (transient)Таймаут, потеря сети, 503 Service Unavailable, 429 Too Many RequestsСостояние временное, скоро пройдётПовторить запрос с задержкой (retry + backoff)
Постоянные (permanent)400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422Ошибка в самом запросе или правахНЕ повторять; исправить запрос, обновить токен или показать сообщение
Системные (fatal/client)500 Internal Server Error, краш парсинга JSON, ошибка в коде клиентаСбой на стороне сервера или баг клиентаЗалогировать, уведомить пользователя, при 500 возможен ограниченный retry

Ключевая идея: повторять имеет смысл только временные ошибки. Повтор постоянной ошибки (например, 400) бесполезен — запрос так и останется некорректным, мы лишь зря потратим трафик и батарею.

1.2. Как различать на практике

Правило простое: отсутствие ответа (нет сети, таймаут) и коды 408, 429, 5xx считаем временными (transient) и пробуем повторить; остальные 4xx (400, 401, 403, 404, 422) — постоянные (permanent), повторять бессмысленно.

Особый случай — 401 Unauthorized: формально это постоянная ошибка, но зачастую её можно «вылечить» один раз, обновив access-токен через refresh-токен, после чего повторить исходный запрос.


2. Стратегия повторных попыток (retry)

2.1. Зачем нужна экспоненциальная задержка

Наивный подход «повторять сразу же, пока не получится» вреден: если сервер перегружен, сотни клиентов будут долбить его одновременно, усугубляя проблему. Это явление называют thundering herd («стадо громовержцев»).

Решение — экспоненциальная задержка (exponential backoff): каждая следующая попытка ждёт вдвое дольше предыдущей. К ней добавляют jitter (случайный разброс), чтобы клиенты не синхронизировались и не приходили на сервер ровно в один и тот же момент.

Попытка 1 → ждём ~1 c
Попытка 2 → ждём ~2 c
Попытка 3 → ждём ~4 c
Попытка 4 → ждём ~8 c (но не больше maxDelay)

2.2. Алгоритм retry + backoff + jitter (псевдокод)

ФУНКЦИЯ retry(operation, maxRetries, baseDelay, maxDelay):
attempt ← 0
ПОВТОРЯТЬ:
ПОПРОБОВАТЬ:
результат ← ВЫПОЛНИТЬ operation()
ВЕРНУТЬ результат // успех
ПЕРЕХВАТИТЬ error:
ЕСЛИ classify(error) ≠ "transient": // постоянная/системная —
ВОЗБУДИТЬ error // повторять нельзя
ЕСЛИ attempt ≥ maxRetries:
ВОЗБУДИТЬ error // попытки кончились
// экспоненциальная задержка
delay ← MIN(baseDelay × 2^attempt, maxDelay)
// jitter: случайный множитель 0.5..1.0 («full jitter»)
delay ← delay × СЛУЧАЙНОЕ(0.5, 1.0)
ЖДАТЬ delay
attempt ← attempt + 1

Реализация на JavaScript для React Native:

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function retry(operation, { maxRetries = 3, baseDelay = 1000, maxDelay = 16000 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await operation();
} catch (error) {
if (!error.isTransient || attempt >= maxRetries) throw error;
const backoff = Math.min(baseDelay * 2 ** attempt, maxDelay);
await sleep(backoff * (0.5 + Math.random() * 0.5)); // full jitter
}
}
}

2.3. Когда НЕ нужно повторять: идемпотентность

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

HTTP-методИдемпотентен?Можно повторять?
GETДаДа, безопасно
PUTДаДа
DELETEДаДа
HEADДаДа
POSTНетОсторожно — может создать дубликат

Опасность: если мы отправили POST /orders (создать заказ), не дождались ответа из-за таймаута и повторили запрос — заказ может создаться дважды. Поэтому:

  • Не повторяйте POST вслепую.
  • Для критичных операций используйте ключ идемпотентности — уникальный идентификатор операции в заголовке (Idempotency-Key), по которому сервер распознаёт и отбрасывает дубликаты.
  • Повторяйте только при сетевых ошибках/5xx, но не при 4xx.

2.4. Circuit Breaker («предохранитель»)

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

  • Closed (замкнут) — запросы проходят нормально; считаем число ошибок.
  • Open (разомкнут) — после серии ошибок запросы мгновенно отклоняются без обращения к сети (экономим ресурсы, даём серверу прийти в себя).
  • Half-Open (полуоткрыт) — через паузу пропускаем один пробный запрос: успех → возвращаемся в Closed, неудача → снова Open.

3. Кэширование данных

Кэш — это локальная копия данных, позволяющая ответить пользователю быстро и работать при слабой сети или офлайн. Различают уровни кэша: память (быстро, но теряется при закрытии), персистентное хранилище (AsyncStorage, SQLite) и HTTP-кэш на уровне заголовков (Cache-Control, ETag).

3.1. Три стратегии (с акцентом на сеть)

СтратегияКак работаетКогда применятьМинусы
Cache-FirstСначала кэш; сеть — только если в кэше пустоРедко меняющиеся данные: справочники, профиль, картинкиМожно показать устаревшие данные
Network-FirstСначала сеть; при ошибке — fallback на кэшЧасто меняющиеся данные, где важна свежесть: лента, балансМедленнее при плохой сети
Stale-While-RevalidateСразу отдаём кэш («stale»), параллельно идём в сеть и обновляемУниверсальный баланс скорости и свежести (ленты, списки)Интерфейс «дёргается» при обновлении

3.2. Stale-While-Revalidate в коде

async function swr(key, fetcher, onUpdate) {
const cached = await cache.get(key);
if (cached) onUpdate(cached); // 1. мгновенно показали кэш
try {
const fresh = await fetcher(); // 2. пошли в сеть
await cache.set(key, fresh);
onUpdate(fresh); // 3. обновили UI свежими данными
} catch (e) {
if (!cached) throw e; // нет ни кэша, ни сети — ошибка
}
}

Именно по принципу SWR работают популярные библиотеки серверного состояния (React Query, SWR): они кэшируют ответы по ключу запроса и фоном их обновляют.

3.3. Инвалидация кэша

«В компьютерных науках есть две сложные задачи: инвалидация кэша и именование переменных». Основные подходы к устареванию кэша:

  • TTL (Time To Live) — у записи есть срок жизни; по истечении считается устаревшей.
  • ETag / условные запросы — сервер присылает «отпечаток» данных; клиент при следующем запросе шлёт If-None-Match, и если данные не менялись, сервер отвечает 304 Not Modified без тела (экономия трафика).
  • Событийная инвалидация — после мутации (POST/PUT/DELETE) принудительно сбрасываем связанные ключи кэша.

4. Оптимизация производительности сети

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

4.1. Батчинг запросов

Батчинг — объединение нескольких логических запросов в один сетевой вызов. Вместо трёх отдельных обращений (user, posts, stats) отправляем один пакет с массивом операций, а сервер возвращает массив ответов. Меньше запросов — меньше накладных расходов на установление соединения, заголовки и «пробуждение» радиомодуля (что важно для энергопотребления).

4.2. Дедупликация запросов

Если несколько компонентов экрана одновременно запросили одни и те же данные, нет смысла слать несколько одинаковых запросов. Дедупликация означает: для одинакового ключа держим один «летящий» запрос (in-flight), а остальным отдаём тот же Promise.

const inFlight = new Map();
function dedupedFetch(key, fetcher) {
if (inFlight.has(key)) return inFlight.get(key); // уже летит — переиспользуем
const promise = fetcher().finally(() => inFlight.delete(key));
inFlight.set(key, promise);
return promise;
}

4.3. Пагинация

Никогда не загружайте «всё сразу». Пагинация разбивает большой набор данных на страницы.

  • Offset-based (?page=2&limit=20) — просто, но «съезжает» при вставке новых элементов и неэффективно на больших offset.
  • Cursor-based (?after=<курсор>) — сервер возвращает указатель на следующую порцию; устойчиво к вставкам, оптимально для бесконечных лент и FlatList с onEndReached.

4.4. Сжатие

  • Включайте сжатие тела ответа: заголовок Accept-Encoding: gzip, br (Brotli плотнее gzip). Текст и JSON сжимаются в разы.
  • Для изображений используйте современные форматы (WebP, AVIF) и отдавайте размер под экран устройства, а не оригинал.
  • Не передавайте лишние поля — запрашивайте только нужное (этому помогает GraphQL, см. ниже).

4.5. Отмена запросов: AbortController

Если пользователь ушёл с экрана или быстро поменял поисковый запрос, старый ответ уже не нужен — а он продолжает грузиться и может «перезаписать» актуальные данные (состояние гонки, race condition). Запрос нужно отменять.

В React Native для этого используется стандартный AbortController:

useEffect(() => {
const controller = new AbortController();
fetch('https://api.example.com/search?q=' + query, {
signal: controller.signal,
})
.then((r) => r.json())
.then(setResults)
.catch((e) => {
if (e.name !== 'AbortError') console.error(e); // отмену игнорируем
});
return () => controller.abort(); // отменяем при размонтировании / смене query
}, [query]);

Отмена особенно полезна для «живого поиска» (search-as-you-type): на каждый новый символ предыдущий запрос отменяется, в сети остаётся только актуальный.


5. Безопасность сетевого взаимодействия

Мобильное приложение часто работает в недоверенной сети (публичный Wi-Fi), поэтому безопасность канала и данных критична.

5.1. Транспортное шифрование (TLS)

  • Всегда используйте HTTPS (HTTP поверх TLS). На iOS это требование обеспечивается App Transport Security, открытый HTTP по умолчанию запрещён.
  • Современный стандарт — TLS 1.3: меньше задержек на рукопожатие, убраны устаревшие слабые алгоритмы.
  • Perfect Forward Secrecy (PFS) гарантирует, что компрометация долговременного ключа сервера не раскроет ранее перехваченный трафик.

TLS защищает от прослушивания и подмены данных «на проводе» (атака man-in-the-middle, MITM).

5.2. Certificate Pinning

Обычный TLS доверяет любому сертификату, подписанному доверенным центром (CA). Если злоумышленник скомпрометирует CA или подсунет собственный корневой сертификат на устройство, MITM снова возможен.

Certificate pinning («закрепление сертификата») — это «зашивание» в приложение ожидаемого сертификата сервера или его публичного ключа. При рукопожатии приложение сверяет сертификат сервера с закреплённым и рвёт соединение при несовпадении.

  • Плюс: сильная защита от MITM даже при скомпрометированном CA.
  • Минус: при смене сертификата на сервере нужно вовремя обновить приложение, иначе оно перестанет работать (поэтому закрепляют несколько ключей и предусматривают резервный).

5.3. Защита токенов

Токены (JWT access/refresh, API-ключи) — самые ценные данные приложения.

  • Хранение: не используйте обычный AsyncStorage для токенов — он не шифрован. Применяйте защищённое хранилище ОС: expo-secure-store (Keychain на iOS, Keystore/EncryptedSharedPreferences на Android).
  • Срок жизни: короткоживущий access-токен + долгоживущий refresh-токен. Утечка короткого токена менее опасна.
  • Передача: только по HTTPS, в заголовке Authorization: Bearer <token>, не в URL (URL попадает в логи и историю).
  • Отзыв: предусмотрите логаут/ротацию refresh-токенов на сервере.

5.4. Валидация на клиенте

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

  • Проверяйте и нормализуйте ввод перед отправкой (email, длина, типы) — это экономит трафик и улучшает UX, но не считается мерой безопасности.
  • Авторитетная проверка всегда на сервере.
  • Валидируйте и ответы сервера: проверяйте структуру JSON перед использованием.

Полезно держать в голове типичные угрозы из OWASP Mobile Top 10: небезопасное хранение данных, небезопасная передача, слабая аутентификация и авторизация.


6. Современные технологии и тренды

6.1. Edge Computing

Edge Computing («граничные вычисления») — это перенос части вычислений и хранения данных ближе к пользователю, на «край» сети, вместо обращения к далёкому центральному дата-центру.

  • Идея: запрос обрабатывается на ближайшем к пользователю узле (точке присутствия CDN), что резко снижает задержку (latency).
  • Технологии: Cloudflare Workers, AWS Lambda@Edge, Vercel Edge Functions — код выполняется в сотнях точек по всему миру.
  • Применение в мобильных приложениях: кэширование и персонализация ответов у границы, проверка авторизации, лёгкая обработка/агрегация данных, A/B-тесты — без обращения к основному серверу. Для мобильного пользователя в другой стране это разница между откликом в 30 мс и 300 мс.

6.2. GraphQL (обзорно)

GraphQL — язык запросов к API, где клиент сам описывает, какие именно поля ему нужны, через единую конечную точку (/graphql).

  • Решает проблемы REST: over-fetching (сервер прислал лишние поля) и under-fetching (пришлось делать несколько запросов).
  • Один запрос может собрать данные из связанных сущностей (пользователь + его посты + комментарии) за один сетевой вызов.
  • Строгая типизация схемы и поддержка подписок (real-time).
  • Для мобильных приложений это меньше трафика и меньше запросов; популярный клиент — Apollo Client.

6.3. gRPC (обзорно)

gRPC — высокопроизводительный RPC-фреймворк от Google поверх HTTP/2.

  • Данные сериализуются в бинарный формат Protocol Buffers — компактнее и быстрее JSON.
  • Строгий контракт через .proto-файлы, кодогенерация клиента и сервера.
  • Поддержка стриминга в обе стороны.
  • Применяется чаще для связи между микросервисами; на мобильных клиентах — там, где критичны производительность и низкая задержка, хотя интеграция сложнее, чем у REST/GraphQL.

Краткие итоги

  • Ошибки делятся на временные (повторять с задержкой), постоянные (4xx — не повторять) и системные (логировать, ограниченно повторять 5xx).
  • Retry строят на экспоненциальной задержке с jitter (против thundering herd). Повторять можно только идемпотентные операции; для POST — ключ идемпотентности. Circuit Breaker защищает падающий сервер.
  • Кэширование ускоряет работу и спасает в офлайне: Cache-First, Network-First, Stale-While-Revalidate; кэш инвалидируют (TTL, ETag, событийно).
  • Оптимизация: батчинг, дедупликация, пагинация (cursor-based), сжатие (gzip/Brotli, WebP/AVIF), отмена через AbortController.
  • Безопасность: обязательный TLS (HTTPS), при необходимости — certificate pinning, токены в secure storage, принцип «не доверяй клиенту» (валидация на сервере).
  • Тренды: Edge Computing снижает задержку у границы сети; GraphQL борется с over-/under-fetching; gRPC даёт бинарную производительность.

Вопросы для самопроверки

  1. На какие три класса делятся сетевые ошибки? Приведите примеры и объясните, почему повторять имеет смысл только временные ошибки.

  2. Что такое экспоненциальная задержка и jitter? От какой проблемы защищает jitter и как выглядит алгоритм retry с backoff?

  3. Что такое идемпотентность операции? Почему POST-запросы нельзя повторять вслепую и как это решается с помощью ключа идемпотентности?

  4. Сравните стратегии кэширования Cache-First, Network-First и Stale-While-Revalidate: как они работают и для каких данных подходят?

  5. Перечислите приёмы оптимизации сетевого трафика (батчинг, дедупликация, пагинация, сжатие, отмена). Для чего нужен AbortController и в каком сценарии он особенно полезен?

  6. Какими средствами обеспечивается безопасность сетевого взаимодействия? Объясните роль TLS, certificate pinning, защищённого хранения токенов и серверной валидации. Что такое Edge Computing и чем GraphQL отличается от REST?