Socket.io چیست و چه کاربردی دارد؟ + پروژه عملی

?What is Socket.io and what is its use

13 مرداد 1402
Socket.io

در این مقاله قصد داریم با یکی از معروف ترین کتابخانه های دنیای برنامه نویسی آشنا شویم. نام این کتابخانه Socket.io است. این کتابخانه ارتباط دو طرفه رویداد محور بین کلاینت و سرور را ممکن می سازد.

Socket.io چیست؟

این کتابخانه با ایجاد یک کانال ارتباطی دوطرفه باعث می شود سرور و کلاینت با هم ارتباطی دو طرفه و سریع داشته باشند.

Socket.io ارتباط دو طرفه رویداد محور را ممکن می سازد. این کتابخانه بر پایه Websocket API ساخته شده است.

Socket.io از دو بخش تشکیل شده است:

  1. کتابخانه سمت client (در مرورگر اجرا می شود)
  2. کتابخانه سمت server (برای nodejs یا سایر زبان های برنامه نویسی مثل php یا go و ... اجرا می شود)

سیستم های بلادرنگ بر اساس سوکت ها معماری می شوند و یک کانال ارتباطی دو جهته بین client و Server فراهم می کنند. به این معنی که سرور می تواند پیام ها را به کلاینت ها ارسال کند. هر زمان که یک رویداد رخ می دهد، ایده این است که سرور آن را دریافت کرده و آن را به client های مرتبط ارسال کند.

اولین دستوری که با آشنا خواهید شد. دستور ()io.on  است که اتصال (connection) و disconnection و رویدادها را با استفاده از شی socket مدیریت می کند.

Socket.io کتابخانه ای است که یک ارتباط کم تاخیر، دو طرفه (از سرور به کلاینت و از کلاینت به سرور) و مبتنی بر رویداد را بین یک کلاینت و یک سرور امکان پذیر می کند.

Socket.io بر پایه پروتکل WebSocket ساخته شده است و ضمانت های اضافی مانند HTTP long-polling و اتصال مجدد خودکار (automatic reconnection) را ارائه می دهد.

ارتباط میان سرور و کلاینت

WebSocket یک پروتکل ارتباطی است که یک کانال دوطرفه و کم تاخیر بین سرور و مرورگر را فراهم می کند. کتابخانه Socket.io یک اتصال TCP باز را به سرور نگه می دارد.

socket ها بر اساس رویدادها کار می کنند. رویدادهای شی socket که در سمت سرور قابل دسترسی هستند:

  • Connect
  • Message
  • Disconnect
  • Reconnect
  • Ping
  • Join
  • Leave

رویدادهای شی socket در سمت کلاینت در زیر آمده اند:

  • Connect
  • Connect_error
  • Connect_timeout
  • Reconnect

در ادامه به توضیح بیشتر این مباحث می پردازیم بنابراین نگران نباشید!

Socket.io چگونه کار می کند؟

کانال ارتباطی دو طرفه که بین سرور و کلاینت وجود دارد با استفاده از Websocket یا HTTP long-polling برقرار می شود. کد اصلی socket.io به دو لایه تقسیم می شود:

  1. طراحی پایپلاین (مسیر) لایه پایین: چیزی که ما Engine.IO می نامیم و موتور داخلی Socket.IO است.
  2. API لایه بالا: خود Socket.IO.

Engine.IO مسئول برقراری ارتباط سطح پایین بین سرور و کلاینت است و موارد زیر را مدیریت می کند:

  • انتقال های مختلف و مکانیسم ارتقا
  • تشخیص قطع اتصال

لایه سطح پایین مسئول برقراری اتصال سطح پایین (low-level) میان سرور و کلاینت است. در Socket.io دو نوع انتقال (transport) داریم:

  1. Websocket
  2. HTTP long-polling

HTTP long-polling شامل درخواست های موفقیت آمیز HTTP است:

    1. درخواست های GET طولانی که برای خواندن داده ها از سرور است
    2. درخواست های POST کوتاه که برای فرستادن داده به سرور است.

انتقال WebSocket از یک اتصال WebSocket تشکیل شده است که یک کانال ارتباطی دو طرفه و با تاخیر کم بین سرور و کلاینت برقرار می کند. با توجه به ماهیت انتقال یا transport هر emit با فریم websocekt مخصوص به خود ارسال می شود.

در نهایت برای یک جمع بندی مختصر ویژگی های اصلی Socket.io را مطرح می کنم:

  1. HTTP long-polling fallback (برگشت به HTTP long-polling)
  2. automatic reconnection (اتصال دوباره خودکار)
  3. packet buffering (ذخیره کردن بسته ها)
  4. Acknowledgements (تاییدها)
  5. broadcasting (پخش گسترده)
  6. multiplexing (تسهیم)

HTTP long-polling fallback (برگشت به HTTP long-polling): اگر اتصال websocket برقرار نشود آن گاه اتصال به HTTP long-polling بر می گردد.

automatic reconnection (اتصال دوباره خودکار): اتصال میان کلاینت و سرور به هر دلیلی ممکن است قطع شود بدون این که آن ها از این موضوع آگاه باشند. به همین دلیل Socket.io دارای سازوکاری به نام ضربان قلب یا heatbeat است که کار آن بررسی لحظه ای یا دوره ای برقرار بودن اتصال است. اگر اتصال کلاینت قطع شود به صورت خودکار با تاخیر نمایی بازگشتی دوباره اتصال را برقرار می کند تا سرور تحت تاثیر قرار نگیرد.

 packet buffering (ذخیره کردن بسته ها): وقتی که اتصال قطع می شود بسته ها به طور خودکار بافر می شوند (بافر معنی های متعددی دارد ولی این جا معنی ذخیره کردن می دهد) تا در اتصال دوباره استفاده شوند.

