ساخت سرور چت TCP با NodeJs

در این مقاله یک سرور TCP با کمک node خواهیم ساخت. قدم بعدی را با ساخت یک سرور چت ساده پیش می‌بریم که اتصالات بسیاری که کلاینت‌ها را قادر می‌سازد تا با یکدیگر ارتباط برقرار کنند را مدیریت می‌کند.

ساخت سرور چت TCP با NodeJs

TCP

قبل از شروع به کار بیایید در مورد TCP و مزایای آن صحبت کنیم. TCP (پروتکل کنترل انتقال) یکی از پروتکل‌های اصلی اینترنت است، در لایه حمل و نقل مدل TCP/IP وجود دارد، TCP بالای لایه شبکه قرار می‌گیرد و یک مکانیزم حمل و نقل برای پروتکل‌های لایه کاربرد مثل HTTP، SMTP، IRC و غیره فراهم می‌کند، TCP به دلیل ویژگی‌های زیر یک مکانیزم حمل و نقل گسترده است:

در صورتی که به یادگیری اصولی و حرفه ای این تکنولوژی قدرتمند علاقمند هستید میتوانید دوره کامل و جامع آموزش Node Js موجود در سایت تاپ لرن را مشاهده کنید .

1. قابل اطمینان: بیت‌ها به ترتیبی که فرستاده شده‌اند منتقل می‌شوند. همچنین مکانیزم‌هایی مثل تأییدیه‌ها هنگامی که بیت‌ها تحویل داده شده‌اند و handshakeها را ترکیب می‌کند.

2. اتصال‌گرا: یک راه‌اندازی بین فرآیندهای سرور و کلاینت مورد نیاز است.

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

4. کنترل ازدحام: وقتی که شبکه شلوغ است، راه فرستنده بسته می‌شود.

TCP Server

حالا که درک بهتری از TCP و مزایای آن داریم، بیایید TCP سرور خودمان را با node بسازیم. خوشبختانه node این امکان را از طریق یک ماژول هسته به نام net ایجاد می‌کند.

