برنامه‌نویسی

آموزش Socket.io


۶ مرداد ۱۳۹۹
آموزش Socket.io

چندین راه برای ایجاد برنامه‌های ارتباطی real-time وجود دارد. دو مورد از شناخته شده‌ترین راه‌ها برای این کار، استفاده از HTTP Long Polling و یا WebSocketها است. در این مقاله خلاصه‌ای از مقایسه میان این دو را مطالعه خواهید کرد. اما تصمیم گرفتیم که در این مقاله بر روی WebSocketها تمرکز کنیم. به این دلیل این مقاله آموزش Socket.io برای ساخت یک برنامه چت real-time است.

WebSockets چیست؟

WebSockets API یک تکنولوژی است که یک کانال دو طرفه برای ارتباط میان کلاینت و سرور را فراهم می‌کند. این بدان معنی است که دیگر لازم نیست کلاینت، به هنگام درخواست دیتا از سرور، آغاز کننده transaction باشد.

به هنگام ایجاد اولین درخواست به سمت سرور، به غیر از دریافت دیتا، یک ارتباط WebSocket ایجاد می‌کند. این فرآیند، تحت عنوان WebSocket handshake نیز شناخته می‌شود. هدر Upgrade نیز در درخواست وجود دارد. این روشی است که کلاینت به سرور اطلاع می‌دهد که نیاز دارد اتصالی را ایجاد کند.

WebSockets در مقابل HTTP

بنابراین، آیا این بدان معنی است که ایجاد برنامه‌های چت و گفت‌وگو، بدون WebSockets غیرممکن است؟

بسیار خب، تکنیکی تحت عنوان HTTP Long Polling وجود دارد. با استفاده از این، کلاینت درخواستی را به سمت سرور ارسال می‌کند و سرور این اتصال را تا زمانی که دیتا جدیدی در دسترس باشد، باز نگه می‌دارد. به هنگامی که دیتا در دسترس قرار بگیرد، کلاینت آن را دریافت می‌کند، سپس درخواست جدید ایجاد و ارسال می‌شود، این عملیات بار‌ها و بار‌ها تکرار می‌شود. به هر حال، یک عیب بزرگ در استفاده از HTTP Long Polling، استفاده زیاد از منابع سرور است.

ایجاد یک برنامه چت توسط Socket.io

در ادامه به طور خلاصه به شما آموزش می‌دهیم که چگونه توسط Socket.io، به همراه Vanilla JS برای فرانت‌اند و NodeJS به عنوان سرور، یک برنامه چت ساده بسازید. ابتدا نیاز دارید که یک پروژه به همراه فایل package.json ایجاد کنید:

ایجاد یک پروژه توسط دستور npm

بعد از ایجاد پروژه، نیاز است که سرور را راه‌اندازی و تمام وابستگی‌ها را نصب کنید. برای انجام این کار، ابتدا فایل index.js را ایجاد و socket.io و express را نصب کنید. برای این کار، می‌توانید از دستور زیر استفاده کنید:

touch index.js && npm install express socket.io && npm install --save-dev nodemon

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

const express = require("express");
const socket = require("socket.io");

// App setup
const PORT = 5000;
const app = express();
const server = app.listen(PORT, function () {
  console.log(`Listening on port ${PORT}`);
  console.log(`http://localhost:${PORT}`);
});

// Static files
app.use(express.static("public"));

// Socket setup
const io = socket(server);

io.on("connection", function (socket) {
  console.log("Made socket connection");
});

حالا نیاز است که سمت فرانت‌اند را آماده کنید. ابتدا با ایجاد پوشه public به همراه فایل‌های index.html و main.js، این کار را شروع می‌کنیم. اگر نیاز داشته باشید، می‌توانید فایل style.css را نیز ایجاد کنید.

در زیر، ساختار و محتویات فایل index.html را مشاهده خواهید کرد. اساسا تمام نیاز شما، چندین تگ HTML به همراه یک سری اسکریپت از Socket.io، خواهد بود. فایل index.html شما، به همراه اسکریپت‌های WebSockets، باید چیزی شبیه کد زیر باشد:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Socket.io simple chat</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="container">
      <div class="inbox">
        <div class="inbox__people">
          <h4>Active users</h4>
        </div>
        <div class="inbox__messages">
          <div class="messages__history"></div>
          <div class="fallback"></div>
        </div>
      </div>

      <form class="message_form">
        <input type="text" class="message_form__input" placeholder="Type a message" />
        <button class="message_form__button" type="submit">
          Enter
        </button>
      </form>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
    <script src="main.js"></script>
  </body>
