Содержание урока · 9 разделов
Клиент не подключается, экран молчит, консоль браузера пуста — и первое желание у большинства в этот момент перечитать код Upgrader'а. Зря: websocket-соединение — это сначала HTTP-запрос и HTTP-ответ, и вся диагностика отказа лежит в коде ответа и заголовках. Вопрос «кто виноват» раскладывается по трём этажам, и на каждом этаже свой инструмент. Не дошло рукопожатие — виноват HTTP-слой, и его показывает curl за одну команду. Рукопожатие прошло, а кадров нет — виноват протокол, и с ним разговаривает wscat из терминала. Кадры ходят, но клиент их теряет — смотрим фреймы в DevTools и логи read error на сервере. За вечер вы проходите эти три этажа сверху вниз и находите место поломки быстрее, чем открывается дамп трафика. Они же покрывают большинство обращений «не работает real-time»: остаток — сетевые политики инфраструктуры, и их лечат конфигом прокси, а не кодом сервера.
Отладка websocket: рукопожатие глазами через curl#
Рукопожатие — обычный GET с несколькими обязательными заголовками, поэтому его можно показать curl'ом и прочитать, как читают ответ любого API. Три заголовка обязательны: Connection: Upgrade, Upgrade: websocket и Sec-WebSocket-Version: 13, плюс Sec-WebSocket-Key — случайная строка, из которой сервер обязан вычислить Sec-WebSocket-Accept. Успех выглядит как HTTP/1.1 101 Switching Protocols: соединение переключено, дальше по нему идут кадры, и curl будет ждать их бесконечно — флаг -N отключает буферизацию, а Ctrl-C возвращает терминал. Самое ценное в этом упражнении — увидеть код отказа: 400 значит, что сервер не узнал websocket-запрос, 403 — что Upgrader его узнал, но не пустил. Обе цифры разобраны ниже, после таблицы.
curl: полный рукопожатие в одну команду
GOcurl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
http://localhost:8080/ws
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Значение пары Key и Accept взято из примера RFC 6455: сервер вычисляет Accept как Base64 от SHA-1 ключа и константы GUID, и gorilla делает это по стандарту. Флаг -i показывает заголовки ответа, -N не буферизует вывод. Если ответ 101 — транспорт и Upgrader невиновны, ищите поломку выше по стеку, в кадрах.
wscat: собеседник для сервера#
curl подтверждает транспорт, но не умеет разговаривать протоколом. Для диалога ставится wscat — утилита на Node, которая открывает соединение и превращает терминал в чат: каждая введённая строка уходит кадром, каждый входящий кадр печатается в ответ. Установка — npm install -g wscat, запуск — wscat -c ws://localhost:8080/ws. Заголовки передаются флагом -H, и это способ проверить авторизацию из тринадцатого урока без браузера: -H Authorization: Bearer с настоящим токеном. wscat показывает и служебные события — Connected, Disconnected с кодом и причиной, — что делает его удобным для проверки heartbeat из шестого урока: открыли соединение, ничего не трогали и смотрим, жив ли ping-цикл. Инструменты разработчика подобного рода экономят минуты: сервер проверяется без фронтенда, сборки и открытий вкладок.
Сессия wscat против сервера чата
GOwscat -c ws://localhost:8080/ws -H "Authorization: Bearer eyJhbGciOi..."
Connected (press CTRL+C to quit)
> {"type":"join","room":"general"}
< {"type":"system","text":"u42 joined general"}
> {"type":"chat","room":"general","text":"привет"}
< {"type":"chat","user":"u42","text":"привет"}Стрелки внутри сессии: строка с угловой скобкой — то, что ушло на сервер, с меньшей — то, что пришло с него. Вводим конверты из десятого урока как сырой JSON: сервер, который отвечает на join системным сообщением и на chat эхом, проверен без браузера. Ctrl-C закрывает соединение по close handshake — в логах сервера это чистый уход, а не обрыв.
DevTools: вкладка Network и WS-фреймы#
Браузерные инструменты разработчика дают то, чего нет у curl и wscat: хронологию кадров конкретной вкладки. Откройте Network, отфильтруйте по типу WS, кликните соединение — появится панель сообщений, где каждый кадр лежит строкой со временем, направлением и размером: исходящие и входящие подсвечены разными цветами, а служебные ping и close видны как отдельные записи. Здесь диагностируются вещи, которых сервер не видит: дубликаты соединений после багованного reconnect из семнадцатого урока (две строки вместо одной), пачки из двадцатого урока с массивом items внутри, отсутствие close frame при закрытии вкладки. Клик по кадру раскрывает JSON, и расследование «почему интерфейс не отрисовал сообщение» заканчивается за минуту: либо кадра нет — виноват сервер, либо кадр есть — виноват обработчик на клиенте. В Firefox панель называется Frames, механика та же: фильтр по WS, клик по соединению, список кадров с направлением.
Молчащие соединения: где умирает heartbeat#
Отдельный класс проблем — соединение, которое числится открытым, но ничего не передаёт: NAT дома и корпоративный брандмауэр убивают тихое TCP-соединение по таймауту простоя, обычно через несколько минут тишины. Симптом узнаваем: клиент работал час, ушёл в фон, вернулся к молчащему интерфейсу, а onclose пришёл через минуты после фактической смерти сокета. Диагностика двухсторонняя: DevTools показывает последние ping и их время, логи read error на сервере дают момент обрыва, и разрыв между отметками — то самое окно молчания. Лечение — heartbeat из шестого урока: ping раз в тридцать-шестьдесят секунд держит соединение помеченным как живое и переводит смерть из «неизвестно когда» в «не позже одного периода пинга». Проверяется вручную за две минуты: wscat, брошенный на Wi-Fi, и телефон на мобильном интернете дают два разных времени жизни, и оба становятся длиннее после включения пинга.
Логи соединений: read error без спама#
Серверный лог — последний этаж, и его главная задача — не показать всё, а отделить норму от поломки. В readPump любая ошибка чтения заканчивается возвратом, и наивный лог печатает каждую: на мобильной сети соединения рвутся по несколько раз в минуту, и лог превращается в поток abnormal closure 1006 — это норма, а не инцидент. Полезная строка содержит userID, код закрытия и причину, а уровень зависит от кода: 1000 и 1001 — штатные уходы, их логируют на уровне debug; 1006 — обрыв, info; неожиданные коды вроде 1009 при обычных сообщениях — warn, потому что намекают на лимит из четырнадцатого урока. Записывать тела сообщений в лог не стоит: там персональные данные, а для разбора протокола достаточно типа конверта.
Деплой во вторую среду, и ни один клиент не подключается: curl изнутри кластера отвечает 403. Причина в CheckOrigin — Upgrader сравнивает Origin запроса со списком, а фронтенд теперь приходит с домена прода, которого в списке нет: локально разработчик гонял localhost, и всё было зелёным. Заодно ловится вторая ловушка: прокси и балансировщики иногда переписывают или вырезают Origin. Лечение — лог в ветке CheckOrigin: одна строка с отвергнутым Origin экономит час гаданий, а список доменов заводится в конфиг вместе с остальными адресами среды из пятнадцатого урока.
Симптом, причина и инструмент#
| Симптом | Типичная причина | Инструмент |
|---|---|---|
| 400 Bad Request при подключении | запрос без заголовков Upgrade дошёл до Upgrader | curl -i: сравнить заголовки |
| 403 при подключении | CheckOrigin не пустил Origin | curl с -H Origin и лог в ветке |
| 101 получен, кадров нет | клиент не в комнате или не подписан | wscat: послать join и смотреть |
| onclose с кодом 1006 | обрыв без close handshake | логи read error на сервере |
| Кадры приходят, интерфейс молчит | обработчик не знает тип конверта | DevTools: раскрыть кадр и сверить type |
| Кадров в два раза больше, чем сообщений | дубль соединения после reconnect | DevTools: две строки в списке WS |
Проверка: что означает 403
Upgrader ответил на рукопожатие кодом 403. Что это чаще всего значит?
Проверка: загадочный 1006
Браузер вызывает onclose с кодом 1006. Что произошло?
curl без заголовков Upgrade
Прочитайте код и выберите, что он выведет:
// сервер: обычный Upgrader из четвёртого урока
// var upgrader = websocket.Upgrader{}
// mux.HandleFunc("/ws", func(w, r) { upgrader.Upgrade(w, r, nil) })
curl -i http://localhost:8080/wsПесочница курса не умеет сеть — соберите эхо-сервер из четвёртого урока на локальном Go и пройдите этажи сверху вниз: curl с заголовками покажет 101, wscat -c прогонит пару конвертов, вкладка WS в DevTools раскроет каждый кадр построчно. На весь круг уходит минут пять, и после него фраза «сервер не подключается» перестаёт быть паникой: вы точно знаете, на каком этаже искать.
Соберите рукопожатие curl'ом
Впишите три пропуска: два обязательных заголовка и версию протокола.
Порядок расследования
Расставьте шаги: от кода ответа до логов сервера.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: отладка
Три этажа отладки встали на место: curl отвечает за рукопожатие, wscat — за диалог, DevTools и логи — за кадры и их потери. Коды 400 и 403 больше не загадка, а указание на конкретную ветку. Когда соединение стабильно, следующий вопрос — что будет, когда клиентов станет больше одного сервера: двадцать третий урок про масштабирование строит мост между инстансами. Эхо-сервер для упражнений описан в четвёртом уроке, ping-цикл и 1006 — в шестом уроке про heartbeat, лимит кадра и 1009 — в четырнадцатом, а Origin и CheckOrigin — в пятнадцатом уроке. Токен в заголовке для wscat берётся из тринадцатого урока про авторизацию. Структурированные логи с уровнями удобно вести через курс по zap — в проде логов много, и без уровней они бесполезны. План курса — на странице курса WebSocket.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Рукопожатие — обычный HTTP: curl с заголовками Upgrade показывает код ответа и Accept за одну команду.
- 101 означает, что транспорт жив: дальше ищите поломку в кадрах, а не в Upgrader'е.
- wscat -c проверяет протокол без браузера: конверты вводятся сырым JSON, заголовки летят через -H.
- DevTools на вкладке WS раскрывает каждый кадр по времени и направлению: дубликаты и пачки видны сразу.
- 400 — сервер не понял рукопожатие, 403 — CheckOrigin не пустил Origin, 1006 — обрыв без close handshake.
- Лог read error сортируется по кодам: штатные уходы в debug, обрывы в info, странные коды в warn.
Домашнее задание
Поднимите эхо-сервер из четвёртого урока и пройдите три этажа: получите 101 через curl с заголовками Upgrade, прогоните конверты join и chat через wscat -c, найдите кадры на вкладке WS в DevTools. Затем ужесточите CheckOrigin под конкретный домен и добейтесь 403 с чужим Origin, добавив в ветку лог с отвергнутым значением. В readPump заведите строку лога с userID и кодом закрытия и убедитесь, что Ctrl-C в wscat даёт чистый 1000, а обрыв Wi-Fi — 1006.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли обойтись без wscat, если есть браузер?
Можно, но дороже: вкладка требует собранного фронтенда и правильного Origin, а wscat работает из любой папки и с любым заголовком. В CI и на сервере без графики браузера нет вовсе, и терминальный клиент остаётся единственным способом поговорить с сокетом. Для скриптов есть ещё вариант на Go из двадцать первого урока — Dialer в main-функции.
Почему нельзя логировать тела сообщений?
В телах лежат персональные данные: тексты переписки, имена, иногда токены, случайно присланные клиентом. Логи живут дольше запросов, копируются в агрегаторы и читаются всеми, у кого есть доступ к инфраструктуре, поэтому тело в логе — утечка по определению. Для отладки протокола хватает типа конверта, userID и размера кадра.
Как поймать рукопожатие между клиентом и прокси?
curl до и после прокси: сравните заголовки запроса и код ответа на обоих участках. Если напрямую 101, а через прокси 400 или обрыв — прокси вырезает заголовки Upgrade или держит соединение открытым для буферизации, и это настраивается на его стороне. Помните, что некоторые прокси по умолчанию не пропускают websocket и требуют явного включения.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
RabbitMQ в Go: очереди сообщений на amqp091-go
Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.
- Подключаться к RabbitMQ из Go и управлять connection и channel
- Объявлять очереди, обменники и привязки с нужными свойствами
- Публиковать и потреблять сообщения с ручными ack
- Настраивать prefetch и справедливое распределение между воркерами