Loading...

ذخیره‌سازی · 11 دقیقه مطالعه

برنامهٔ چت با Object Storage از نوع S3: S3 چه کاری می‌کند، چه کاری نمی‌کند و چطور همه را به هم وصل کنیم

بله، می‌توان یک برنامهٔ چت را روی Object Storage از نوع S3 ساخت، اما نه فقط با S3. S3 لایهٔ فایل و بایگانی است: عکس‌ها، پیام‌های صوتی، اسناد و تاریخچهٔ قدیمی پیام‌ها. برای رساندن پیام‌ها در لحظه به یک سرور برنامهٔ کوچک با WebSocket نیاز دارید و خود پیام‌ها جایشان در پایگاه داده است. این راهنما دلیلش را توضیح می‌دهد و بعد سه بخش را با کدی که در یک دموی واقعی کار کرده، روی CDNTR به هم وصل می‌کند.

به‌روزرسانی

برنامهٔ چت با Object Storage از نوع S3: S3 چه کاری می‌کند، چه کاری نمی‌کند و چطور همه را به هم وصل کنیم

ویدیو به انگلیسی با زیرنویس فارسی.

جواب کوتاه: بله، اما S3 یکی از سه لایه است

یک برنامهٔ چت سه کار متفاوت انجام می‌دهد. هر پیام تازه را همان لحظه به همهٔ اعضای اتاق می‌رساند. پیام‌ها را به خاطر می‌سپارد، تا کسی که اتاق را باز می‌کند چند ده پیام آخر را ببیند و برنامه بتواند به پرسش‌هایی مثل «چه چیزهایی خوانده نشده» جواب بدهد. و فایل نگه می‌دارد: عکس‌ها، ویدیوهای کوتاه، پیام‌های صوتی و اسنادی که کاربران در گفتگو می‌گذارند.

Object Storage از نوع S3 در کار سوم بسیار خوب است و برای دو کار اول ساخته نشده. پس جواب صادقانه به «آیا می‌توانم چت را روی S3 بسازم؟» این است: بله، به شرطی که S3 لایهٔ فایل و بایگانی باشد و کنار دو بخش دیگر قرار بگیرد.

سرور برنامه با هر کلاینت یک اتصال WebSocket باز نگه می‌دارد و پیام‌ها را پخش می‌کند. کوچک است: چند صد خط Node.js با Socket.io برای یک چت کارا کافی است.

پایگاه داده پیام‌ها را به شکل ردیف نگه می‌دارد، تا «50 پیام آخر این اتاق» فقط یک کوئری ایندکس‌دار باشد.

Object Storage برای هر چیزی است که فایل باشد. پایگاه داده و سرور را سبک نگه می‌دارد، چون هیچ‌کدام لازم نیست مگابایت‌ها رسانه را نگه دارند یا جابه‌جا کنند.

ادامهٔ راهنما نشان می‌دهد مرز میان این لایه‌ها کجاست و بعد هر سه را روی CDNTR به هم وصل می‌کند. برنامهٔ دموی این راهنما به‌صورت زنده روی CDNTR اجرا می‌شود (Node.js، Express و Socket.io): برنامه، Postgres، اتصال باکت و آپلود و دانلود امضاشده در تاریخ 2026-11-06 روی آن تأیید شدند. بخش‌هایی که به‌عنوان گزینه آمده‌اند، یعنی Redis adapter، کار بایگانی و سرورهای آماده، بخشی از دمو نبودند.

کارهایی که S3 به‌تنهایی برای چت انجام نمی‌دهد

push ندارد. S3 به درخواست‌ها جواب می‌دهد، اما خودش هیچ درخواستی را شروع نمی‌کند. وقتی پیام تازه‌ای می‌رسد، چیزی به کلاینت‌های دیگر خبر نمی‌دهد. بدون سرور خودتان، آن‌ها باید مدام سرکشی (polling) کنند: هر یکی دو ثانیه فهرست اشیای اتاق را از هر زبانهٔ باز و هر گوشی بگیرند. این کند حس می‌شود، درخواست‌ها را هدر می‌دهد و باز هم بلادرنگ نیست.