Acknowledgements (تاییدها): Socket.io یک روش ساده برای فرستادن رویدادها  و دریافت پاسخ ها فراهم می کند.

broadcasting (پخش گسترده): در سمت سرور می توان یک رویداد را به همه کاربران متصل یا گروه ویژه ای از کاربران فرستاد.

multiplexing (تسهیم): فضاهای نام باعث می شوند تا بتوانیم منطق برنامه را بر روی یک اتصال مشترک تقسیم کنیم. این ویژگی به عنوان نمونه برای وقتی که بخواهیم یک کانال به نام admin داشته باشیم تا تنها کاربران احراز هویت شده به آن دسترسی داشته باشند مفید است.

سرور Socket.IO سه نوع رویداد ویژه را می فرستد (emit می کند):

    1. initial_headers
    2. headers
    3. connection_error

توضیح این سه رویداد در زیر آمده است.

initial_headers: درست پیش از نوشتن هدرهای reponse اولین درخواست HTTP سشن (دست دادن) منتشر می شود و به شما امکان می دهد آن ها را سفارشی کنید.

io.engine.on("initial_headers", (headers, req) => {
  headers["test"] = "123";
  headers["set-cookie"] = "mycookie=456";
});

headers: درست پیش از نوشتن هدرهای reponse هر درخواست HTTP سشن (از جمله ارتقاWebSocket) منتشر می‌شود و به شما امکان می‌دهد آن ها را سفارشی کنید.

io.engine.on("headers", (headers, req) => {
  headers["test"] = "789";
});

connection_error: هنگامی که یک اتصال به طور غیر عادی بسته شود، منتشر می شود.

io.engine.on("connection_error", (err) => {
  console.log(err.req);      // the request object
  console.log(err.code);     // the error code, for example 1
  console.log(err.message);  // the error message, for example "Session ID unknown"
  console.log(err.context);  // some additional error context
});

همچنین یک مکانیسم مانند ضربان قلب وجود دارد که بررسی می‌کند ارتباط بین سرور و کلاینت هنوز فعال است. در یک بازه زمانی معین (مقدار pingInterval ارسال شده در handsake) سرور یک بسته PING ارسال می کند و کلاینت چند ثانیه (مقدار pingTimeout)  فرصت دارد تا یک بسته PONG را پس بگیرد. اگر سرور یک بسته PONG را دریافت نکند، در نظر خواهد گرفت که اتصال بسته شده است. برعکس، اگر کلاینت یک بسته PING را در pingInterval + pingTimeout دریافت نکند، در نظر خواهد گرفت که اتصال بسته است.

Broadcasting

Broadcasting یا پخش گسترده یعنی فرستادن پیام به همه کلاینت هایی که متصل (connected) هستند.Broadcasting به سه شکل می تواند انجام شود:

  1. فرستادن پیام به همه کاربرانی که متصل هستند
  2. فرستادن پیام به همه کاربرانی که در یک اتاق هستند
  3. فرستادن پیام به همه کاربرانی که در یک فضای نام هستند

اگر بخواهیم پیام را به همه از جمله خودمان بفرستیم از io.sockets.emit(‘broadcast’) و اگر بخواهیم به همه به جز به خودمان بفرستیم از socket.broadcast.emit(‘arbitrary name’) با یک رویداد با نام دلخواه استفاده می کنیم. توجه داشته باشید که پخش گسترده یا broadcasting یک ویژگی مختص سرور است.

رویدادها را به همه کاربران متصل از جمله خود فرستده می فرستد
io.emit("hello", "world");

کلاینت هایی که در حال حاضر قطع شده اند (یا در حال اتصال مجدد هستند) رویداد را دریافت نخواهند کرد. بسته به مورد استفاده شما، ذخیره این رویداد در جایی (مثلا در یک پایگاه داده) به شما بستگی دارد.

رویدادها را به همه کاربران متصل به جز فرستده می فرستد
io.on("connection", (socket) => {
  socket.broadcast.emit("hello", "world");
});

در مثال بالا، با استفاده از socket.emit("hello world")، رویداد را برای «کلاینت» A  ارسال می کنیم.

در موارد خاص، ممکن است بخواهید فقط به کلاینت هایی که به سرور فعلی متصل broadcast کنید. می توانید با فلگ local به این هدف برسید:

io.local.emit("hello", "world");

متد‌های کاربردی در Socket.IO

متد‌های کاربردی زیر را در Socket.IO برای مدیریت نمونه‌های Socket و اتاق‌ (room) های آن‌ها داریم:

socketsJoin: باعث می شود نمونه های سوکت منطبق به اتاق های مشخص شده بپیوندند.

// make all Socket instances join the "room1" room
io.socketsJoin("room1");

// make all Socket instances in the "room1" room join the "room2" and "room3" rooms
io.in("room1").socketsJoin(["room2", "room3"]);

// make all Socket instances in the "room1" room of the "admin" namespace join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");

// this also works with a single socket ID
io.in(theSocketId).socketsJoin("room1");

