Кодировки, часовые пояса, время на сервере

↑ 1.2 ОС и Linux для разработчика · ← Предыдущая

Желательно~6 мин чтенияУровень: junior

Зачем это на собесе

Баги со временем и кодировками — самые «дорогие» и незаметные: «кракозябры» в PDF и CSV, записи, «уехавшие» на час после перехода на летнее время, разные результаты на сервере и у клиента. Вопрос «как хранить и передавать время?» задают почти всегда.

Подтемы

  • Unicode и UTF-8, BOM, кодировки в .NET
  • Время: UTC, часовые пояса, DST
  • DateTime, DateTimeOffset, DateOnly, TimeProvider
  • Хранение времени в PostgreSQL
  • Часы на сервере: NTP, дрейф, монотонное время

Объяснение

Кодировки

  • Unicode — таблица символов (code points). UTF-8 — кодировка переменной длины (1–4 байта), совместима с ASCII; стандарт для веба, JSON и Linux.
  • UTF-16 — внутреннее представление string в .NET (символ = 1–2 char, суррогатные пары для эмодзи).
  • BOM (EF BB BF) — метка порядка байтов в начале файла; нужна некоторым программам (Excel для CSV в UTF-8), но ломает парсеры JSON и shell-скрипты.
  • Причина «кракозябр» — данные закодированы одной кодировкой, а прочитаны другой (UTF-8 vs Windows-1251).
var bytes = Encoding.UTF8.GetBytes("Привет");          // кодирование
var text  = Encoding.UTF8.GetString(bytes);            // декодирование
 
// CSV для Excel: UTF-8 с BOM
await File.WriteAllTextAsync("report.csv", csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
 
// Длина «видимых» символов, а не char
var info = new System.Globalization.StringInfo("e\u0301");
Console.WriteLine(info.LengthInTextElements);          // 1
  • В HTTP кодировку задаёт Content-Type: application/json; charset=utf-8. System.Text.Json работает с UTF-8.
  • В PostgreSQL кодировку базы задают при создании (UTF8); collation влияет на сортировку и сравнение строк.

Время: главное правило

Хранить и передавать в UTC, переводить в локальное время только на границе показа пользователю.

  • UTC — универсальная шкала без переходов. Часовой пояс — правило смещения (IANA: Europe/Moscow, America/New_York), у которого бывают переходы на летнее время (DST) и исторические изменения.
  • Смещение (+03:00) ≠ часовой пояс: смещение — число на конкретный момент, пояс — набор правил.
  • Формат обмена: ISO 8601 (2026-09-30T12:31:14Z или с +03:00).

Типы времени в .NET

ТипЧто хранитКогда использовать
DateTimeтики + Kind (Utc/Local/Unspecified)осторожно: Kind легко потерять
DateTimeOffsetмомент времени + смещениемомент события (создан, оплачен)
DateOnly / TimeOnlyдата / время суток без поясадень рождения, расписание
TimeSpanдлительностьинтервалы, таймауты
TimeZoneInfoправила поясаконвертация в локальное время пользователя
var utc = DateTimeOffset.UtcNow;
var tz  = TimeZoneInfo.FindSystemTimeZoneById("Europe/Moscow");   // IANA id работает на Linux и Windows (.NET 6+)
var local = TimeZoneInfo.ConvertTime(utc, tz);
 
// Тестируемое время: TimeProvider (встроен с .NET 8)
public class TokenService(TimeProvider clock)
{
    public bool IsExpired(DateTimeOffset expiresAt) => clock.GetUtcNow() >= expiresAt;
}
// в тесте: var fake = new FakeTimeProvider(); fake.Advance(TimeSpan.FromMinutes(5));

Время в PostgreSQL

  • timestamptz — момент времени; внутри хранится в UTC, при выводе конвертируется в пояс сессии. Использовать по умолчанию.
  • timestamp (без tz) — «настенное» время без пояса, подходит для будущих запланированных событий в локальном времени.
  • Npgsql 6+: DateTime с Kind=Utc ↔ timestamptz; попытка записать Kind=Local или Unspecified в timestamptz — исключение. Это защита, не баг.

Часы сервера

  • Часы серверов расходятся (дрейф) — синхронизация по NTP (chrony, systemd-timesyncd). Расхождение ломает JWT (nbf/exp), TOTP, подписи запросов, порядок событий в логах.
  • Для измерения длительности используйте монотонные часы (Stopwatch, Environment.TickCount64), а не разность DateTime.UtcNow: системное время может скакнуть назад.
  • Контейнер использует часы хоста; часовой пояс контейнера по умолчанию UTC (TZ переменная и tzdata в образе для локального пояса; в chiseled/alpine-образах tzdata может отсутствовать → TimeZoneNotFoundException).

Нюансы и подводные камни

  • DateTime.Now на сервере даёт пояс сервера — результат зависит от окружения. В серверном коде используйте UtcNow или TimeProvider.
  • Переход на летнее время: локальные времена бывают несуществующими (пропущенный час) и неоднозначными (повторяющийся час) — TimeZoneInfo.IsInvalidTime / IsAmbiguousTime.
  • «Каждый день в 03:00» — cron в UTC даст сдвиг для пользователей с DST; задача, зависящая от локального времени, планируется с учётом пояса.
  • Сравнение дат без времени: date.Date и границы суток в UTC ≠ границы суток пользователя.
  • JSON: DateTime без Z десериализуется с Kind=Unspecified; убедитесь, что клиенты шлют Z или смещение.
  • Не сортируйте и не сравнивайте даты как строки в нестандартных форматах.

Вопросы с ответами

Связанные темы