append ندارد. یک شیء یکجا نوشته می‌شود؛ عملیاتی مثل «یک خط به انتهای این فایل اضافه کن» وجود ندارد. پس هر پیام یا شیء جداگانهٔ خودش می‌شود (هزاران شیء ریز برای هر اتاق در روز) یا بازنویسی یک شیء بزرگ‌تر، که در آن دو فرستنده برای رونویسی نسخهٔ همدیگر رقابت می‌کنند.

کوئری ندارد. S3 فقط می‌تواند کلیدها را بر اساس پیشوند فهرست کند. «50 پیام آخر این اتاق»، «پیام‌های بعد از آخرین بازدید من»، «تعداد خوانده‌نشده‌ها»، «جستجوی کلمهٔ فاکتور»: همهٔ این‌ها پرسش‌های پایگاه داده‌اند. کلیدهایی که بر اساس زمان مرتب شده‌اند شما را بخشی از راه می‌برند و همان‌جا می‌ایستند.

کاربر و دسترسی ندارد. S3 فقط یک چیز را بررسی می‌کند: آیا درخواست با یک کلید معتبر امضا شده است. چه کسی در کدام اتاق است، چه کسی کدام فایل را می‌تواند ببیند، چه کسی مسدود است: این منطق مال برنامهٔ شماست. دادن کلیدهای S3 به مرورگر برای خواندن و نوشتن مستقیم یعنی دسترسی هر کاربر به کل باکت.

هیچ‌کدام از این‌ها ضعف S3 نیست. S3 یک مخزن فایل است و چت یک گفتگوی بلادرنگ و دارای وضعیت. هر ابزار را برای کار خودش به کار ببرید.

S3 در یک برنامهٔ چت در چه چیزی خوب است

پیوست‌ها. عکس، ویدیو، پیام صوتی، PDF. بزرگ‌اند، یک بار نوشته و بارها خوانده می‌شوند: دقیقاً همان باری که Object Storage برایش ساخته شده. پایگاه داده برای هر پیام فقط یک کلید و یک نام فایل نگه می‌دارد، پس هر قدر هم رسانه به اشتراک گذاشته شود کوچک و سریع می‌ماند.

بایگانی پیام‌ها. گفتگوهای قدیمی کم خوانده می‌شوند اما نباید از بین بروند. بیرون بردن آن‌ها از پایگاه داده و نگه‌داشتنشان در S3 به شکل بلوک‌های فشرده (مثلاً یک بلوک برای هر اتاق در هر روز) جدول فعال را کوچک نگه می‌دارد، بی آنکه چیزی از تاریخچه گم شود.

خروجی و پشتیبان. «دانلود داده‌های من»، خروجی گرفتن از یک اتاق، dump شبانهٔ پایگاه داده: هر کدام یک فایل است و جای هر کدام در یک باکت است.

سرور برنامهٔ سبک‌تر. چون مرورگر فایل را مستقیم در S3 آپلود می‌کند و مستقیم از آن دانلود می‌کند (بخش‌های بعد نشان می‌دهند چطور)، سرور برنامه هیچ‌وقت رسانه را از خودش عبور نمی‌دهد. فقط URLهای کوتاه‌عمر امضا می‌کند و پیام‌های کوچک JSON جابه‌جا می‌کند؛ برای همین یک کانتینر کوچک راه زیادی می‌رود.

در CDNTR، سرویس S3 به نشانی s3.cdn.com.tr اشیا را در مراکز دادهٔ ما در ترکیه نگه می‌دارد و با S3 سازگار است: AWS SDKها با یک endpoint سفارشی و آدرس‌دهی path-style با آن کار می‌کنند.

معماری روی CDNTR و راه‌اندازی با cdnctl

برنامه روی Managed Container Apps اجرا می‌شود. دمو یک برنامهٔ Node.js با Express و Socket.io است که از پوشهٔ سورس با cdnctl deploy منتشر شد: ایمیج داخل کلاستر ساخته می‌شود و به registry نیازی نیست. برنامه نشانی‌ای مثل https://xxxx.cdn.com.tr می‌گیرد و ترافیک WebSocket آن از CDN عبور می‌کند (در دمو، Socket.io روش انتقال را websocket گزارش کرد). اگر کانتینر برایتان تازه است، راهنمای کانتینر Docker روی CDN مبانی را توضیح می‌دهد.

