1. чаты делятся на комнаты, реализуешь протокол, можно типо ирц. Например есть на сервере подписка request:join
Когда твой клиент решает войти в определенный чат, он делает отправку запроса на сервер emit('request:join', roomName); на сервере есть реакция, обработка можно ли клиенту присоединиться к этому чату или нельзя.
2. в обработке события на сервере у тебя идет on('request.join', roomName=>socket.join(roomName); В этот момент надо перебрать все активные сокеты, на предмет того, используется ли клиентом второе окно (если это требуется конечно).
3. По этому принципу можно строить и общие чаты, и приватные. Люди начинают общаться не друг с другом, а писать в канал. клиент пишет emit('message:send", {room: id, message}), на сервере это пересылается в рум как .on('message:send', message=> io.to(message.room).emit(message.message)
В результате получается что клиенты общаются между собой, но по факту лишь шлют все данные своему серверу, а он уже принимает решение уведомить об этом остальных или нет.