socketsLeave: باعث می شود نمونه های سوکت منطبق از اتاق های مشخص شده خارج شوند.

// make all Socket instances leave the "room1" room
io.socketsLeave("room1");

// make all Socket instances in the "room1" room leave the "room2" and "room3" rooms
io.in("room1").socketsLeave(["room2", "room3"]);

// make all Socket instances in the "room1" room of the "admin" namespace leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");

// this also works with a single socket ID
io.in(theSocketId).socketsLeave("room1");

disconnectSockets: باعث می‌شود نمونه‌های سوکت منطبق قطع شوند.

// make all Socket instances disconnect
io.disconnectSockets();

// make all Socket instances in the "room1" room disconnect (and discard the low-level connection)
io.in("room1").disconnectSockets(true);

// make all Socket instances in the "room1" room of the "admin" namespace disconnect
io.of("/admin").in("room1").disconnectSockets();

// this also works with a single socket ID
io.of("/admin").in(theSocketId).disconnectSockets();

fetchSockets: نمونه‌های سوکت منطبق را برمی‌گرداند.

// return all Socket instances of the main namespace
const sockets = await io.fetchSockets();

// return all Socket instances in the "room1" room of the main namespace
const sockets = await io.in("room1").fetchSockets();

// return all Socket instances in the "room1" room of the "admin" namespace
const sockets = await io.of("/admin").in("room1").fetchSockets();

// this also works with a single socket ID
const sockets = await io.in(theSocketId).fetchSockets();

ویژگی data یک شی دلخواه است که می تواند برای اشتراک گذاری اطلاعات بین سرورهای Socket.io استفاده شود:

// server A
io.on("connection", (socket) => {
  socket.data.username = "alice";
});

// server B
const sockets = await io.fetchSockets();
console.log(sockets[0].data.username); // "alice"

serverSideEmit: این متد اجازه می دهد تا رویدادها را به سایر سرورهای Socket.IO در یک کلاستر، در یک راه اندازی multi-server، بفرستد.

io.serverSideEmit("hello", "world");

و در سمت دریافت کننده:

io.on("hello", (arg1) => {
  console.log(arg1); // prints "world"
});

ویژگی های کلاس Socket

id: به هر اتصال جدید یک شناسه تصادفی 20 کاراکتری اختصاص داده می شود.این شناسه با مقدار سمت سرور همگام سازی می شود.

connected: این ویژگی توضیح می دهد که آیا سوکت در حال حاضر به سرور متصل است یا خیر.

socket.on("connect", () => {
  console.log(socket.connected); // true
});

socket.on("disconnect", () => {
  console.log(socket.connected); // false
});

io

socket.on("connect", () => {
  const engine = socket.io.engine;
  console.log(engine.transport.name); // in most cases, prints "polling"

  engine.once("upgrade", () => {
    // called when the transport is upgraded (i.e. from HTTP long-polling to WebSocket)
    console.log(engine.transport.name); // in most cases, prints "websocket"
  });

  engine.on("packet", ({ type, data }) => {
    // called for each packet received
  });

  engine.on("packetCreate", ({ type, data }) => {
    // called for each packet sent
  });

  engine.on("drain", () => {
    // called when the write buffer is drained
  });

  engine.on("close", (reason) => {
    // called when the underlying connection is closed
  });
});


رویدادهای Socket.io

نمونه Socket سه رویداد خاص را منتشر می کند:

  • connect
  • connect_error
  • disconnect

این رویدادها را در زیر بررسی خواهیم کرد.

رویداد connectاین رویداد پس از اتصال و اتصال مجدد توسط نمونه سوکت فعال می شود.

socket.on("connect", () => {
  // ...
});

لطفا توجه داشته باشید که نباید کنترل‌کننده‌های رویداد را در خود کنترل‌کننده اتصال بنویسید (register کنید)، زیرا هر بار که Socket دوباره وصل می‌شود، یک کنترل‌کننده جدید ثبت می‌شود:

// BAD
socket.on("connect", () => {
  socket.on("data", () => { /* ... */ });
});

// GOOD
socket.on("connect", () => {
  // ...
});

socket.on("data", () => { /* ... */ });

رویداد connect_error:این رویداد زمانی فعال می شود که:

  • اتصال سطح پایین را نمی توان برقرار کرد
  • اتصال توسط سرور در یک تابع میان‌افزار رد می‌شود

در حالت اول، Socket به طور خودکار سعی می کند پس از یک تاخیر معین دوباره وصل شود.

در مورد دوم، باید به صورت دستی دوباره وصل شوید. ممکن است لازم باشد credential ها را به روز کنید:

// either by directly modifying the `auth` attribute
socket.on("connect_error", () => {
  socket.auth.token = "abcd";
  socket.connect();
});

// or if the `auth` attribute is a function
const socket = io({
  auth: (cb) => {
    cb(localStorage.getItem("token"));
  }
});

socket.on("connect_error", () => {
  setTimeout(() => {
    socket.connect();
  }, 1000);
});

رویداد disconnect: این رویداد پس از قطع ارتباط فعال می شود.

socket.on("disconnect", (reason) => {
  // ...
});

رویدادها و اشیا در Socket.io

Socket یک کلاس پایه برای برقراری هم کنش و ارتباط با کلاینت است. این کلاس همه متد ها و ویژگی های EventEmitter مانند on ,once ,removeListener و emit را به ارث می برد.به هر اتصال جدید یک شناسه تصادفی 20 کاراکتری اختصاص داده می شود.این شناسه با مقدار سمت کلاینت همگام سازی می شود.