</html>

سپس نیاز است که اتصال را در سمت فرانت‌اند راه‌اندازی کنید. تنها خط کدی که در فایل main.js نیاز دارید، const socket = io() است. در ویدیو زیر این موضوع را مشاهده خواهید کرد:

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

در سوکت، بعد از اتصال کاربر، دو راه برای ایجاد eventها وجود دارد. راه اول از سمت کاربر به سمت دیگران (حتی خود کاربر) است و دیگری event را در سایر instanceها ایجاد می‌کند. ایده در اینجا، نمایش کاربران فعال است. بنابراین هنگامی که یک کاربر متصل می‌شود، آن‌ها باید در رابطه با آن اطلاعاتی کسب و لیست کاربران فعال در آن لحظه را دریافت کنند.

const activeUsers = new Set();

io.on("connection", function (socket) {
  console.log("Made socket connection");

  socket.on("new user", function (data) {
    socket.userId = data;
    activeUsers.add(data);
    io.emit("new user", [...activeUsers]);
  });

  socket.on("disconnect", () => {
    activeUsers.delete(socket.userId);
    io.emit("user disconnected", socket.userId);
  });
});

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

در فایل main.js، دو تابع و دو socket listener ایجاد کرده‌ایم:

const socket = io();

const inboxPeople = document.querySelector(".inbox__people");

let userName = "";

const newUserConnected = (user) => {
  userName = user || `User${Math.floor(Math.random() * 1000000)}`;
  socket.emit("new user", userName);
  addToUsersBox(userName);
};

const addToUsersBox = (userName) => {
  if (!!document.querySelector(`.${userName}-userlist`)) {
    return;
  }

  const userBox = `
    <div class="chat_ib ${userName}-userlist">
      <h5>${userName}</h5>
    </div>
  `;
  inboxPeople.innerHTML += userBox;
};

// new user is created so we generate nickname and emit event
newUserConnected();

socket.on("new user", function (data) {
  data.map((user) => addToUsersBox(user));
});

socket.on("user disconnected", function (userName) {
  document.querySelector(`.${userName}-userlist`).remove();
});

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

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

socket.on("chat message", function (data) {
    io.emit("chat message", data);
});

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

const inputField = document.querySelector(".message_form__input");
const messageForm = document.querySelector(".message_form");
const messageBox = document.querySelector(".messages__history");

const addNewMessage = ({ user, message }) => {
  const time = new Date();
  const formattedTime = time.toLocaleString("en-US", { hour: "numeric", minute: "numeric" });

  const receivedMsg = `
  <div class="incoming__message">
    <div class="received__message">
      <p>${message}</p>
      <div class="message__info">
        <span class="message__author">${user}</span>
        <span class="time_date">${formattedTime}</span>
      </div>
    </div>
  </div>`;

  const myMsg = `
  <div class="outgoing__message">
    <div class="sent__message">
      <p>${message}</p>
      <div class="message__info">
        <span class="time_date">${formattedTime}</span>
      </div>
    </div>
  </div>`;

  messageBox.innerHTML += user === userName ? myMsg : receivedMsg;
};

messageForm.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!inputField.value) {
    return;
  }

  socket.emit("chat message", {
    message: inputField.value,
    nick: userName,
  });

  inputField.value = "";
});

socket.on("chat message", function (data) {
  addNewMessage({ user: data.nick, message: data.message });
});

تابع مسئول نمایش پیام جدید، بلافاصله بعد از دریافت اطلاعات از سوکت است. پیام توسط فرم، به سمت listener موجود در سمت سرور ارسال می‌شود. تنها نیاز است که چک کنید کلاینت نویسنده پیام است یا نه.

ارسال و دریافت پیام در سمت کلاینت

اکنون به شما چگونگی اجرای eventهای مسئول برای سایر اتصالات، به غیر از اتصال خودتان را نمایش خواهیم داد. می‌خواهیم نشان دهیم که در این لحظه، یک کاربر در حال نوشتن چیزی است یا خیر. در سمت سرور، نیاز دارید تا socket.broadcast.emit را اضافه کنید. به کد زیر، که کد نهایی است، نگاهی بیندازید:

const express = require("express");
const socket = require("socket.io");

// App setup
const PORT = 5000;
const app = express();
const server = app.listen(PORT, function () {
  console.log(`Listening on port ${PORT}`);
  console.log(`http://localhost:${PORT}`);
});