پایگاه داده افزونهٔ مدیریت‌شدهٔ Postgres است. وقتی با --env-prefix DATABASE فعال شود، متغیرهای DATABASE_HOST، DATABASE_PORT، DATABASE_NAME، DATABASE_USER و رمز محرمانهٔ DATABASE_PASSWORD را به برنامه می‌دهد.

فایل‌ها در یک باکت روی Object Storage قرار می‌گیرند. باکت و یک کلید دسترسی برای آن می‌سازید و بعد هر دو را با --env-prefix S3 به برنامه متصل می‌کنید. برنامه S3_ENDPOINT ‏(https://s3.cdn.com.tr)، S3_BUCKET، S3_REGION ‏(cdn-tr)، S3_ACCESS_KEY_ID و کلید محرمانهٔ S3_SECRET_ACCESS_KEY را دریافت می‌کند. کلیدها فقط در محیط برنامه وجود دارند و هرگز به مرورگر نمی‌رسند.

کل راه‌اندازی چهار مرحله است. به جای <account>، <app>، <bucket> و <key> شناسه‌هایی را بگذارید که دستورهای قبلی چاپ می‌کنند.

برنامه، Postgres، باکت و اتصال با cdnctl
# 1. deploy the app from source (built in the cluster, no registry)
cdnctl deploy --dir . --account <account> --name chat-demo --healthcheck /health

# 2. managed Postgres -> DATABASE_HOST, _PORT, _NAME, _USER, _PASSWORD
cdnctl container addons enable-postgres --account <account> --app <app> --env-prefix DATABASE

# 3. a bucket and an access key for it
cdnctl object-storage buckets create --account <account> --name chat-demo-files
cdnctl object-storage access-keys create --account <account> --bucket <bucket>

# 4. bind them -> S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY
cdnctl object-storage bindings create --account <account> --app <app> \
  --bucket <bucket> --access-key <key> --env-prefix S3

پیام‌ها: یک جدول Postgres و یک handler در Socket.io

هر پیام یک ردیف است: اتاق، نویسنده، متن و برای پیوست‌ها کلید شیء و نام اصلی فایل. ایندکس روی (room, id) کوئری‌ای را که چت مدام اجرا می‌کند، یعنی 50 پیام آخر یک اتاق، به یک پیمایش کوتاه ایندکس تبدیل می‌کند؛ هر قدر هم جدول بزرگ شود.

سمت Socket.io فقط دو رویداد است. با join کلاینت وارد یک اتاق می‌شود و تاریخچه‌اش را می‌گیرد. با message سرور اول ردیف را می‌نویسد و بعد ردیف ذخیره‌شده را برای همهٔ اعضای اتاق، از جمله فرستنده، پخش می‌کند. پخش کردن چیزی که پایگاه داده برگردانده، نه چیزی که کلاینت فرستاده، به همه همان id و همان زمان را می‌دهد؛ و پیامی که ذخیره نشده به هیچ‌کس نمی‌رسد.

socket.data.user را middleware احراز هویت خودتان هنگام باز شدن اتصال تنظیم می‌کند. کلاینت هیچ‌وقت خودش در محتوای پیام نمی‌گوید کیست.

دمو با یک replica اجرا می‌شود. با چند replica، پیام باید به کلاینت‌هایی که به replicaهای دیگر وصل‌اند هم برسد؛ راه معمول افزونهٔ مدیریت‌شدهٔ Redis ‏(cdnctl container addons enable-redis) همراه با Redis adapter در Socket.io است. دربارهٔ خود Redis در راهنمای Redis چیست بخوانید.

اسکیما و handler پیام‌ها (Postgres، socket.io 4)

-- messages table
CREATE TABLE IF NOT EXISTS messages (
  id         bigserial PRIMARY KEY,
  room       text NOT NULL,
  author     text NOT NULL,
  body       text,
  file_key   text,
  file_name  text,
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS messages_room_id ON messages (room, id);

// server.js (socket.io 4)
io.on('connection', (socket) => {
  socket.on('join', async (room) => {
    socket.join(room);
    const { rows } = await db.query(
      `SELECT * FROM (SELECT * FROM messages WHERE room = $1
         ORDER BY id DESC LIMIT 50) last50 ORDER BY id`, [room]);
    socket.emit('history', rows);
  });

  socket.on('message', async ({ room, body, fileKey, fileName }) => {
    const { rows } = await db.query(
      `INSERT INTO messages (room, author, body, file_key, file_name)
       VALUES ($1, $2, $3, $4, $5) RETURNING *`,
      [room, socket.data.user, body ?? null, fileKey ?? null, fileName ?? null]);
    io.to(room).emit('message', rows[0]);   // everyone in the room, at once
  });
});

آپلود: URL امضاشده، مستقیم از مرورگر به S3

عبور دادن فایل‌ها از سرور برنامه آن را به گلوگاه تبدیل می‌کند: هر عکس دو بار آپلود می‌شود و در مسیر حافظه و پهنای باند مصرف می‌کند. در عوض، برنامه فقط اجازه می‌دهد و کنار می‌رود.

1. مرورگر با فرستادن نوع و اندازهٔ فایل، از برنامه یک URL آپلود می‌خواهد. برنامه کاربر، اتاق و اندازه را بررسی می‌کند (دمو تا 10 مگابایت اجازه می‌دهد)، کلیدی مثل rooms/<room>/<uuid> انتخاب می‌کند و یک URL امضاشدهٔ PUT کوتاه‌عمر با اعتبار 5 دقیقه برمی‌گرداند. 2. مرورگر فایل را با همین URL مستقیم با PUT به S3 می‌فرستد. 3. مرورگر پیام چت را همراه با کلید شیء می‌فرستد؛ سرور آن را مثل هر پیام دیگری ذخیره می‌کند. 4. برای باز کردن فایل، گیرنده برنامه را صدا می‌زند؛ برنامه دسترسی را بررسی می‌کند و به یک URL امضاشدهٔ GET، باز هم با اعتبار 5 دقیقه، ریدایرکت می‌کند.

URL امضاشده یک درخواست معمولی S3 است که امضایش در query string قرار دارد: هر کس آن را داشته باشد، تا وقتی منقضی نشده، دقیقاً همان یک عملیات را روی همان یک کلید می‌تواند انجام دهد. راهنمای پیوندهای امضاشده این ایده را با جزئیات بیشتر توضیح می‌دهد. برنامه اندازهٔ اعلام‌شده را پیش از امضا بررسی می‌کند؛ سقف اندازه، مدت اعتبار و قالب کلید را روی سرور نگه دارید، جایی که کاربران نمی‌توانند تغییرشان دهند.

PUT امضاشده با Content-Type امضاشده و ریدایرکت دانلود

import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { randomUUID } from 'node:crypto';

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,   // https://s3.cdn.com.tr
  region: process.env.S3_REGION,       // cdn-tr
  forcePathStyle: true,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
});
const MAX_BYTES = 10 * 1024 * 1024;