// server-side
io.on("connection", (socket) => {
  console.log(socket.id); // ojIckSD2jqNzOqIrAGzL
});

// client-side
socket.on("connect", () => {
  console.log(socket.id); // ojIckSD2jqNzOqIrAGzL
});

ویژگی های مفید socket.io در زیر آمده اند:

شی id: پس از ایجاد، Socket به اتاقی که با id خودش مشخص شده می‌پیوندد، به این معنی که می‌توانید از آن برای پیام‌های خصوصی استفاده کنید:

io.on("connection", socket => {
  socket.on("private message", (anotherSocketId, msg) => {
    socket.to(anotherSocketId).emit("private message", socket.id, msg);
  });
});

شی rooms: اشاره ای به اتاق هایی است که سوکت در حال حاضر در آن ها قرار دارد

io.on("connection", (socket) => {
  console.log(socket.rooms); // Set { <socket.id> }
  socket.join("room1");
  console.log(socket.rooms); // Set { <socket.id>, "room1" }
});

شی data: می تواند همراه با متد fetchSockets استفاده شود:

// server A
io.on("connection", (socket) => {
  socket.data.username = "alice";
});

// server B
const sockets = await io.fetchSockets();
console.log(sockets[0].data.username); // "alice"

شی conn: ارجاع به سوکت Engine.IO است.

io.on("connection", (socket) => {
  console.log("initial transport", socket.conn.transport.name); // prints "polling"

  socket.conn.once("upgrade", () => {
    // called when the transport is upgraded (i.e. from HTTP long-polling to WebSocket)
    console.log("upgraded transport", socket.conn.transport.name); // prints "websocket"
  });

  socket.conn.on("packet", ({ type, data }) => {
    // called for each packet received
  });

  socket.conn.on("packetCreate", ({ type, data }) => {
    // called for each packet sent
  });

  socket.conn.on("drain", () => {
    // called when the write buffer is drained
  });

  socket.conn.on("close", (reason) => {
    // called when the underlying connection is closed
  });
});

راه های فرستادن رویداد بین سرور و کلاینت

برای فرستادن رویدادها بین سرور و کلاینت راه های زیر را داریم:

  • emit
  • acknowledges
  • بدون timeout: می توانید برای هر emit یک بازه زمانی تعیین کنید:
socket.timeout(5000).emit("my-event", (err) => {
  if (err) {
    // the other side did not acknowledge the event in the given delay
  }
});

هم چنین می‌توانید از timeout و acknowledges استفاده کنید:

socket.timeout(5000).emit("my-event", (err, response) => {
  if (err) {
    // the other side did not acknowledge the event in the given delay
  } else {
    console.log(response);
  }
});

رویدادهای volatile: رویدادهای voltile رویدادهایی هستند که در صورت آماده نبودن اتصال اصلی ارسال نمی شوند.این رویدادها می توانند برای مثال اگر شما نیاز به ارسال موقعیت شخصیت ها در یک بازی آنلاین دارید مفید باشند:

socket.volatile.emit("hello", "might or might not be received");

راه های مدیریت رویداد

راه های مختلفی برای مدیریت رویدادهایی که بین سرور و کلاینت منتقل می شوند وجود دارد.این راه ها در زیر بررسی شده اند:

متدهای EventEmmiter: در سمت سرور، نمونه Socket کلاسNode.js EventEmitter را گسترش می دهد.

در سمت کلاینت، نمونه Socket از فرستنده رویداد ارائه شده توسط کتابخانه component-emitter استفاده می کند که زیر مجموعه ای از متدهای EventEmitter را نشان می دهد.

socket.on(eventName, listener)

تابع شنونده را برای رویدادی با نام eventName به انتهای آرایه شنوندگان اضافه می کند.

socket.on("details", (...args) => {
  // ...
});

socket.once(eventName, listener)

یک تابع شنونده one-time را برای رویدادی به نام eventName اضافه می کند:

  socket.once("details", (...args) => {
  // ...
});

socket.off(eventName, listener)

شنونده مشخص شده را از آرایه شنونده رویداد با نام eventName حذف می کند.

const listener = (...args) => {
  console.log(args);
}

socket.on("details", listener);

// and then later...
socket.off("details", listener);

socket.removeAllListeners([eventName])

همه شنوندگان یا شنوندگان مشخص شده با eventName را حذف می کند.

// for a specific event
socket.removeAllListeners("details");
// for all events
socket.removeAllListeners();

گرفتن (catch کردن) همه شنوندگان: از زمان Socket.IO نسخه 3، یک API جدید با الهام از کتابخانه EventEmitter2 اجازه می‌دهد تا شنونده‌های catch-all را تعریف کنید.این ویژگی هم روی کلاینت و هم روی سرور موجود است.

socket.onAny(listener)

شنونده‌ای اضافه می‌کند که هنگام انتشار هر رویدادی فعال می‌شود.

socket.onAny((eventName, ...args) => {
  // ...
});

socket.prependAny(listener)

شنونده‌ای اضافه می‌کند که هنگام انتشار هر رویدادی فعال می‌شود. شنونده به ابتدای آرایه شنوندگان اضافه می شود.

socket.prependAny((eventName, ...args) => {
  // ...
});

socket.offAny([listener])

