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

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 19 из 25 ~16 мин +20 XP

Presence: кто сейчас онлайн

Presence — это ответ websocket-сервера на продуктовый вопрос «кто сейчас онлайн»: карта статусов по userID, которая живёт в хабе и обновляется событиями. В этом уроке достроим каркас двенадцатого урока до полноценной системы: уровни online, away и offline, автоматическое угасание по активности и события, которые не мигают при каждом переподключении.

Содержание урока · 6 разделов

Зелёная точка рядом с аватаром кажется мелочью, пока не посчитаешь, что стоит за ней. В двенадцатом уроке presence родился как производное карты users: запись есть — в сети, запись удалилась — не в сети, и для учебного чата этого достаточно. Живой мессенджер спрашивает больше: человек открыл вкладку и ушёл за кофе — он онлайн или нет? Отвлёкся на звонок? Сидит с телефона в фоне? Один бит «в сети или нет» на эти вопросы не отвечает, и продуктовые команды добавляют уровни. Заодно всплывает цена: при тысячах пользователей каждое подключение и каждый уход — это потенциальная рассылка, и рассылать её всему серверу нельзя. Presence онлайн статусы — это три вещи: карта с уровнями, механика их смены без ручного труда и круг подписчиков, который делает рассылку дешёвой. Кто онлайн websocket сообщает не списком раз в час, а потоком событий — и весь поток должен родиться в одном владельце.

Presence онлайн статусы: карта userID и уровни#

Хаб получает карту presence map[string]Status: ключ — userID, значение — один из уровней. Три уровня покрывают большинство продуктов: online — есть живые устройства, away — устройства есть, но активности давно не было, offline — устройств нет. Уровень dnd, «не беспокоить», в эту тройку не входит принципиально: его знает только человек, и серверу остаётся принимать его как клиентское состояние, а не угадывать. Смена уровней — обязанность горутины run: тот же владелец карт, что и в восьмом уроке, потому что presence — производное от rooms и users, и вторая горутина, читающая их, сломала бы дисциплину. Кадр смены — обычный конверт из десятого урока с типом presence, полем User и полем Status. Отдельное правило, которое экономит трафик потом: запись в карту меняется только тогда, когда уровень действительно сменился — переход online в online не порождает ни кадра, ни записи.

setStatus: смена уровня и рассылка в комнату

GO
func (h *Hub) setStatus(c *Client, s Status) {
        if h.presence[c.userID] == s {
                return // без изменения уровня нет и кадра
        }
        h.presence[c.userID] = s
        env := &Envelope{Type: "presence", User: c.userID, Status: string(s)}
        for client := range h.rooms[c.room] {
                select {
                case client.send <- env:
                default:
                        close(client.send)
                }
        }
}

Метод живёт в владельце: дедупликация, запись в карту presence и рассылка конверта тем, кто сидит в комнате пользователя. Select с default — тот же предохранитель медленных клиентов, что и в обычной рассылке: presence lossy по природе, и потерянное событие чинится запросом снапшота. Круг адресатов — комната из одиннадцатого урока: в большой системе подписки сузят его до контактов.

Уровни online и offline server ставит сам, в тех же ветках register и unregister, где двенадцатый урок уже рассылал системные конверты: подключилось первое устройство — online, ушло последнее — offline. С away интереснее: сервер не спрашивает человека, отвлёкся ли тот, — он измеряет. В readPump на каждом входящем кадре обновляется отметка lastActivity; в select хаба живёт тикер присутствия, который раз в минуту понижает online в away тем, кто молчит дольше порога. Порог — продуктовое решение с границей: два-три минуты для живого чата, десять — для тикетов поддержки. И приятная экономия: lastPong из восемнадцатого урока — уже готовая отметка жизни, heartbeat обновляет её автоматически, и for многих клиентов отдельно трогать lastActivity не нужно — достаточно брать максимум из двух отметок.

Тикер присутствия: понижение до away