app.post('/api/upload-url', express.json(), async (req, res) => {
  const { room, type, size } = req.body;
  if (!type || !(size > 0) || size > MAX_BYTES) {
    return res.status(400).json({ error: 'max 10 MB' });
  }
  const key = `rooms/${room}/${randomUUID()}`;
  const url = await getSignedUrl(s3,
    new PutObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key, ContentType: type }),
    { expiresIn: 300, signableHeaders: new Set(['content-type']) });
  res.json({ url, key });
});

app.get('/api/files', async (req, res) => {
  // check here that the user may read this room
  const url = await getSignedUrl(s3,
    new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: req.query.key }),
    { expiresIn: 300 });
  res.redirect(302, url);
});

دام Content-Type: خطای 403 AccessDenied روی PUT امضاشده

این مورد معمولاً یک بعدازظهر وقت آدم را می‌گیرد، برای همین بخش جداگانه دارد. S3 ما روی Ceph RGW اجرا می‌شود و RGW دربارهٔ یک هدر از AWS سخت‌گیرتر است: اگر مرورگر Content-Typeای بفرستد که امضا آن را پوشش نمی‌دهد، PUT امضاشده با 403 AccessDenied رد می‌شود. AWS این هدر را وقتی امضا نشده نادیده می‌گیرد؛ پس کدی که آنجا کار می‌کند اینجا ممکن است با خطایی شکست بخورد که شبیه مشکل کلید به نظر می‌رسد.