همه شنونده‌های catch-all یا شنونده داده شده را حذف می‌کند.

const listener = (eventName, ...args) => {
  console.log(eventName, args);
}

socket.onAny(listener);

// and then later...
socket.offAny(listener);

// or all listeners
socket.offAny();

middlewareها (میان افزارها)

تابع میان افزار تابعی است که برای هر اتصال ورودی اجرا می شود. توابع میان افزار می توانند برای موارد زیر مفید باشند:

  • logging
  • authentication / authorization
  • rate limiting

توجه: این تابع تنها یک بار در هر اتصال اجرا می شود (حتی اگر اتصال شامل چندین درخواست HTTP باشد).

یک تابع میان‌افزار به نمونه Socket و تابع میان‌افزار ثبت‌شده next دسترسی دارد.

io.use((socket, next) => {
  if (isValid(socket.request)) {
    next();
  } else {
    next(new Error("invalid"));
  }
});

شما می توانید چندین تابع میان افزار را بنویسید، و آن ها به صورت متوالی اجرا می شوند:

io.use((socket, next) => {
  next();
});

io.use((socket, next) => {
  next(new Error("thou shall not pass"));
});

io.use((socket, next) => {
  // not executed, since the previous middleware has returned an error
  next();
});

حتما ()next را فراخوانی کنید. در غیر این صورت، اتصال تا زمانی که پس از یک بازه زمانی مشخص بسته نشود، معلق باقی می ماند.

نکته مهم: هنگام اجرای میان‌افزار، نمونه Socket متصل نمی‌شود، به این معنی که در صورت عدم موفقیت اتصال، هیچ رویدادdisconnect  منتشر نخواهد شد.

فضاهای نام (namespaceها)

Socket.io به شما این امکان را می دهد که سوکت های خود را در "فضای نام" قرار دهید، که در اصل به معنای اختصاص نقاط پایانی یا مسیرهای مختلف است.

فضای نام یک ویژگی مفید برای به حداقل رساندن تعداد منابع (اتصال های TCP) است جداسازی نگرانی ها در برنامه شما با معرفی جداسازی بین کانال های ارتباطی است.

فضاهای نام چندگانه در واقع اتصال WebSockets یکسانی را به اشتراک می‌گذارند و بنابراین پورت‌های سوکت را روی سرور ذخیره می‌کنند.فضاهای نام در سمت سرور ایجاد می شوند. با این حال، کلاینت ها با ارسال یک درخواست به سرور به آن ها ملحق می شوند.

فضای نام یک کانال ارتباطی است که به شما امکان می‌دهد منطق برنامه خود را بر روی یک اتصال مشترک تقسیم کنید (که به آن «مالتی پلکس») نیز می‌گویند.

هر فضای نامی ویژگی های زیر را دارد:

  • event handlers (مدیریت کنندگان رویداد)
  • rooms (اتاق ها)
  • middlewares (میان افزار ها)

فضای نام ریشه '/' فضای نام پیش‌فرض است.همه اتصال های به سرور با استفاده از شی socket در سمت کلاینت به فضای نام پیش فرض یعنی ‘/’ انجام می شود.

var socket = io();

کد بالا کلاینت را به فضای نام متصل می کند.فضاهای نام می توانند با استفاده از تابع of در سمت سرور ساخته شوند.

کد سمت سرور:

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
   res.sendFile('E:/test/index.html');});
   
var nsp = io.of('/my-namespace');
nsp.on('connection', function(socket){
   console.log('someone connected');
   nsp.emit('hi', 'Hello everyone!');
});
http.listen(3000, function(){
   console.log('listening on localhost:3000');
});

کد سمت کلاینت:

<!DOCTYPE html>
<html>
   <head><title>Hello world</title></head>
   <script src="/socket.io/socket.io.js"></script>
   <script>
      var socket = io('/my-namespace');
      socket.on('hi',function(data){
         document.body.innerHTML = '';
         document.write(data);
      });
   </script>
   <body></body>
 </html>

در فضاهای نام می توان کانال های اختیاری ایجاد کرد که کاربران می توانند به آن ها وارد شوند یا آن ها را ترک کنند.این کانال ها اتاق نامیده می شوند.با متد join وارد اتاق و با متد leave آن را ترک می کنیم.

API کلاینت رویدادهای زیر را در اختیار ما قرار می دهد:

  • connect - هنگامی که کلاینت با موفقیت وصل می شود.
  • connectting - هنگامی که کلاینت در حال اتصال است.
  • disconnect - هنگامی که کلاینت قطع می شود.
  • connect_failed - هنگامی که اتصال به سرور با شکست مواجه می شود.
  • error - یک رویداد خطا از سرور ارسال می شود.
  • message - وقتی سرور با استفاده از تابع ارسال پیامی را ارسال می کند.
  • reconnect - زمانی که اتصال مجدد به سرور موفقیت آمیز باشد.
  • reconnecting - زمانی که کلاینت در حال اتصال است.
  • reconnect_failed - زمانی که تلاش برای اتصال مجدد با شکست مواجه شد.

به عنوان مثال - اگر اتصالی داریم که خراب می شود، می توانیم از کد زیر برای اتصال دوباره به سرور استفاده کنیم:

socket.on('connect_failed', function() {
   document.write("Sorry, there seems to be an issue with the connection!");
})

Room (اتاق)

