Что показывает схема
DNS заканчивается списком адресных кандидатов, а не готовым соединением. Dual-stack client должен превратить этот список в одну успешную transport connection. Если код слепо берёт первый addrinfo, нерабочий IPv6 path способен скрыть исправный IPv4 endpoint, и наоборот.
Happy Eyeballs решает пользовательскую задержку не постоянным предпочтением IPv4, а упорядочиванием и staggered racing попыток. Клиент начинает с preferred candidate, после небольшой задержки разрешает следующую попытку и выбирает первый socket с успешно завершённым handshake. Конкретные delays являются частью реализации и могут учитывать историю RTT; схема намеренно не превращает одно рекомендованное число в вечную protocol constant.
Как читать компоненты
- getaddrinfo(AF_UNSPEC). Возвращает linked list с адресами разных families и параметрами socket.
- Ordered candidates. Результаты DNS проходят address-selection policy. Порядок — вход в алгоритм соединения, а не гарантия доступности.
- IPv6/IPv4 attempts. Для каждого candidate нужен socket той же family. Одновременно существующие nonblocking attempts имеют независимые handles и error states.
- Selected connection. Только завершившийся transport handshake превращает candidate в соединение, пригодное для TLS или прикладного протокола.
Три сценария
- IPv6 выигрывает. Preferred path быстро завершает handshake, лишние попытки не нужны.
- IPv4 fallback. IPv6 не даёт progress; клиент запускает IPv4 attempt после короткой stagger delay и не ждёт полного timeout первого пути.
- First-only trap. Наивная программа проверяет только первый addrinfo и сообщает ложный outage.
Предсказание до запуска
Пусть AAAA record существует, но маршрут IPv6 сломан после локального gateway, а A record ведёт к рабочему серверу. Что доказывает успешный getaddrinfo()? Только наличие candidates. Что доказывает connect success? Работоспособность одного выбранного transport path в момент проверки. Он ничего не говорит о втором path.
Практика на C и Winsock
Минимально корректный последовательный client проходит весь addrinfo list: socket(ai_family, ai_socktype, ai_protocol), connect(), closesocket() при неудаче и переход к следующему элементу. Это лучше first-only кода, но всё ещё может заставить пользователя ждать timeout каждого чёрного маршрута.
Для staggered racing нужны nonblocking sockets, отдельное состояние каждого attempt и ожидание readiness через select(), WSAPoll() или другую разрешённую модель курса. После writable event проверяйте SO_ERROR через getsockopt(): writable не равен «connect успешен». Победивший socket сохраняется, остальные закрываются. Не переиспользуйте один failed socket для адреса другой family.
Evidence: что собирать
- полный упорядоченный addrinfo list, а не только первый элемент;
- family, destination address и start/end time каждой попытки;
- результат SO_ERROR для каждого nonblocking socket;
- какой attempt победил и почему остальные были отменены;
- packet trace обеих families, если resolver success расходится с connect behavior.
Границы модели и частые ошибки
- Happy Eyeballs помогает при initial connection failures; он не лечит более поздний PMTU black hole или application timeout.
- Нельзя считать IPv4 «fallback навсегда»: исправный IPv6 должен оставаться preferred по политике клиента.
- Закрытие losing attempts — часть lifecycle; утечка handles быстро исчерпает ресурсы.
- Один общий last-error после нескольких sockets теряет причинность. Логируйте error рядом с конкретным candidate.
Контрольные вопросы
- Почему два addrinfo нельзя безопасно проверять одним и тем же socket handle?
- Как отличить writable nonblocking socket с успешным connect от writable socket с отказом?
- Какой артефакт докажет, что код действительно попробовал обе families?
Источники