راه‌حل این است که نوع فایل را در امضای URL بگنجانید و بعد دقیقاً همان نوع را بفرستید. در AWS SDK v3، ‏ContentType را در PutObjectCommand بدهید و مثل نمونهٔ بالا signableHeaders: new Set(['content-type']) را به getSignedUrl اضافه کنید. در مرورگر هم همان هدر Content-Type را روی درخواست PUT بگذارید.

دو نکته این کار را مطمئن می‌کند. در هر دو طرف از یک مقدار پیش‌فرض استفاده کنید: فایلی که نوع ندارد (file.type رشتهٔ خالی است) باید با یک مقدار واحد، مثلاً application/octet-stream، هم امضا و هم فرستاده شود. و نگذارید کتابخانه‌ای هدر را بازنویسی کند: بعضی ابزارهای آپلود به فایل‌های متنی ; charset=utf-8 اضافه می‌کنند و دیگر با امضا یکی نیست.

سمت مرورگر: همان نوعی که سرور امضا کرده

// browser: ask for a URL, PUT the file to S3, then send the message
const { url, key } = await fetch('/api/upload-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ room, type: file.type || 'application/octet-stream', size: file.size }),
}).then((r) => r.json());

const put = await fetch(url, {
  method: 'PUT',
  headers: { 'Content-Type': file.type || 'application/octet-stream' },  // exactly the signed type
  body: file,
});
if (!put.ok) throw new Error(`upload failed: ${put.status}`);

socket.emit('message', { room, fileKey: key, fileName: file.name });

CORS روی باکت

صفحه روی نشانی برنامهٔ شما اجرا می‌شود و آپلود به s3.cdn.com.tr می‌رود، یعنی یک origin دیگر. بنابراین مرورگر پیش از PUT یک preflight می‌فرستد و فقط وقتی ادامه می‌دهد که باکت با قانون CORSای جواب بدهد که origin برنامهٔ شما، متد PUT و هدر Content-Type را مجاز کند. بدون آن، آپلود در مرورگر شکست می‌خورد در حالی که همان درخواست با curl کار می‌کند؛ نشانهٔ کلاسیک نبودن قانون CORS.

قوانین CORS باکت از طریق S3 API و با PutBucketCors تعریف می‌شوند. دمو این کار را یک بار هنگام شروع انجام می‌دهد و به این ترتیب قانون کنار کدی می‌ماند که به آن وابسته است. به origin خودتان اجازه بدهید، نه به *: این قانون بخشی از چیزی است که جلوی استفادهٔ سایت‌های دیگر از باکت شما را در صفحه‌هایشان می‌گیرد. راهنمای CORS چیست preflightها و پیام‌های خطای رایج را با جزئیات توضیح می‌دهد.

قانون CORS باکت برای آپلود و دانلود مستقیم

import { PutBucketCorsCommand } from '@aws-sdk/client-s3';

// once at startup: let the app's own origin PUT and GET directly
await s3.send(new PutBucketCorsCommand({
  Bucket: process.env.S3_BUCKET,
  CORSConfiguration: {
    CORSRules: [{
      AllowedOrigins: ['https://xxxx.cdn.com.tr'],   // your app's address
      AllowedMethods: ['PUT', 'GET'],
      AllowedHeaders: ['content-type'],
      MaxAgeSeconds: 3600,
    }],
  },
}));

WebSocket از طریق CDN: مهلت بیکاری و اتصال دوباره