//Require the 'net' module
const net = require('net);

//Store the 'net.Server' object returned by 'net.createServer()'
const server = net.createServer();

//Add event listener for connection events
server.on('connection', connection => { });

//Add event listener for close events
server.on('close' () => { 
    console.log(`Server disconnected`);
});

//Add listener for error events
server.on('error', error => { 
    console.log(`Error : ${error}`);
});

//Listen for connections on port 4000
server.listen(4000);

net.createServer یک شیء net.Server که از کلاس Event emitter ارث‌بری می‌کند را برمی‌گرداند، امکان انتشار رویدادهایی مثل close، error، connection و غیره را ایجاد می‌کند.

وقتی کلاینت به سرور متصل می‌شود، رویداد connection منتشر می‌شود و شیء net.Socket به عنوان یکی از آرگومان‌ها در callback تابعی که نشان‌دهنده کلاینتی است که فقط متصل است پاس داده می‌شود. net.socket یک جریان دوتایی است (می‌توانیم از آن بخوانیم و در آن بنویسیم) و همچنین از کلاس Event emitter ارث‌بری کرده است.

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

Telnet

Telnet یک پروتکل بسیار قدیمی است، اگرچه امروزه به طور عمده توسط SSH جایگزین شده است، هنوز هم همراه با هر سیستم عامل اصلی می‌آید و یک ترمینال مجازی و بسیاری از ویژگی‌ها را فراهم می‌کند. Telnet یک لایه بالاتر از پروتکل TCP قرار می‌گیرد، بنابراین به جای ایجاد یک کلاینت TCP، ما از آن برای برنامه چت خود استفاده می‌کنیم تا چیزها تا حد ممکن ساده باشند. حالا بیاید آن را بسازیم.

سرور چت

ما قبلا یک سرور TCP ایجاد کردیم، که اساسا همه آن چیزی است که انجام می‌شود، مهم‌ترین مواردی که در حال حاضر اجرا می‌شوند عملکردهای مربوط به اتصال کلاینت‌ها از طریق telnet هستند. بنابراین اولین عملکردی که ما می‌خواهیم این است که وقتی اتصالی رخ می‌دهد کلاینت نام کاربری را وارد کند. ما باید کلیدهای ارسال شده توسط کلاینت را بگیریم، این امر با افزودن یک شنونده رویداد data بر روی شیء connection ما امکان‌پذیر است. این رویداد data هر زمان که کاربر کلیدی را فشار دهد رخ می‌دهد. همچنین باید هر اتصال فعال در یک شیء را ذخیره کنیم.

نکته: در سیستم عامل لینوکس که در حال اجرای telnet است، رویداد data تنها زمانی صادر می‌شود که کلید enter فشرده شود اما در سایر سیستم عامل‌ها، رویداد data هر زمان که کلاینت هر کلیدی را فشار دهد انتشار می‌یابد. بنابراین نیاز به شیوه‌ای داریم تا آن را توسط گرفتن کلید ورودی، تنها زمانی که کلاینت کلید enter را فشار می‌دهد، که توسط کاراکتر ‘\r\n’ نشان داده می‌شود، اصلاح کنیم. همچنین آن‌ها را وقتی کاررا با آن انجام می دهیم، کنار می‌گذاریم.

 ....

//All active connections are stored in this object together with their client name
let clients = {}
//Stores the number of active clients
let clientCount = 0;

server.on('connection', connection => {
  let clientname
  //Keep track of keystrokes
  let message = [];

  //Remember `net.Socket` is a dublex stream, so we can write into it
  connection.write(`Please enter a room name\r\n`);

  //data is sent in buffers so we want to use the utf-8 character encoding to make it a readable format
  connection.setEncoding('utf-8');

  //Listen for input from the client
  connection.on('data', data => {
    //Push every keystroke into an array
    message.push(data);

    //Proceed only if the 'enter' key has been pressed
    if( data == '\r\n'){

      //Join the keystrokes stored and remove the 'enter' character
      let clientInput = message.join('').replace('\r\n','');

      //Proceed if client name does not exist
      if(!clientname){
        //Check whether client name is already taken
        if( clients[clientInput] ){
          connection.write(' - Name is taken, try another name\r\n')
          //Discard the previous keystrokes the client entered
          message = [];
          return;
        } else {
          //Store the client name
          clientname = clientInput;
          //Increase the number of active clients
          clientCount++;
          //Store the connections by the name entered
          clients[clientInput] = connection;
          //Welcome the client
          connection.write(` - Welcome to the Chatbox, There are ${clientCount} active users\r\n`);
          //Discard the previous keystrokes the client entered
          message = [];
        }
      } else { 
        //We'll get back here
      }
    }

  })

})

  ....

بنابراین در حال حاضر ما قادر به اجرای چند عملکرد هستیم:

ارسال پیام زمانی که کلاینت کلید enter را فشار می‌دهد

نگه داشتن مسیر همه کلاینت‌های فعال

اطمینان از اینکه هر کلاینت یک نام منحصربه‌فرد دارد

بنابراین آنچه در حال حاضر باقی می‌ماند این است که این عملکردهای باقی‌مانده را اجرا کنیم:

پخش/ارسال پیام به همه کلاینت‌های فعال دیگر

حذف اتصال شیء، وقتی اتصال کلاینت قطع می‌شود

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

 ...

//Helper function that helps us send a message to every other client connected
//We'll see how this works when we put everything together
function broadcast( msg ){
  //Loop through the active clients object
  for( let user in clients ){
    //Send message to all active clients except yourself 
    if( clients[ user ] !== connection ){
      clients[ user ].write(msg);
    }
  }
}

//A close event is emitted when a connection has disconnected from the server
connection.on('close', () => {
    //When a client disconnects, remove the name and connection
    delete clients[clientname];
    //Decrease the active client count
    clientCount--;
    //Send a message to every active client that someone just left the chat
})

//Handle error events
connection.on('error', error => {
  connection.write(`Error : ${error}`);
}

  ...

بنابراین حالا که توانسته‌ایم تمام عملکردهای مورد نیاز سرور چت خود و نحوه مدیریت اتصالات را پیاده‌سازی کنیم، بیاید تمام این‌ موارد را در کنار هم قرار داده و برنامه‌یمان را تست کنیم.

const net = require('net);

const server = net.createServer();

//All active connections are stored in this object together with their client name
let clients = {}
//Stores the number of active clients
let clientCount = 0;

server.on('Connection', Connection => {
  let clientname;
  let message = [];

  //Helper function that helps us send a message to every other client connected  
  function broadcast( msg ){
    //Loop through the active clients object
    for( let client in clients ){
      //Send message to all active clients except yourself 
      if( clients[client] !== connection ){
        clients[client].write(msg);
      }
    }
  }

  connection.write(`Please enter a room name\r\n`);

  connection.setEncoding('utf-8');

  connection.on('data', data => {
    //Push every keystroke into an array
    message.push(data);

    //Proceed only if the 'enter' key has been pressed
    if( data == '\r\n'){

      //Join the keystrokes stored and remove the 'enter' character
      let clientInput = message.join('').replace('\r\n','');

      //Proceed if client name does not exist
      if(!clientname){
        //Check whether client name is already taken
        if( clients[clientInput] ){
          connection.write(' - Name is taken, try another name\r\n')
          //Discard of the previous keystrokes the client entered
          message = [];
          return;
        } else {
          //Store the client name
          clientname = clientInput;
          //Increase the number of active clients
          clientCount++;
          //Store the connections by client name
          clients[clientInput] = connection;
          //Welcome the client
          connection.write(` - Welcome to the Chatbox, There are ${clientCount} active users\r\n`);
          //Send a message to every active client that someone just joined the room 
          broadcast(` - ${clientname} has joined the room\r\n`);          
          //Discard the previous keystrokes the client entered
          message = [];
        }
      } else { 
        //Send the message received to every client
        broadcast(` > ${clientname} : ${clientInput}\r\n`);
        //Discard the previous keystrokes the client entered
        message = [];
      }
    }
  })

  //A close event is emmited when a connection is disconnected from the server
  connection.on('close', () => {
    //When a client disconnecs, remove the name and connection
    delete clients[clientname];
    //Decrease the active client count
    clientCount--;
    //Send a message to every active client that someone just left the room
    broadcast(` - ${clientname} has left the room\r\n Active Users : ${clientCount}\r\n`);
  })

  //Handle error events
  connection.on('error', error => {
    connection.write(`Error : ${error}`);
  }

});

server.on('close' () => { 
  console.log(`Server disconnected`)
});

server.on('error', error => { 
  console.log(`Error : ${error}`);
});

server.listen(4000);

اکنون که ما همه چیز را با هم داریم، بیایید پیش رویم و آن را امتحان کنیم.

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

 $ telnet localhost 4000

نام و بقیه موارد از شما خواسته می شود و شما قادر خواهید بود تا پیام‌ها را به کلاینت‌های فعال دیگر ارسال کنید، بنابراین ادامه دهید و چند ترمینال را باز کنید و به سرور TCP خود متصل شوید، ویژگی‌های چت خود را تست کنید، این برای ماست:

نتیجه‌گیری

ما توانستیم از net ماژول هسته node برای ساخت سرور TCP استفاده کنیم و متعاقبا یک سرور چت با telnet به عنوان کلاینت TCP خود بسازیم، ماژول net دارای ویژگی‌های بسیاری است، از جمله اینکه شما می‌توانید از آن برای ساخت برنامه کلاینت IRC استفاده کنید، و همچنین موارد دیگر. به هر حال ما یک سرور TCP که اتصالات بسیاری را مدیریت می‌کند و برای کلاینت‌ها سودمند است تا در یک فرم با یکدیگر ارتباط برقرار کنند، ساختیم، که این مسأله درک بهتری از نحوه استفاده از ماژول net به ما می‌دهد.