GO
case <-h.presenceTick.C: // Ticker раз в минуту
        now := time.Now().UnixNano()
        for c := range h.clients {
                if h.presence[c.userID] != StatusOnline {
                        continue
                }
                idle := now - atomic.LoadInt64(&c.lastActivity)
                if idle > int64(5*time.Minute) {
                        h.setStatus(c, StatusAway)
                }
        }

Раз в минуту владелец обходит клиентов, смотрит только на тех, кто числится online, и понижает замолчавших. Атомарная отметка lastActivity обновляется в readPump на каждом кадре, включая явные команды активности от клиента — мобильные приложения шлют их при выходе из фона. Смена уровня идёт через setStatus, поэтому кадр уйдёт ровно один раз, а обратный переход в online поднимет любое движение.

Круг рассылки — то место, где presence обычно и разваливается. Наивный вариант — broadcast всем: при десяти тысячах пользователей каждый вход — это десять тысяч конвертов, и чат превращается в генератор присутствия. Лечение то же, что в двенадцатом уроке: сузить круг. Минимум — комната пользователя: статус видят те, кто сидит с ним рядом, и это почти бесплатно, потому что карта rooms уже есть. Дальше — подписки на контакты: map userID на множество наблюдателей, построенная по явным запросам клиента, — тот же механизм комнат, только круг задаёт не сервер, а человек. Для интерфейса важен ещё снапшот: клиент при входе шлёт команду who и получает текущий список с уровнями, а дальше живут только дельты. Без снапшота подписчик, подключившийся позже события, никогда не узнает, кто онлайн: события потока не восстанавливают прошлое.

Два устройства одного человека — второй источник сюрпризов. Пользователь сидит в браузере и с телефона: браузер заснул, телефон в фоне, — по отметкам активности один ушёл в away, второй молчит дольше, и агрегировать их надо в один статус на userID. Правило честной агрегации — лучший из уровней: online, пока живо хоть одно активное устройство, offline, когда ушли все. Вложенная карта устройств из двенадцатого урока даёт это почти даром: статус меняется на границах пустого и непустого множества, и отдельной синхронизации не требует. Заодно решается и обратный вопрос: что показывать, когда устройства расходятся, — пользователь явно выставил dnd на телефоне; если dnd хранить как клиентский уровень с приоритетом, сервер перестаёт угадывать и просто публикует то, что человек сказал о себе. Граница: dnd с таймером «на час» уже требует серверного состояния, и оно живёт в той же карте presence, только источник у него другой.

Кто имеет право видеть статус#

Presence — не публичное свойство сервера, а данные о поведении человека, и круг видимости стоит решить до релиза, а не после жалобы. Три уровня доступа встречаются чаще всего: коллеги в общей комнате видят статусы друг друга — это цена входа в рабочее место; контакты в мессенджере видят друг друга по подписке, и подписка двусторонняя; все остальные не видят ничего, даже наличия в сети. Отдельная настройка — скрыть свой статус: человек включает «невидимку», и сервер перестаёт рассылать его переходы, оставляя только личную переписку; статус в карте presence при этом живёт — он нужен самому серверу для маршрутизации, просто наружу не выходит. Проверка круга происходит в setStatus перед рассылкой: тот же владелец, одна лишняя ветка. Пропустить этот вопрос дороже, чем кажется: список «кто сейчас на сайте» без фильтра становится разведчиком для тех, кто продаёт внимание — маркетинг попросит, поддержка попросит, и через год сервер шлёт каждому о каждом.

Уровни и их источники#

УровеньКто решаетКак определяется
onlineсервересть живые устройства, карта users непуста
awayсерверактивности нет дольше порога: пять минут по умолчанию
dndклиентявный уровень от человека, сервер публикует без споров
offlineсерверпоследнее устройство ушло плюс гистерезис в тридцать секунд

Проверка: откуда берётся away

Как сервер узнаёт, что пользователь отошёл?

Проверка: круг рассылки

