← Tình huống

Trò chuyện với chỉ báo đang nhập và trạng thái trực tuyến

Chat hỗ trợ, nhắn tin giữa người mua và người bán, phòng chat nhóm: tin nhắn đến ngay, gợi ý “đang nhập…” giữa các trình duyệt và danh sách trực tuyến luôn chính xác.

Chat là nơi người dùng cảm nhận độ trễ rõ nhất. Một tin nhắn hiện muộn một giây, hay dòng “đang nhập…” không bao giờ xuất hiện, khiến sản phẩm trông như bị bỏ mặc. Chat hỗ trợ, nhắn tin giữa người mua và người bán, phòng chat nhóm và chat trong game đều cần ba thứ giống nhau: tin nhắn đến ngay, chỉ báo đang nhập và danh sách trực tuyến trung thực.

Người dùng nhận được gì

  • Tin nhắn hiện ra cho mọi người trong phòng ngay khi được gửi, không cần tải lại.
  • “Ann đang nhập…” trong lúc ai đó viết trả lời.
  • Chấm xanh cạnh tên những người đang ở trong cuộc trò chuyện.

Dùng những gì

  • Kênh presence (presence-), giữ danh sách thành viên của phòng chính xác trên mọi node.
  • Sự kiện phía client (client-*, whisper của Laravel Echo) cho chỉ báo đang nhập, đi thẳng giữa các trình duyệt mà không qua máy chủ của bạn.
  • Loại trừ socket (socket_id, toOthers() của Laravel), để người gửi không nhận lại tin nhắn của chính mình.
  • Webhook (member_added, member_removed) khi máy chủ cần phản ứng với người vào hoặc ra.

Bước 1: cấp quyền vào phòng

Chỉ thành viên của cuộc trò chuyện mới vào được kênh của nó. Route cấp quyền của bạn quyết định, và trả về thông tin người dùng cho danh sách thành viên:

app.post('/pusher/auth', requireLogin, express.urlencoded({ extended: false }), async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const room = channel_name.replace(/^presence-room-/, '');
  if (!(await db.rooms.isMember(room, req.user.id))) return res.sendStatus(403);
  res.json(pusher.authorizeChannel(socket_id, channel_name, {
    user_id: String(req.user.id),
    user_info: { name: req.user.name, avatar: req.user.avatarUrl },
  }));
});

Bước 2: vào phòng, liệt kê thành viên và hiện đang nhập

Bật Cho phép sự kiện phía client trong cài đặt ứng dụng, rồi:

const room = pusher.subscribe('presence-room-42');

room.bind('pusher:subscription_succeeded', (members) => {
  members.each((m) => online.add(m.id, m.info));
});
room.bind('pusher:member_added', (m) => online.add(m.id, m.info));
room.bind('pusher:member_removed', (m) => online.remove(m.id));

// Typing: at most one event every 2 seconds while the user writes
let last = 0;
input.addEventListener('input', () => {
  if (Date.now() - last < 2000) return;
  last = Date.now();
  room.trigger('client-typing', { name: me.name });
});
room.bind('client-typing', ({ name }) => typing.show(name, 3000));

room.bind('message', (m) => messages.append(m));

Bước 3: gửi tin nhắn qua máy chủ

Tin nhắn đi qua máy chủ của bạn, nên được lưu và kiểm duyệt rồi mới phát đi. Truyền socket ID của người gửi để tab của họ, vốn đã hiện tin nhắn, được bỏ qua:

app.post('/rooms/:id/messages', requireLogin, async (req, res) => {
  const msg = await db.messages.create({ room: req.params.id, user: req.user.id, text: req.body.text });
  await pusher.trigger('presence-room-' + req.params.id, 'message', msg, { socket_id: req.body.socket_id });
  res.json(msg);
});

Trình duyệt gửi kèm pusher.connection.socket_id trong yêu cầu.

Tương tự với Laravel

// routes/channels.php
Broadcast::channel('room.{id}', function ($user, $id) {
    return $user->rooms()->whereKey($id)->exists() ? ['id' => $user->id, 'name' => $user->name] : false;
});

// In the controller
broadcast(new MessageSent($message))->toOthers();
Echo.join(`room.${roomId}`)
  .here((users) => online.set(users))
  .joining((user) => online.add(user))
  .leaving((user) => online.remove(user))
  .listen('MessageSent', (e) => messages.append(e.message))
  .listenForWhisper('typing', (e) => typing.show(e.name, 3000));

Echo.join(`room.${roomId}`).whisper('typing', { name: me.name });

Mẹo

  • Kênh presence chứa tối đa 100 thành viên. Với phòng lớn hơn, như chat của buổi phát trực tiếp, xem đấu giá trực tiếp và lượng khán giả lớn.
  • Giới hạn tần suất sự kiện phía client ngay trong trình duyệt: mỗi kết nối đều có giới hạn tốc độ, và mỗi sự kiện tính là một tin nhắn.
  • Để báo cho những người đã rời phòng, kết hợp với thông báo trong ứng dụng và thông báo đẩy.