CDN ترافیک WebSocket را در سطح هر قانون تحویل منتقل می‌کند. روی دامنهٔ Managed Container Apps، ‏WebSocket به‌طور پیش‌فرض روشن است و مهلت بیکاری (idle timeout) آن 300 ثانیه است، مگر اینکه قانون تحویل مقدار دیگری تعیین کند؛ یک قانون می‌تواند هر مقداری بین 1 تا 3600 ثانیه بگذارد. این مهلت سکوت را می‌شمارد، نه کل مدت اتصال را؛ اتصالی که داده در آن رد و بدل می‌شود باز می‌ماند. Socket.io به‌طور پیش‌فرض هر 25 ثانیه heartbeat خودش را می‌فرستد و همین یک اتاق ساکت را هم با فاصلهٔ زیاد زیر مقدار پیش‌فرض نگه می‌دارد. تنظیمات در راهنمای اتصال به سرور مبدأ آمده است.

کلاینت‌ها باید خودشان دوباره وصل شوند. اتصال‌های باز WebSocket ممکن است هنگام بارگذاری دوبارهٔ پیکربندی edge بسته شوند و گوشی‌ها هم با هر بار عوض کردن شبکه اتصال را از دست می‌دهند. Socket.io خودش دوباره وصل می‌شود؛ اگر از WebSocket خام استفاده می‌کنید، اتصال دوباره با backoff اضافه کنید.

پس از اتصال دوباره، کلاینت باید چیزهایی را که از دست داده بگیرد. ساده‌ترین راه فرستادن دوبارهٔ join است: سرور با تاریخچهٔ اخیر اتاق جواب می‌دهد و کلاینت آن را بر اساس id پیام ادغام می‌کند تا هیچ پیامی دو بار نمایش داده نشود.

بایگانی پیام‌های قدیمی یا استفاده از یک سرور چت آماده

بایگانی. دمو همهٔ پیام‌ها را در Postgres نگه می‌دارد و این تا مدت‌ها مشکلی ندارد. وقتی جدول بزرگ شد، یک الگوی رایج کاری دوره‌ای است که پیام‌های قدیمی‌تر از مثلاً 90 روز را برمی‌دارد، آن‌ها را به شکل یک بلوک JSON برای هر اتاق در هر روز در S3 می‌نویسد (archive/<room>/2026-08-01.json.gz) و سپس آن ردیف‌ها را حذف می‌کند. «بارگذاری پیام‌های قدیمی‌تر» بلوک مناسب را با یک GET امضاشده از S3 می‌خواند. پایگاه داده چیزی را نگه می‌دارد که کاربران واقعاً مرور می‌کنند و S3 بقیه را.

سرورهای آماده. اگر نمی‌خواهید چت خودتان را بنویسید، Mattermost، ‏Rocket.Chat و Matrix Synapse می‌توانند فایل‌های آپلودشده را روی فضای ذخیره‌سازی سازگار با S3 نگه دارند. همان سه لایه آنجا هم برقرار است: فرایند سرور خودشان، پایگاه دادهٔ خودشان و یک باکت برای فایل‌ها. روی CDNTR این یعنی همان کانتینر، افزونهٔ پایگاه داده و باکتِ این راهنما، که با endpoint، باکت و کلیدهای S3 مطابق مستندات هر پروژه پیکربندی می‌شوند.

چک‌لیست: اشتباه‌هایی که آپلود در چت را خراب می‌کنند

قانون CORS روی باکت. origin برنامهٔ شما، PUT و GET، هدر Content-Type. اگر نباشد، فقط به شکل یک آپلود ناموفق در مرورگر دیده می‌شود.

Content-Type امضاشده. آن را در امضای URL مربوط به PUT بگنجانید و دقیقاً همان مقدار را بفرستید، با همان مقدار پیش‌فرض برای فایل‌های بدون نوع. در غیر این صورت: 403 AccessDenied.

اعتبار کوتاه. چند دقیقه، نه چند روز. دمو برای PUT و GET هر دو 5 دقیقه به کار می‌برد؛ URLی که لو برود تقریباً بلافاصله بی‌فایده می‌شود.

فایل‌ها از برنامه عبور نمی‌کنند. آپلود و دانلود مستقیم میان مرورگر و S3 انجام می‌شود. برنامه امضا و ریدایرکت می‌کند، داده را عبور نمی‌دهد.

اتصال دوبارهٔ خودکار. انتظار داشته باشید اتصال‌های WebSocket بسته و دوباره باز شوند. دوباره به اتاق بپیوندید و تاریخچه را بر اساس id پیام ادغام کنید.