// Static files
app.use(express.static("public"));

// Socket setup
const io = socket(server);

const activeUsers = new Set();

io.on("connection", function (socket) {
  console.log("Made socket connection");

  socket.on("new user", function (data) {
    socket.userId = data;
    activeUsers.add(data);
    io.emit("new user", [...activeUsers]);
  });

  socket.on("disconnect", () => {
    activeUsers.delete(socket.userId);
    io.emit("user disconnected", socket.userId);
  });

  socket.on("chat message", function (data) {
    io.emit("chat message", data);
  });
  
  socket.on("typing", function (data) {
    socket.broadcast.emit("typing", data);
  });
});

در سمت کلاینت، باید به event مرتبط به فشردن کلید برای inputField و socket listener برای typing، نگاهی بیندازید. در زیر هر دو مورد را در نسخه نهایی فایل main.js مشاهده می‌کنید:

const socket = io();

const inboxPeople = document.querySelector(".inbox__people");
const inputField = document.querySelector(".message_form__input");
const messageForm = document.querySelector(".message_form");
const messageBox = document.querySelector(".messages__history");
const fallback = document.querySelector(".fallback");

let userName = "";

const newUserConnected = (user) => {
  userName = user || `User${Math.floor(Math.random() * 1000000)}`;
  socket.emit("new user", userName);
  addToUsersBox(userName);
};

const addToUsersBox = (userName) => {
  if (!!document.querySelector(`.${userName}-userlist`)) {
    return;
  }

  const userBox = `
    <div class="chat_ib ${userName}-userlist">
      <h5>${userName}</h5>
    </div>
  `;
  inboxPeople.innerHTML += userBox;
};

const addNewMessage = ({ user, message }) => {
  const time = new Date();
  const formattedTime = time.toLocaleString("en-US", { hour: "numeric", minute: "numeric" });

  const receivedMsg = `
  <div class="incoming__message">
    <div class="received__message">
      <p>${message}</p>
      <div class="message__info">
        <span class="message__author">${user}</span>
        <span class="time_date">${formattedTime}</span>
      </div>
    </div>
  </div>`;

  const myMsg = `
  <div class="outgoing__message">
    <div class="sent__message">
      <p>${message}</p>
      <div class="message__info">
        <span class="time_date">${formattedTime}</span>
      </div>
    </div>
  </div>`;

  messageBox.innerHTML += user === userName ? myMsg : receivedMsg;
};

// new user is created so we generate nickname and emit event
newUserConnected();

messageForm.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!inputField.value) {
    return;
  }

  socket.emit("chat message", {
    message: inputField.value,
    nick: userName,
  });

  inputField.value = "";
});

inputField.addEventListener("keyup", () => {
  socket.emit("typing", {
    isTyping: inputField.value.length > 0,
    nick: userName,
  });
});

socket.on("new user", function (data) {
  data.map((user) => addToUsersBox(user));
});

socket.on("user disconnected", function (userName) {
  document.querySelector(`.${userName}-userlist`).remove();
});

socket.on("chat message", function (data) {
  addNewMessage({ user: data.nick, message: data.message });
});


socket.on("typing", function (data) {
  const { isTyping, nick } = data;

  if (!isTyping) {
    fallback.innerHTML = "";
    return;
  }

  fallback.innerHTML = `<p>${nick} is typing...</p>`;
});

عملکرد این مورد را می‌توانید در ویدیو زیر مشاهده کنید:

جمع‌بندی

امیدواریم این آموزش به شما در فهم سادگی استفاده از Socket.io کمکی کرده باشد، همچنین توسط دانشی پایه‌ای در رابطه با جاوااسکریپت و NodeJS می‌توانید کارهای جذاب زیادی انجام دهید. لیست زیر، برنامه‌هایی هستند که می‌توان توسط WebSockets، آن‌ها را ایجاد کرد:

  • برنامه‌ای برای استریم مسابقات تیم ورزشی محل‌تان. می‌توانید بازی‌ها و یا ویدیوها یا حتی هر دو را استریم کنید.
  • بازی چندنفره برای شما و دوستان‌تان.
  • برنامه چت محرمانه برای گروه‌تان.
  • برنامه‌های براساس GPS، شاید بازی‌هایی که از لوکیشن کاربر استفاده می‌کنند.

به هنگام استفاده از Socket.io، تنها محدودیت، تصورات و ابتکارات شما است.

منبع: https://tsh.io/blog/socket-io-tutorial-real-time-communication