Пользователь сменил статус на away. Кому уходит presence-кадр?

Дедупликация в setStatus

Прочитайте код и выберите, что он выведет:

// u42 уже числится в away, тикер присутствия тикнул дважды,
// кадров от клиента за это время не было
h.setStatus(c, StatusAway)
h.setStatus(c, StatusAway)

Соберите setStatus

Впишите три пропуска: сравнение текущего уровня, запись нового и круг адресатов рассылки.

func (h *Hub) setStatus(c *Client, s Status) { if h.presence[c.userID] == { return } h.presence[c.userID] = env := &Envelope{Type: "presence", User: c.userID, Status: string(s)} for client := range h.rooms[] { select { case client.send <- env: default: close(client.send) } } }

Соберите каркас setStatus

Расставьте строки метода: проверка уровня, запись и возврат без действий.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: presence

Термин

presence map[string]Status

нажмите, чтобы перевернуть
Определение

Карта userID на уровень: online, away, offline. Живёт в владельце run, кадр уходит только при смене уровня.

1 / 5

Presence готов: карта уровней, автоматическое угасание, гистерезис и круг подписчиков. Кадры статуса пойдут чаще, чем сообщения чата, — и их тоже стоит собирать пачками, чем займётся двадцатый урок про батчинг. Каркас присутствия и очередь оффлайн-сообщений описаны в двенадцатом уроке, отметка lastPong и конвейер смерти — в восемнадцатом про отвалы, а переподключения, из-за которых мигают статусы, — в семнадцатом про reconnect. Тикеры, отметки времени и пороги на duration разобраны в продвинутом курсе Go в теме про time и таймеры. Когда инстансов станет два, карта presence разъедется по процессам — мост между ними построит двадцать третий урок про масштабирование. Проверьте гистерезис на локальном Go с двумя вкладками — мигание видно сразу. План курса — на странице курса WebSocket.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Presence — карта userID на уровень в хабе: online и offline ставит сам сервер в ветках входа и ухода.
  • Away рождается из тишины: lastActivity в readPump плюс тикер присутствия, порог два-десять минут.
  • lastPong из heartbeat — готовая отметка жизни: для presence хватает максимума двух отметок.
  • setStatus дедуплицирует: смена уровня без изменения не пишет карту и не шлёт кадр.
  • Круг рассылки сужается до комнаты и подписок: broadcast всем превращает вход в десять тысяч конвертов.
  • Гистерезис в тридцать секунд убирает мигание статуса при переподключениях и деплоях.

Домашнее задание

Достройте presence поверх хаба с картой устройств: setStatus с дедупликацией, тикер присутствия с понижением до away и гистерезис offline в тридцать секунд. Добавьте команду who со снапшотом статусов комнаты при входе клиента. Проверьте локально с двумя вкладками одного пользователя: уход одной не гасит статус, уход обеих публикует offline один раз, а возврат до тридцати секунд не даёт ни одного кадра.

Личные заметкиПусто
Хранятся только в вашем браузере (WebSocket · Presence: кто сейчас онлайн)

Вопросы и ответы по уроку

Почему away нельзя узнать от клиента?

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

Что делать со статусами при нескольких инстансах?

Одна карта presence живёт внутри одного процесса, и её события не пересекают границу. Связку строят через шину событий: каждый инстанс публикует свои смены статусов, слушает чужие и держит копию карты. Детали — в двадцать третьем уроке про Redis Pub/Sub; до масштабирования карту можно не трогать.

Как часто можно опрашивать статусы через who?

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

Термины урока

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

Следующая ступень учебного пути

RabbitMQ25 уроков~11 чпрактика25 уроков уже доступны

RabbitMQ в Go: очереди сообщений на amqp091-go

Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.

  • Подключаться к RabbitMQ из Go и управлять connection и channel
  • Объявлять очереди, обменники и привязки с нужными свойствами
  • Публиковать и потреблять сообщения с ручными ack
  • Настраивать prefetch и справедливое распределение между воркерами