(room) اتاق یک کانال دلخواه است که سوکت ها می توانند به آن بپیوندند و از آن خارج شوند. می توان از آن برای پخش رویدادها به زیر مجموعه ای از کلاینت ها استفاده کرد:

توجه داشته باشید که اتاق ها مختص سرور هستند (یعنی کلاینت به لیست اتاق هایی که به آن ها ملحق شده دسترسی ندارد). برای عضویت در سوکت در یک کانال خاص می توانید متد join را فرخوانی کنید:

io.on("connection", (socket) => {
  socket.join("some room");
});

و سپس به سادگی از to یا in  ( یکسان هستند) هنگام broadcast یا emit استفاده کنید:

io.to("some room").emit("some event");

می توان به طور همزمان به چند اتاق emit داشته باشیم:

io.to("room1").to("room2").to("room3").emit("some event");

در آن صورت، یک اتحاد انجام می شود.هر سوکتی که حداقل در یکی از اتاق ها باشد، یک بار رویداد را دریافت می کند (حتی اگر سوکت در دو یا چند اتاق باشد). همچنین می توانید از یک سوکت معین به یک اتاق broadcast کنید:

io.on("connection", (socket) => {
  socket.to("some room").emit("some event");
});

در این صورت، هر سوکت در اتاق به استثنای فرستنده، رویداد را دریافت می کند:

برای خروج از یک کانال به همان روشی که عضویت را join کنید، آن را leave کنید. هر سوکت در Socket.IO با یک شناسه تصادفی، غیرقابل حدس زدن و منحصر به فرد، به نام id شناسایی می شود. برای راحتی شما، هر سوکت به طور خودکار به اتاقی می‌پیوندد که با شناسه خودش مشخص شده است. این امر اجرای پیام های خصوصی را آسان می کند:

io.on("connection", (socket) => {
  socket.on("private message", (anotherSocketId, msg) => {
    socket.to(anotherSocketId).emit("private message", socket.id, msg);
  });
});

ویژگی «room» توسط چیزی که ما آداپتور می نامیم پیاده سازی می شود. این آداپتور جزئی از سمت سرور است که مسئول موارد زیر است:

  • ذخیره سازی روابط بین نمونه های سوکت و room ها
  • broadcast کردن رویدادها برای همه (یا زیر مجموعه ای از) کلاینت ها

رویدادهای Room در زیر آمده اند:

  • create-room (argument: room)
  • delete-room (argument: room)
  • join-room (argument: room, id)
  • leave-room (argument: room, id)

آداپتور

آداپتور جزئی از سمت سرور است که مسئول broadcast کردن رویدادها به همه یا زیر مجموعه ای از کلاینت ها است.

هنگام مقیاس‌گذاری روی چندین سرور Socket.IO، باید آداپتور پیش‌فرض درون حافظه را با پیاده‌سازی دیگری جایگزین کنید، بنابراین رویدادها به درستی برای همه کلاینت‌ها هدایت می‌شوند.

علاوه بر آداپتور درون حافظه، چهار پیاده سازی رسمی وجود دارد:

توجه داشته باشید که هنگام استفاده از چندین سرور Socket.IO و نظرسنجی طولانی HTTP هم چنان به فعال کردن sticky session ها نیاز است.

شما می توانید با کدهای زیر به نمونه آداپتور دسترسی داشته باشید:

// main namespace
const mainAdapter = io.of("/").adapter; // WARNING! io.adapter() will not work
// custom namespace
const adminAdapter = io.of("/admin").adapter;

ساخت یک برنامه ساده با Socket.io

برای ساخت این پروژه به nodejs نیاز داریم.هم چنین به یک ویرایشگر کد مانند Vs Code نیاز داریم. اگر nodejs را نصب نکرده اید می توانید آن را از این نشانی دانلود کنید. هم چنین می توانید Vs Code را از این نشانی دانلود و نصب کنید. البته می توانید از هر ویرایشگر کدی که خواستید استفاده کنید.

نوشتن یک برنامه چت با استک های برنامه نویسی محبوب مانند LAMP(PHP) می تواند بسیار سخت و پیچیده باشد. برنامه های ساخته شده با این پشته ها بسیار کندتر از آن چه باید باشند، هستند. زیرا برای تغییرات باید به طور پیوسته سرور را راه اندازی دوباره بکنند و در حین انجام این کار نیز باید timestamp ها را بررسی بکنند.

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

فریم ورک (چهار چوب) وب

اولین کاری که باید بکنیم ساخت یک صفحه ساده HTML است که یک فرم و لیستی از همه پیام ها را نمایش می دهد. برای ساخت برنامه از وب فریمورک Node.JS استفاده می کنیم. مطمئن شوید که Node.JS نصب شده است.

init کردن پروژه (ساخت پروژه)

ابتدا یک پوشه با نام chat-app در هر جایی از سیستم که دوست داریم ایجاد می کنیم. نام آن دلخواه است و این نام در واقع مشخص کننده نام پروژه است. این پوشه را با استفاده از ویرایشگر کد خود باز می کنیم. من از vs code استفاده می کنم.

ترمینال را باز می کنیم و با استفاده از دستور npm init -y پروژه خود را می سازیم.ترمینال را می توان از نوار بالایی vs code باز کرد.

سپس با استفاده از گزینه New Terminal ترمینال را باز می کنیم:

دستور npm init -y را در ترمینال می نویسیم و سپس Enter را می زنیم.پس از این کار باید تصویر زیر را ببینیم:

پس از این کار یک فایل package.json ساخته می شود. فایل package.json قلب هر پروژه Node است. این فایل ویژگی های عملکردی پروژه را تعریف می کند و npm برای نصب وابستگی ها، اجرای اسکریپت ها و شناسایی فایل اصلی برنامه یا همان entry point از آن استفاده می کند.

اگر این فایل را باز کنیم تصویر زیر را خواهیم دید:

برای اطلاع بیش تر در مورد این فایل به این نشانی مراجعه کنید.در ادامه برای ساخت برنامه باید package های زیر را نصب کنیم:

  • express (یک فریم ورک وب برای ساخت یک برنامه js)
  • socket.io (برای ایجاد ارتباط بین سرور و کلاینت و فرستادن پیام ها)
  • nodemon (nodemon ابزاری است که به توسعه برنامه‌های مبتنی بر js با راه‌اندازی خودکار برنامه هنگام شناسایی تغییرات، کمک می‌کند.)

با دستور npm i مانند تصویر زیر آن ها را نصب می کنیم:

سپس فایل index.js را که فایل اصلی برنامه است ایجاد می کنیم و کد زیر را در آن قرار می دهیم:

const express = require('express');

const app = express();

const http = require('http');

const server = http.createServer(app);




app.get('/', (req, res) => {

  res.send('<h1>Hello world</h1>');

});




server.listen(3000, () => {

  console.log('listening on *:3000');

});

برای اجرای برنامه تغییر زیر را در فایل package.json  می دهیم:

برای اجرای برنامه و دیدن نتیجه از دستور npm run dev استفاده می کنیم.این دستور در ترمینال می نویسیم و سپس Enter را می زنیم.پس از این کار باید تصویر زیر را ببینیم:

برای دیدن برنامه به نشانی http://localhost:3000/ می رویم. با این کار باید تصویر زیر را ببینیم:

نمایش فایل های HTML

در index.js برای نمایش HTML متد res.send را فراخوانی کردیم و تگ h1 را به عنوان یک رشته به آن فرستادیم. اگر همه فایل HTML برنامه را در این متد قرار دهیم، کد ما بسیار گیج کننده و آشفته به نظر می رسد و هم چنین خوانایی آن کاهش می یابد. برای جلوگیری از چنین وضعی یک فایل index.html ایجاد می کنیم و کدهای HTML را در آن قرار می دهیم.

به جای send از sendFile استفاده می کنیم تا فایل HTML نمایش داده شود.

app.get('/', (req, res) => {

  res.sendFile(__dirname + '/index.html');

});

کد زیر را در فایل index.html قرار می دهیم:

<!DOCTYPE html>

<html>

  <head>

    <title>Socket.IO chat</title>

    <style>

      body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }




      #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }

      #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }

      #input:focus { outline: none; }

      #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }




      #messages { list-style-type: none; margin: 0; padding: 0; }

      #messages > li { padding: 0.5rem 1rem; }

      #messages > li:nth-child(odd) { background: #efefef; }

    </style>

  </head>

  <body>

    <ul id="messages"></ul>

    <form id="form" action="">

      <input id="input" autocomplete="off" /><button>Send</button>

    </form>

  </body>

</html>

حالا اگر دوباره به http://localhost:3000/ برویم باید تصویر زیر را ببینیم:

استفاده از Socket.io

Socket.io در دو بخش نوشته شده است:

  1. یک سرور که با سرور HTTP از node js ترکیب می شود.
  2. یک کتابخانه کلاینت که در سمت مرورگر io-client بارگذاری می شود.

همان طور که خواهیم دید، در طول توسعه، socket.io به طور خودکار به کلاینت سرویس می دهد.

socket.io را قبلا نصب کردیم. اگر به فایل package.json نگاه بیاندازیم آن را خواهیم دید:

socket.io را وارد فایل index.js می کنیم و تغییرهای زیر را در این فایل به کار می بندیم:

const express = require('express');

const app = express();

const http = require('http');

const server = http.createServer(app);

const { Server } = require("socket.io");

const io = new Server(server);




app.get('/', (req, res) => {

  res.sendFile(__dirname + '/index.html');

});




io.on('connection', (socket) => {

  console.log('a user connected');

});




server.listen(3000, () => {

  console.log('listening on *:3000');

});

توجه داشته باشید که یک نمونه جدید از socket.io را با فرستادن شی server (سرور HTTP) مقداردهی اولیه می کنیم. سپس به رویداد connection برای سوکت های ورودی گوش می دهیم و آن ها را به کنسول نمایش می دهیم. در واقع هرگاه یک کاربر متصل شود رویداد connection رخ می دهد و عبارت «a user connected» در کنسول نمایش داده می شود.

گام بعدی استفاده از socket.io در سمت کلاینت است. کد بالا مربوط به قسمت سرور بود. کد زیر را در فایل index.html قرار می دهیم:

  <script src="/socket.io/socket.io.js"></script>

    <script>

        let socket = io()

    </script>

اگر به نشانی http://localhost:3000/ برویم و یکبار صفحه را رفرش کنیم باید نوشته “a user connected” را در کنسول خود ببینیم:

اگر می‌خواهید از نسخه لوکال استفاده کنید، می‌توانید آن را در node_modules/socket.io/client-dist/socket.io.js پیدا کنید.

توجه داشته باشید که هنگام فراخوانی io() هیچ URL را برای آن در نظر نمی گیریم، زیرا به طور پیش‌فرض به / متصل می شود.