سقف اندازه. اندازهٔ اعلام‌شده را پیش از امضا بررسی کنید و هر چیزی بالاتر از سقف را رد کنید (در دمو 10 مگابایت).

کلیدها روی سرور می‌مانند. کلیدهای دسترسی S3 از طریق اتصال باکت در محیط برنامه قرار دارند. هرگز آن‌ها را به مرورگر نفرستید و در کد کلاینت نگذارید؛ مرورگر فقط URLهای امضاشده را می‌بیند.

بررسی دسترسی هنگام دانلود. لینک فایل‌ها را از برنامه عبور دهید تا برنامه پیش از ریدایرکت مطمئن شود کاربر اجازهٔ دیدن آن اتاق را دارد.

نحوه راه‌اندازی

راهنماهای گام‌به‌گام در مرکز راهنما:

پرسش‌های رایج دربارهٔ چت با S3

آیا می‌توان برنامهٔ چت را فقط با S3 ساخت؟

نسخهٔ بلادرنگ را نه. S3 نمی‌تواند پیام تازه را برساند، به اشیا چیزی اضافه کند یا روی آن‌ها کوئری بزند؛ کلاینت‌ها مجبور به سرکشی مداوم می‌شوند و شما یک پایگاه داده را روی فهرست کلیدها از نو می‌سازید. از S3 برای فایل‌ها و بایگانی، از پایگاه داده برای پیام‌ها و از یک سرور کوچک WebSocket برای رساندن پیام استفاده کنید.

چرا کلاینت‌ها برای پیام‌های تازه مدام S3 را بررسی نکنند؟

هر کلاینت باز هر یکی دو ثانیه فهرست اشیا را می‌گرفت: کند حس می‌شود، تعداد درخواست‌ها را چند برابر می‌کند و باز هم به «50 پیام آخر» و «خوانده‌نشده‌ها» جواب نمی‌دهد. اتصال WebSocket پیام را همان لحظه که سرور ذخیره‌اش می‌کند به همهٔ اعضای اتاق می‌رساند.

آیا فایل‌های آپلودشده از سرور برنامهٔ من عبور می‌کنند؟

نه. برنامه یک URL امضاشدهٔ PUT برمی‌گرداند که در دمو 5 دقیقه اعتبار دارد و مرورگر فایل را مستقیم در S3 آپلود می‌کند. دانلود هم همین‌طور است: برنامه دسترسی را بررسی می‌کند و به یک URL امضاشدهٔ GET ریدایرکت می‌کند.

چرا PUT امضاشدهٔ من خطای 403 AccessDenied می‌دهد؟

در S3 ما رایج‌ترین دلیل هدر Content-Typeای است که امضا آن را پوشش نمی‌دهد. آن را در امضای URL بگنجانید (در AWS SDK v3: ‏ContentType در PutObjectCommand و signableHeaders: new Set(['content-type'])) و دقیقاً همان نوع را از مرورگر بفرستید. اگر مرورگر به جای آن خطای CORS نشان می‌دهد، باکت به قانون CORS نیاز دارد.

آیا WebSocket از طریق CDN کار می‌کند؟

بله. روی دامنهٔ Managed Container Apps، ‏WebSocket به‌طور پیش‌فرض روشن است و مهلت بیکاری آن 300 ثانیه است، مگر اینکه قانون تحویل مقدار دیگری تعیین کند (یک قانون می‌تواند بین 1 تا 3600 ثانیه بگذارد). کلاینت‌ها باید خودکار دوباره وصل شوند، چون اتصال‌های باز ممکن است هنگام بارگذاری دوبارهٔ پیکربندی edge بسته شوند؛ Socket.io این کار را خودش انجام می‌دهد.

آیا برای چت با Socket.io به Redis نیاز دارم؟

با یک replica نه؛ دمو با یک replica اجرا می‌شود. با چند replica، پیام باید به کلاینت‌هایی که به replicaهای دیگر وصل‌اند هم برسد و راه معمول افزونهٔ مدیریت‌شدهٔ Redis همراه با Redis adapter در Socket.io است.