Not so long ago, if we wanted to build a simple JavaScript chat we had
to work with Ajax, Comet or Flash component in the browser.
Ajax can be a little bad for performance, (in the case of chatting)
because it’s based on a request and response. We also needed to hold an
interval in the client which asked the server for new messages and this process
was repeated again and again.
Comet (also known as reverse Ajax and long polling) is pretty much the
same. While using Ajax, we asked the server every time for messages, with Comet
we needed to open one connection and wait. This connection remained open in the
server for a while until the server received messages for us, then the server
responded with the messages and closed the opened connection. The client, when
receiving these messages opened a new connection and waited again. One
advantage of Comet is the reduction of the request amounts, but still, we
needed to open and close many of them for each session (in case the user is
active).
And lastly, a Flash component - in this case we placed a small flash
component in our page to exchange messages with the JavaScript client, the
flash could open a socket to the server and this gave us push communication.
However, we are here to talk about Node.js and Socket.IO, so, basically,
the flash component is a good solution but in those days not every device or
browser came with flash support.
Node.js and Socket.IO
With node.js and socket.io we can enjoy better performance, a
bidirectional push communication between a server and a JavaScript client and
even the support for all the browsers. Socket.io can work with several
transports in order to support even old browsers like IE6 (RiP(.
In this tutorial, I'll show you how to create a multi chat room server
with node.js and socket.io and a basic jquery client.
Installing Socket.IO and Express
If you don’t have socket.io and/or expressjs node modules installed on your machine,
this will be a great time to do it, you can install them with npm:
$ npm install expressand...
$ npm install socket.io
Now we are ready to go, let's start with the server...
server.js
// creating global parameters and start // listening to 'port', we are creating an express // server and then we are binding it with socket.io var express = require('express'), app = express(), server = require('http').createServer(app), io = require('socket.io').listen(server), port = 8080, // hash object to save clients data, // { socketid: { clientid, nickname }, socketid: { ... } } chatClients = new Object(); // listening to port... server.listen(port); // configure express, since this server is // also a web server, we need to define the // paths to the static files app.use("/styles", express.static(__dirname + '/public/styles')); app.use("/scripts", express.static(__dirname + '/public/scripts')); app.use("/images", express.static(__dirname + '/public/images')); // serving the main applicaion file (index.html) // when a client makes a request to the app root // (http://localhost:8080/) app.get('/', function (req, res) { res.sendfile(__dirname + '/public/index.html'); }); // sets the log level of socket.io, with // log level 2 we wont see all the heartbits // of each socket but only the handshakes and // disconnections io.set('log level', 2); // setting the transports by order, if some client // is not supporting 'websockets' then the server will // revert to 'xhr-polling' (like Comet/Long polling). // for more configurations go to: // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO io.set('transports', [ 'websocket', 'xhr-polling' ]); // socket.io events, each connection goes through here // and each event is emited in the client. // I created a function to handle each event io.sockets.on('connection', function(socket){ // after connection, the client sends us the // nickname through the connect event socket.on('connect', function(data){ connect(socket, data); }); // when a client sends a messgae, he emits // this event, then the server forwards the // message to other clients in the same room socket.on('chatmessage', function(data){ chatmessage(socket, data); }); // client subscribtion to a room socket.on('subscribe', function(data){ subscribe(socket, data); }); // client unsubscribtion from a room socket.on('unsubscribe', function(data){ unsubscribe(socket, data); }); // when a client calls the 'socket.close()' // function or closes the browser, this event // is built in socket.io so we actually dont // need to fire it manually socket.on('disconnect', function(){ disconnect(socket); }); }); // create a client for the socket function connect(socket, data){ //generate clientId data.clientId = generateId(); // save the client to the hash object for // quick access, we can save this data on // the socket with 'socket.set(key, value)' // but the only way to pull it back will be // async chatClients[socket.id] = data; // now the client objtec is ready, update // the client socket.emit('ready', { clientId: data.clientId }); // auto subscribe the client to the 'lobby' subscribe(socket, { room: 'lobby' }); // sends a list of all active rooms in the // server socket.emit('roomslist', { rooms: getRooms() }); } // when a client disconnect, unsubscribe him from // the rooms he subscribed to function disconnect(socket){ // get a list of rooms for the client var rooms = io.sockets.manager.roomClients[socket.id]; // unsubscribe from the rooms for(var room in rooms){ if(room && rooms[room]){ unsubscribe(socket, { room: room.replace('/','') }); } } // client was unsubscribed from the rooms, // now we can delete him from the hash object delete chatClients[socket.id]; } // receive chat message from a client and // send it to the relevant room function chatmessage(socket, data){ // by using 'socket.broadcast' we can send/emit // a message/event to all other clients except // the sender himself socket.broadcast.to(data.room).emit('chatmessage', { client: chatClients[socket.id], message: data.message, room: data.room }); } // subscribe a client to a room function subscribe(socket, data){ // get a list of all active rooms var rooms = getRooms(); // check if this room is exist, if not, update all // other clients about this new room if(rooms.indexOf('/' + data.room) < 0){ socket.broadcast.emit('addroom', { room: data.room }); } // subscribe the client to the room socket.join(data.room); // update all other clients about the online // presence updatePresence(data.room, socket, 'online'); // send to the client a list of all subscribed clients // in this room socket.emit('roomclients', { room: data.room, clients: getClientsInRoom(socket.id, data.room) }); } // unsubscribe a client from a room, this can be // occured when a client disconnected from the server // or he subscribed to another room function unsubscribe(socket, data){ // update all other clients about the offline // presence updatePresence(data.room, socket, 'offline'); // remove the client from socket.io room socket.leave(data.room); // if this client was the only one in that room // we are updating all clients about that the // room is destroyed if(!countClientsInRoom(data.room)){ // with 'io.sockets' we can contact all the // clients that connected to the server io.sockets.emit('removeroom', { room: data.room }); } } // 'io.sockets.manager.rooms' is an object that holds // the active room names as a key, returning array of // room names function getRooms(){ return Object.keys(io.sockets.manager.rooms); } // get array of clients in a room function getClientsInRoom(socketId, room){ // get array of socket ids in this room var socketIds = io.sockets.manager.rooms['/' + room]; var clients = []; if(socketIds && socketIds.length > 0){ socketsCount = socketIds.lenght; // push every client to the result array for(var i = 0, len = socketIds.length; i < len; i++){ // check if the socket is not the requesting // socket if(socketIds[i] != socketId){ clients.push(chatClients[socketIds[i]]); } } } return clients; } // get the amount of clients in aroom function countClientsInRoom(room){ // 'io.sockets.manager.rooms' is an object that holds // the active room names as a key and an array of // all subscribed client socket ids if(io.sockets.manager.rooms['/' + room]){ return io.sockets.manager.rooms['/' + room].length; } return 0; } // updating all other clients when a client goes // online or offline. function updatePresence(room, socket, state){ // socket.io may add a trailing '/' to the // room name so we are clearing it room = room.replace('/',''); // by using 'socket.broadcast' we can send/emit // a message/event to all other clients except // the sender himself socket.broadcast.to(room).emit('presence', { client: chatClients[socket.id], state: state, room: room }); } // unique id generator function generateId(){ var S4 = function () { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); }; return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4()); } // show a message in console console.log('Chat server is running and listening to port %d...', port);
Don't be afraid of this long code, actually it's not so long if you removes the comment lines :), However, I won't explain it because it's well documented so if you missed something feel free to comment to this post and I will explain...
Now let's see the client side, I will show here only the JavaScript part but if you want to download the full application, see the references at the bottom for a github link and a live demo url.
chat.io.js
(function($){ // create global app parameters... var NICK_MAX_LENGTH = 15, ROOM_MAX_LENGTH = 10, lockShakeAnimation = false, socket = null, clientId = null, nickname = null, // holds the current room we are in currentRoom = null, // server information serverAddress = 'localhost', serverDisplayName = 'Server', serverDisplayColor = '#1c5380', // some templates we going to use in the chat, // like message row, client and room, this // templates will be rendered with jQuery.tmpl tmplt = { room: [ '<li data-roomId="${room}">', '<span class="icon"></span> ${room}', '</li>' ].join(""), client: [ '<li data-clientId="${clientId}" class="cf">', '<div class="fl clientName">', '<span class="icon"></span>', '${nickname}', '</div>', '<div class="fr composing"></div>', '</li>' ].join(""), message: [ '<li class="cf">', '<div class="fl sender">${sender}: </div>', '<div class="fl text">${text}</div>', '<div class="fr time">${time}</div>', '</li>' ].join("") }; // bind DOM elements like button clicks and keydown function bindDOMEvents(){ $('.chat-input input').on('keydown', function(e){ var key = e.which || e.keyCode; if(key == 13) { handleMessage(); } }); $('.chat-submit button').on('click', function(){ handleMessage(); }); $('#nickname-popup .input input').on('keydown', function(e){ var key = e.which || e.keyCode; if(key == 13) { handleNickname(); } }); $('#nickname-popup .begin').on('click', function(){ handleNickname(); }); $('#addroom-popup .input input').on('keydown', function(e){ var key = e.which || e.keyCode; if(key == 13) { createRoom(); } }); $('#addroom-popup .create').on('click', function(){ createRoom(); }); $('.big-button-green.start').on('click', function(){ $('#nickname-popup .input input').val(''); Avgrund.show('#nickname-popup'); window.setTimeout(function(){ $('#nickname-popup .input input').focus(); },100); }); $('.chat-rooms .title-button').on('click', function(){ $('#addroom-popup .input input').val(''); Avgrund.show('#addroom-popup'); window.setTimeout(function(){ $('#addroom-popup .input input').focus(); },100); }); $('.chat-rooms ul').on('scroll', function(){ $('.chat-rooms ul li.selected').css('top', $(this).scrollTop()); }); $('.chat-messages').on('scroll', function(){ var self = this; window.setTimeout(function(){ if($(self).scrollTop() + $(self).height() < $(self).find('ul').height()){ $(self).addClass('scroll'); } else { $(self).removeClass('scroll'); } }, 50); }); $('.chat-rooms ul li').live('click', function(){ var room = $(this).attr('data-roomId'); if(room != currentRoom){ socket.emit('unsubscribe', { room: currentRoom }); socket.emit('subscribe', { room: room }); } }); } // bind socket.io event handlers // this events fired in the server function bindSocketEvents(){ // when the connection is made, the server emiting // the 'connect' event socket.on('connect', function(){ // firing back the connect event to the server // and sending the nickname for the connected client socket.emit('connect', { nickname: nickname }); }); // after the server created a client for us, the ready event // is fired in the server with our clientId, now we can start socket.on('ready', function(data){ // hiding the 'connecting...' message $('.chat-shadow').animate({ 'opacity': 0 }, 200, function(){ $(this).hide(); $('.chat input').focus(); }); // saving the clientId localy clientId = data.clientId; }); // after the initialize, the server sends a list of // all the active rooms socket.on('roomslist', function(data){ for(var i = 0, len = data.rooms.length; i < len; i++){ // in socket.io, their is always one default room // without a name (empty string), every socket is // automaticaly joined to this room, however, we // don't want this room to be displayed in the // rooms list if(data.rooms[i] != ''){ addRoom(data.rooms[i], false); } } }); // when someone sends a message, the sever push it to // our client through this event with a relevant data socket.on('chatmessage', function(data){ var nickname = data.client.nickname; var message = data.message; //display the message in the chat window insertMessage(nickname, message, true, false, false); }); // when we subscribes to a room, the server sends a list // with the clients in this room socket.on('roomclients', function(data){ // add the room name to the rooms list addRoom(data.room, false); // set the current room setCurrentRoom(data.room); // announce a welcome message insertMessage(serverDisplayName, 'Welcome to the room: `' + data.room + '`... enjoy!', true, false, true); $('.chat-clients ul').empty(); // add the clients to the clients list addClient({ nickname: nickname, clientId: clientId }, false, true); for(var i = 0, len = data.clients.length; i < len; i++){ if(data.clients[i]){ addClient(data.clients[i], false); } } // hide connecting to room message message $('.chat-shadow').animate({ 'opacity': 0 }, 200, function(){ $(this).hide(); $('.chat input').focus(); }); }); // if someone creates a room the server updates us // about it socket.on('addroom', function(data){ addRoom(data.room, true); }); // if one of the room is empty from clients, the server, // destroys it and updates us socket.on('removeroom', function(data){ removeRoom(data.room, true); }); // with this event the server tells us when a client // is connected or disconnected to the current room socket.on('presence', function(data){ if(data.state == 'online'){ addClient(data.client, true); } else if(data.state == 'offline'){ removeClient(data.client, true); } }); } // add a room to the rooms list, socket.io may add // a trailing '/' to the name so we are clearing it function addRoom(name, announce){ // clear the trailing '/' name = name.replace('/',''); // check if the room is not already in the list if($('.chat-rooms ul li[data-roomId="' + name + '"]').length == 0){ $.tmpl(tmplt.room, { room: name }) .appendTo('.chat-rooms ul'); // if announce is true, show a message about this room if(announce){ insertMessage(serverDisplayName, 'The room `' + name + '` created...', true, false, true); } } } // remove a room from the rooms list function removeRoom(name, announce){ $('.chat-rooms ul li[data-roomId="' + name + '"]').remove(); // if announce is true, show a message about this room if(announce){ insertMessage(serverDisplayName, 'The room `' + name + '` destroyed...', true, false, true); } } // add a client to the clients list function addClient(client, announce, isMe){ var $html = $.tmpl(tmplt.client, client); // if this is our client, mark him with color if(isMe){ $html.addClass('me'); } // if announce is true, show a message about this client if(announce){ insertMessage(serverDisplayName, client.nickname + ' has joined the room...', true, false, true); } $html.appendTo('.chat-clients ul') } // remove a client from the clients list function removeClient(client, announce){ $('.chat-clients ul li[data-clientId="' + client.clientId + '"]').remove(); // if announce is true, show a message about this room if(announce){ insertMessage(serverDisplayName, client.nickname + ' has left the room...', true, false, true); } } // every client can create a new room, when creating one, the client // is unsubscribed from the current room and then subscribed to the // room he just created, if he trying to create a room with the same // name like another room, then the server will subscribe the user // to the existing room function createRoom(){ var room = $('#addroom-popup .input input').val().trim(); if(room && room.length <= ROOM_MAX_LENGTH && room != currentRoom){ // show room creating message $('.chat-shadow').show().find('.content') .html('Creating room: ' + room + '...'); $('.chat-shadow').animate({ 'opacity': 1 }, 200); // unsubscribe from the current room socket.emit('unsubscribe', { room: currentRoom }); // create and subscribe to the new room socket.emit('subscribe', { room: room }); Avgrund.hide(); } else { shake('#addroom-popup', '#addroom-popup .input input', 'shake', 'yellow'); $('#addroom-popup .input input').val(''); } } // sets the current room when the client // makes a subscription function setCurrentRoom(room){ currentRoom = room; $('.chat-rooms ul li.selected').removeClass('selected'); $('.chat-rooms ul li[data-roomId="' + room + '"]') .addClass('selected'); } // save the client nickname and start the chat by // calling the 'connect()' function function handleNickname(){ var nick = $('#nickname-popup .input input').val().trim(); if(nick && nick.length <= NICK_MAX_LENGTH){ nickname = nick; Avgrund.hide(); connect(); } else { shake('#nickname-popup', '#nickname-popup .input input', 'shake', 'yellow'); $('#nickname-popup .input input').val(''); } } // handle the client messages function handleMessage(){ var message = $('.chat-input input').val().trim(); if(message){ // send the message to the server with the room name socket.emit('chatmessage', { message: message, room: currentRoom }); // display the message in the chat window insertMessage(nickname, message, true, true); $('.chat-input input').val(''); } else { shake('.chat', '.chat input', 'wobble', 'yellow'); } } // insert a message to the chat window, this function can be // called with some flags function insertMessage(sender, message, showTime, isMe, isServer){ var $html = $.tmpl(tmplt.message, { sender: sender, text: message, time: showTime ? getTime() : '' }); // if isMe is true, mark this message so we can // know that this is our message in the chat window if(isMe){ $html.addClass('marker'); } // if isServer is true, mark this message as a server // message if(isServer){ $html.find('.sender') .css('color', serverDisplayColor); } $html.appendTo('.chat-messages ul'); $('.chat-messages').animate({ scrollTop: $('.chat-messages ul').height() }, 100); } // return a short time format for the messages function getTime(){ var date = new Date(); return (date.getHours() < 10 ? '0' + date.getHours().toString() : date.getHours()) + ':' + (date.getMinutes() < 10 ? '0' + date.getMinutes().toString() : date.getMinutes()); } // just for animation function shake(container, input, effect, bgColor){ if(!lockShakeAnimation){ lockShakeAnimation = true; $(container).addClass(effect); $(input).addClass(bgColor); window.setTimeout(function(){ $(container).removeClass(effect); $(input).removeClass(bgColor); $(input).focus(); lockShakeAnimation = false; }, 1500); } } // after selecting a nickname we call this function // in order to init the connection with the server function connect(){ // show connecting message $('.chat-shadow .content').html('Connecting...'); // creating the connection and saving the socket socket = io.connect(serverAddress); // now that we have the socket we can bind events to it bindSocketEvents(); } // on document ready, bind the DOM elements to events $(function(){ bindDOMEvents(); }); })(jQuery);
Summary
There are some ways to send a message from the server to the client with Socket.io, Let's explore some of them:
- socket.emit - Fire event to the client side which associated to this socket.
socket.emit('eventName', { data: 'tosend' });
- socket.broadcast.emit - Like the socket.emit, firing an event to the client but instead of firing the event to the associated socket, the event will be fired in all other connected socket except from the broadcasting one.
socket.broadcast.emit('eventName', { data: 'tosend'});
- socket.broadcast.to('roomName').emit - Sometimes we want to broadcast from one socket to all another socket in the same room rather than all the sockets in the server, in this way we can broadcast to all other sockets who joined a specific room
socket.broadcast.to('roomName').emit('eventName', {data: 'tosend'});
- io.sockets.emit - In this way we can fire an event to all the clients that connected to the server.
io.sockets.emit('eventName', { data: 'tosend'});
- io.of('/namespaceName').emit - Like rooms, in socket.io we can create namespaces, if you want to emit an event to all the clients that connected to a namespace, this is the way.
io.of('/namespaceName').emit('eventName', { data: 'tosend'});
References
salam kenal bos. lagi jalan jalan pagi nih
ReplyDeleteThanks and I have a dandy provide: How Much House Renovation Cost Philippines house renovation jobs
ReplyDeleteHi thanks ffor posting this
ReplyDelete