در این مقاله قصد داریم با یکی از معروف ترین کتابخانه های دنیای برنامه نویسی آشنا شویم. نام این کتابخانه Socket.io است. این کتابخانه ارتباط دو طرفه رویداد محور بین کلاینت و سرور را ممکن می سازد.
این کتابخانه با ایجاد یک کانال ارتباطی دوطرفه باعث می شود سرور و کلاینت با هم ارتباطی دو طرفه و سریع داشته باشند.
Socket.io ارتباط دو طرفه رویداد محور را ممکن می سازد. این کتابخانه بر پایه Websocket API ساخته شده است.
Socket.io از دو بخش تشکیل شده است:
سیستم های بلادرنگ بر اساس سوکت ها معماری می شوند و یک کانال ارتباطی دو جهته بین client و Server فراهم می کنند. به این معنی که سرور می تواند پیام ها را به کلاینت ها ارسال کند. هر زمان که یک رویداد رخ می دهد، ایده این است که سرور آن را دریافت کرده و آن را به client های مرتبط ارسال کند.
اولین دستوری که با آشنا خواهید شد. دستور ()io.on
است که اتصال (connection) و disconnection و رویدادها را با استفاده از شی socket مدیریت می کند.
Socket.io کتابخانه ای است که یک ارتباط کم تاخیر، دو طرفه (از سرور به کلاینت و از کلاینت به سرور) و مبتنی بر رویداد را بین یک کلاینت و یک سرور امکان پذیر می کند.
Socket.io بر پایه پروتکل WebSocket ساخته شده است و ضمانت های اضافی مانند HTTP long-polling و اتصال مجدد خودکار (automatic reconnection) را ارائه می دهد.
WebSocket یک پروتکل ارتباطی است که یک کانال دوطرفه و کم تاخیر بین سرور و مرورگر را فراهم می کند. کتابخانه Socket.io یک اتصال TCP باز را به سرور نگه می دارد.
socket ها بر اساس رویدادها کار می کنند. رویدادهای شی socket که در سمت سرور قابل دسترسی هستند:
رویدادهای شی socket در سمت کلاینت در زیر آمده اند:
در ادامه به توضیح بیشتر این مباحث می پردازیم بنابراین نگران نباشید!
کانال ارتباطی دو طرفه که بین سرور و کلاینت وجود دارد با استفاده از Websocket یا HTTP long-polling برقرار می شود. کد اصلی socket.io به دو لایه تقسیم می شود:
Engine.IO مسئول برقراری ارتباط سطح پایین بین سرور و کلاینت است و موارد زیر را مدیریت می کند:
لایه سطح پایین مسئول برقراری اتصال سطح پایین (low-level) میان سرور و کلاینت است. در Socket.io دو نوع انتقال (transport) داریم:
HTTP long-polling شامل درخواست های موفقیت آمیز HTTP است:
انتقال WebSocket از یک اتصال WebSocket تشکیل شده است که یک کانال ارتباطی دو طرفه و با تاخیر کم بین سرور و کلاینت برقرار می کند. با توجه به ماهیت انتقال یا transport هر emit با فریم websocekt مخصوص به خود ارسال می شود.
در نهایت برای یک جمع بندی مختصر ویژگی های اصلی Socket.io را مطرح می کنم:
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 می کند):
توضیح این سه رویداد در زیر آمده است.
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 یا پخش گسترده یعنی فرستادن پیام به همه کلاینت هایی که متصل (connected) هستند.Broadcasting به سه شکل می تواند انجام شود:
اگر بخواهیم پیام را به همه از جمله خودمان بفرستیم از 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 و اتاق (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" });
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 سه رویداد خاص را منتشر می کند:
این رویدادها را در زیر بررسی خواهیم کرد.
رویداد 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 یک کلاس پایه برای برقراری هم کنش و ارتباط با کلاینت است. این کلاس همه متد ها و ویژگی های 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 }); });
برای فرستادن رویدادها بین سرور و کلاینت راه های زیر را داریم:
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 را نشان می دهد.
تابع شنونده را برای رویدادی با نام eventName به انتهای آرایه شنوندگان اضافه می کند.
socket.on("details", (...args) => { // ... });
یک تابع شنونده one-time را برای رویدادی به نام eventName اضافه می کند:
socket.once("details", (...args) => { // ... });
شنونده مشخص شده را از آرایه شنونده رویداد با نام eventName حذف می کند.
const listener = (...args) => { console.log(args); } socket.on("details", listener); // and then later... socket.off("details", listener);
همه شنوندگان یا شنوندگان مشخص شده با eventName را حذف می کند.
// for a specific event socket.removeAllListeners("details"); // for all events socket.removeAllListeners();
گرفتن (catch کردن) همه شنوندگان: از زمان Socket.IO نسخه 3، یک API جدید با الهام از کتابخانه EventEmitter2 اجازه میدهد تا شنوندههای catch-all را تعریف کنید.این ویژگی هم روی کلاینت و هم روی سرور موجود است.
شنوندهای اضافه میکند که هنگام انتشار هر رویدادی فعال میشود.
socket.onAny((eventName, ...args) => { // ... });
شنوندهای اضافه میکند که هنگام انتشار هر رویدادی فعال میشود. شنونده به ابتدای آرایه شنوندگان اضافه می شود.
socket.prependAny((eventName, ...args) => { // ... });
همه شنوندههای catch-all یا شنونده داده شده را حذف میکند.
const listener = (eventName, ...args) => { console.log(eventName, args); } socket.onAny(listener); // and then later... socket.offAny(listener); // or all listeners socket.offAny();
تابع میان افزار تابعی است که برای هر اتصال ورودی اجرا می شود. توابع میان افزار می توانند برای موارد زیر مفید باشند:
توجه: این تابع تنها یک بار در هر اتصال اجرا می شود (حتی اگر اتصال شامل چندین درخواست 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 منتشر نخواهد شد.
Socket.io به شما این امکان را می دهد که سوکت های خود را در "فضای نام" قرار دهید، که در اصل به معنای اختصاص نقاط پایانی یا مسیرهای مختلف است.
فضای نام یک ویژگی مفید برای به حداقل رساندن تعداد منابع (اتصال های TCP) است جداسازی نگرانی ها در برنامه شما با معرفی جداسازی بین کانال های ارتباطی است.
فضاهای نام چندگانه در واقع اتصال WebSockets یکسانی را به اشتراک میگذارند و بنابراین پورتهای سوکت را روی سرور ذخیره میکنند.فضاهای نام در سمت سرور ایجاد می شوند. با این حال، کلاینت ها با ارسال یک درخواست به سرور به آن ها ملحق می شوند.
فضای نام یک کانال ارتباطی است که به شما امکان میدهد منطق برنامه خود را بر روی یک اتصال مشترک تقسیم کنید (که به آن «مالتی پلکس») نیز میگویند.
هر فضای نامی ویژگی های زیر را دارد:
فضای نام ریشه '/' فضای نام پیشفرض است.همه اتصال های به سرور با استفاده از شی 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 کلاینت رویدادهای زیر را در اختیار ما قرار می دهد:
به عنوان مثال - اگر اتصالی داریم که خراب می شود، می توانیم از کد زیر برای اتصال دوباره به سرور استفاده کنیم:
socket.on('connect_failed', function() { document.write("Sorry, there seems to be an issue with the connection!"); })
(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 کردن رویدادها به همه یا زیر مجموعه ای از کلاینت ها است.
هنگام مقیاسگذاری روی چندین سرور 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;
برای ساخت این پروژه به nodejs نیاز داریم.هم چنین به یک ویرایشگر کد مانند Vs Code نیاز داریم. اگر nodejs را نصب نکرده اید می توانید آن را از این نشانی دانلود کنید. هم چنین می توانید Vs Code را از این نشانی دانلود و نصب کنید. البته می توانید از هر ویرایشگر کدی که خواستید استفاده کنید.
نوشتن یک برنامه چت با استک های برنامه نویسی محبوب مانند LAMP(PHP) می تواند بسیار سخت و پیچیده باشد. برنامه های ساخته شده با این پشته ها بسیار کندتر از آن چه باید باشند، هستند. زیرا برای تغییرات باید به طور پیوسته سرور را راه اندازی دوباره بکنند و در حین انجام این کار نیز باید timestamp ها را بررسی بکنند.
سوکت ها راه حلی برای مشکلات گفته شده در بالا هستند. بیش تر سیستم های چت بلادرنگ بر اساس آن ها معماری می شوند.سوکت ها یک کانال ارتباطی دو طرفه بین کلاینت و سرور برقرار می کنند بنابراین دیگر نیاز نیست که سرور به طور مداوم راه اندازی دوباره شود. یعنی سرور می تواند پیام ها را به کلاینت ها بفرستد. هر زمان که یک پیام می نویسیم، سرور آن را دریافت می کند و پیام را به همه کلاینت های متصل می فرستد.
اولین کاری که باید بکنیم ساخت یک صفحه ساده HTML است که یک فرم و لیستی از همه پیام ها را نمایش می دهد. برای ساخت برنامه از وب فریمورک Node.JS استفاده می کنیم. مطمئن شوید که Node.JS نصب شده است.
ابتدا یک پوشه با نام chat-app در هر جایی از سیستم که دوست داریم ایجاد می کنیم. نام آن دلخواه است و این نام در واقع مشخص کننده نام پروژه است. این پوشه را با استفاده از ویرایشگر کد خود باز می کنیم. من از vs code استفاده می کنم.
ترمینال را باز می کنیم و با استفاده از دستور npm init -y پروژه خود را می سازیم.ترمینال را می توان از نوار بالایی vs code باز کرد.
سپس با استفاده از گزینه New Terminal ترمینال را باز می کنیم:
دستور npm init -y را در ترمینال می نویسیم و سپس Enter را می زنیم.پس از این کار باید تصویر زیر را ببینیم:
پس از این کار یک فایل package.json ساخته می شود. فایل package.json قلب هر پروژه Node است. این فایل ویژگی های عملکردی پروژه را تعریف می کند و npm برای نصب وابستگی ها، اجرای اسکریپت ها و شناسایی فایل اصلی برنامه یا همان entry point از آن استفاده می کند.
اگر این فایل را باز کنیم تصویر زیر را خواهیم دید:
برای اطلاع بیش تر در مورد این فایل به این نشانی مراجعه کنید.در ادامه برای ساخت برنامه باید package های زیر را نصب کنیم:
با دستور 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/ می رویم. با این کار باید تصویر زیر را ببینیم:
در 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 به طور خودکار به کلاینت سرویس می دهد.
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» را در کنسول ببینیم:
هدف اصلی از ساخت 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 را می زنیم:
پس از این کار اگر ترمینال را نگاه کنیم باید تصویر زیر را ببینیم:
هدف بعدی در این جا این است که یک رویداد را از سرور به همه کاربران بفرستیم.برای فرستادن یک رویداد به همه، 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
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.