هر socket به طور پیش فرض رویداد disconnect را نیز گسیل می کند.اگر کاربر صفحه خود را ببندد یا اتصال قطع شود این رویداد فراخوانی می گردد. کد زیر را در فایل index.js قرار می دهیم و قسمت io.on را بازنویسی می کنیم:

io.on('connection', (socket) => {

  console.log('a user connected');

  socket.on("disconnect",()=>{

      console.log("user disconnected");

  })

});

اگر صفحه ای را که برنامه در  آن در حال اجرا است ببندیم باید نوشته «user disconnected» را در کنسول ببینیم:

emit (فرستادن) رویدادها

هدف اصلی از ساخت Socket.IO این است که بتوان هر رویدادی را که می خواهیم همراه با داده های دلخواه، ارسال و دریافت کنیم. هر شی از نوع JSON و داده های باینری می توانند همراه با رویداد فرستاده شوند.

می خواهیم کاری کنیم که وقتی کاربر پیامی را در input تایپ می کند، سرور آن را به عنوان یک رویداد با نام «chat_message» دریافت کند.یعنی یک رویداد از سمت کلاینت به سرور بفرستیم. بخش اسکریپت در index.html اکنون باید به صورت زیر نوشته شود:

    <script>

       let socket = io()




       let input=document.getElementById("input")

       let form=document.getElementById("form")




       form.addEventListener("submit",function (e) {

        e.preventDefault()

        if(input.value){

            socket.emit("chat_message",input.value)

            input.value=""

        }

       })

    </script>

در کد بالا input و form را از DOM می گیریم و آن ها را در متغییرهای input و form ذخیره می کنیم. سپس به form یک رویداد submit اضافه می کنیم.می خواهیم هرگاه این فرم submit می شود یک رویداد chat_message به سمت سرور فرستاده شود. به شرطی که input خالی نباشد.

قبلا اشاره کردیم که با رویدادها می توان دادهایی را فرستاد.داده ای که در این جا می فرستیم همان چیزی است که کاربر در  input می نویسد.این داده را از input.value دریافت می کنیم و سپس آن را با دستور socket.emit(“chat_message”,input.value) به سمت سرور می فرستیم.  پس از submit کردن input را با دستور input.value=”” خالی می کنیم تا بتوان پیام های دیگری نیز فرستاد.

اکنون نوبت دریافت رویداد فرستاده داده شده از سمت کلاینت یعنی رویداد chat_message است.کد زیر را در index.js قرار می دهیم و درواقع آن را ویرایش می کنیم:

io.on('connection', (socket) => {

//   console.log('a user connected');

    socket.on("chat_message",(msg)=>{

        console.log("user message: " + msg);

    })

    socket.on("disconnect",()=>{

        console.log("user disconnected");

    })

});

اکنون سراغ فرستادن پیام می رویم.صفحه وب را باز می کنیم و به عنوان مثال کلمه hello را در input می نویسیم و سپس دکمه Send را می زنیم:

پس از این کار اگر ترمینال را نگاه کنیم باید تصویر زیر را ببینیم:

پخش گسترده یا Broadcasting

هدف بعدی در این جا این است که یک رویداد را از سرور به همه کاربران بفرستیم.برای فرستادن یک رویداد به همه، Socket.IO متد ()io.emit  را در اختیار ما قرار می دهد.

io.emit('some event', { someProperty: 'some value', otherProperty: 'other value' }); // This will emit the event to all connected sockets

کد بالا رویداد را به همه کاربران از جمله به کسی که آن را فرستاده است نیز می فرستد. اگر بخواهیم رویداد به همه ارسال شود ولی به خود فرستده ارسال نشود از کد زیر استفاده می کنیم:

io.on('connection', (socket) => {

    socket.broadcast.emit('hi');

});

برای سادگی کار رویداد را به همه از جمله خود فرستنده می فرستیم. باید توجه داشته باشیم که broadcast کردن متخص سمت سرور است.

کد زیر را در index.js قرار می دهیم:

io.on('connection', (socket) => {

//   console.log('a user connected');

    socket.on("chat_message",(msg)=>{

        // console.log("user message: " + msg);

        io.emit('chat_message', msg);

    })

    socket.on("disconnect",()=>{

        console.log("user disconnected");

    })

});

سپس سراغ کلاینت خود یا همان فایل index.js می رویم.برای نمایش همه پیام های فرستاده شده و دریافت شده تگ ul با id=messages داریم. آن را از DOM می گیریم و سپس در متغیر messages می گذاریم.

 <script>

       let socket = io()




       let input=document.getElementById("input")

       let form=document.getElementById("form")

       let messages=document.getElementById("messages")




       form.addEventListener("submit",function (e) {

        e.preventDefault()

        if(input.value){

            socket.emit("chat_message",input.value)

            input.value=""

        }

       })




       socket.on("chat_message",(msg)=>{

           let item=document.createElement("li")

           item.textContent=msg

           messages.appendChild(item)

           window.scrollTo(0, document.body.scrollHeight);

       })

    </script>

پس از دریافت رویداد chat_message توسط کلاینت باید آن را مدیریت کنیم. می خواهیم همه پیام ها در صفحه نمایش داده شوند. برای هر پیام یک li درست می کنیم و سپس آن پیام را در آن قرار می دهیم.


منبع: وب سایت socket

نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.