Socket.io: How to correctly join and leave rooms - node.js

I'm trying to learn Socket.io by building a set of dynamically created chatrooms that emit 'connected' and 'disconnected' messages when users enter and leave. After looking at a couple of questions I've put together something functional but most of the response linked are from people who admit they've hacked together answers and I've noticed there's a more general - and recent - discussion about the right way to do this on the Socket.io repo (notably here and here)
As I'm such a novice I don't know if the work below is an acceptable way to do things or it just happens to incidentally function but will cause performance issues or result in too many listeners. If there's an ideal - and official - way to join and leave rooms that feels less clunky than this I'd love to learn about it.
Client
var roomId = ChatRoomData._id // comes from a factory
function init() {
// Make sure the Socket is connected
if (!Socket.socket) {
Socket.connect();
}
// Sends roomId to server
Socket.on('connect', function() {
Socket.emit('room', roomId);
});
// Remove the event listener when the controller instance is destroyed
$scope.$on('$destroy', function () {
Socket.removeListener('connect');
});
}
init();
Server
io.sockets.once('connection', function(socket){
socket.on('room', function(room){ // take room variable from client side
socket.join(room) // and join it
io.sockets.in(room).emit('message', { // Emits a status message to the connect room when a socket client is connected
type: 'status',
text: 'Is now connected',
created: Date.now(),
username: socket.request.user.username
});
socket.on('disconnect', function () { // Emits a status message to the connected room when a socket client is disconnected
io.sockets.in(room).emit({
type: 'status',
text: 'disconnected',
created: Date.now(),
username: socket.request.user.username
});
})
});

Socket.IO : recently released v2.0.3
Regarding joining / leaving rooms [read the docs.]
To join a room is as simple as socket.join('roomName')
//:JOIN:Client Supplied Room
socket.on('subscribe',function(room){
try{
console.log('[socket]','join room :',room)
socket.join(room);
socket.to(room).emit('user joined', socket.id);
}catch(e){
console.log('[error]','join room :',e);
socket.emit('error','couldnt perform requested action');
}
})
and to leave a room, simple as socket.leave('roomName'); :
//:LEAVE:Client Supplied Room
socket.on('unsubscribe',function(room){
try{
console.log('[socket]','leave room :', room);
socket.leave(room);
socket.to(room).emit('user left', socket.id);
}catch(e){
console.log('[error]','leave room :', e);
socket.emit('error','couldnt perform requested action');
}
})
Informing the room that a room user is disconnecting
Not able to get the list of rooms the client is currently in on disconnect event
Has been fixed (Add a 'disconnecting' event to access to socket.rooms upon disconnection)
socket.on('disconnect', function(){(
/*
socket.rooms is empty here
leaveAll() has already been called
*/
});
socket.on('disconnecting', function(){
// socket.rooms should isn't empty here
var rooms = socket.rooms.slice();
/*
here you can iterate over the rooms and emit to each
of those rooms where the disconnecting user was.
*/
});
Now to send to a specific room :
// sending to all clients in 'roomName' room except sender
socket.to('roomName').emit('event', 'content');
Socket.IO Emit Cheatsheet

This is how I inform users of a "disconnecting user"
socket.on('disconnecting', function(){
console.log("disconnecting.. ", socket.id)
notifyFriendOfDisconnect(socket)
});
function notifyFriendOfDisconnect(socket){
var rooms = Object.keys(socket.rooms);
rooms.forEach(function(room){
socket.to(room).emit('connection left', socket.id + ' has left');
});
}

Here is a working method.
I am using socketio "socket.io": "^2.3.0" on server side. On Client side is android
// SocketIO
implementation('io.socket:socket.io-client:1.0.0') {
// excluding org.json which is provided by Android
exclude group: 'org.json', module: 'json'
}
Following is the code that is working for me.
// Join Chat Room
socket.on('ic_join', function(data) {
// Json Parse String To Access Child Elements
let messageJson = JSON.parse(data)
let room1 = messageJson.room1
console.log('======Joined Room========== ')
console.log(room1)
socket.join(room1, function(err) {
console.log(io.sockets.adapter.rooms[room1].length);
console.log(err)
})
})
// Leave Chat Room
socket.on('ic_leave', function(data) {
// Json Parse String To Access Child Elements
let messageJson = JSON.parse(data)
let room1 = messageJson.room1
console.log('======Left Room========== ')
console.log(room1)
socket.leave(room1, function(err) {
if (typeof io.sockets.adapter.rooms[room1] !== 'undefined' && io.sockets.adapter.rooms[room1] != null) {
console.log(io.sockets.adapter.rooms[room1].length);
console.log(err)
} else{
console.log("room is deleted")
}
})
})

For anyone reading this beyond 2/1/2021, using socket.io 3.1.0 and hopefully later, you can reference my example. I have found that the example on how to do this in socket.io's documentation is incorrect. They claim that in the disconnect event that socket.rooms is an object. While is uses the block container and is comma separated, there are no key pairs, meaning their demonstration of const rooms = Object.keys(socket.rooms) returns an empty value. It's creating an array out of an object that is really just an array. To my knowledge {} can only be used for block statements and objects. By some quirk, NodeJS is treating it like a normal array. I assign custom, 4 digit rooms on each connect event. So I have on the disconnect event I have the server skim through all the rooms, and if it encounters a room name with a length of 4, it tells everyone in the room that the size of the room decreased by one. On the client side, I have a socket event listener monitoring for this and when it's detected, updates a innerHTML property so that the clients can display the number of connected users.
//client side code:
socket.on('roomSize', (roomSize) => {
document.getElementById('clientCount').innerHTML = roomSize + ' clients connected';
});
//Server side code:
io.on("connection", (socket) => {
socket.on('createRoom', () => {
let ID = makeID(4)
while (io.sockets.adapter.rooms.has(ID)) {
ID = makeID(4)
}
socket.join(ID);
socket.emit('setID', ID);
});
socket.on("joinRoom", (room) => {
socket.join(room);
let roomSize = io.sockets.adapter.rooms.get(room).size
io.in(room).emit('roomSize', roomSize);
socket.emit('roomJoined', room, roomSize);
});
socket.on('disconnecting', function() {
let rooms = socket.rooms;
rooms.forEach(function(room) {
if (room.length === 4) {
let roomSize = io.sockets.adapter.rooms.get(room).size - 1
io.in(room).emit('roomSize', roomSize);
}
});
});
function makeID(length) {
var result = '';
var characters = '0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
})

Related

Add a private chat feature in my application (Node js)

I have recently started learning about Node js and developed a Realtime chat application where whoever is connected to the server they can have a chat but now I want to add a feature of private chat between two users only so how can I achieve it? I have read about the room functionality of socket.io but by that I can get that it's a room and in that room there can be many users and they can have chat but it's not personal means many users can join the same chat. I need to know how to implement chat feature where two people can have a chat no third person can enter that particular chat. My question is different than other questions present here as I want to implement both the functionalities I need group and private chat both in one application.
My idea is in my group chat functionality username and message are displayed so if one user clicks on username then the person can start their personal private chat no third person can join it.
Here I am sharing my code snippets for your reference
Server.js
const io = require('socket.io')(http)
io.on('connection', (socket) => {
console.log('Connected...')
socket.on('message', (msg) => {
socket.broadcast.emit('message', msg)
})
})
Client.js
const socket = io()
let name;
let textarea = document.querySelector('#textarea')
let messageArea = document.querySelector('.message__area')
do {
name = prompt('Please enter your name: ')
} while(!name)
textarea.addEventListener('keyup', (e) => {
if(e.key === 'Enter') {
sendMessage(e.target.value)
}
})
function sendMessage(message) {
let msg = {
user: name,
message: message.trim()
}
// Append
appendMessage(msg, 'outgoing')
textarea.value = ''
scrollToBottom()
// Send to server
socket.emit('message', msg)
}
function appendMessage(msg, type) {
let mainDiv = document.createElement('div')
let className = type
mainDiv.classList.add(className, 'message')
let markup = `
<h4>${msg.user}</h4>
<p>${msg.message}</p>
`
mainDiv.innerHTML = markup
messageArea.appendChild(mainDiv)
}
// Recieve messages
socket.on('message', (msg) => {
appendMessage(msg, 'incoming')
scrollToBottom()
})
function scrollToBottom() {
messageArea.scrollTop = messageArea.scrollHeight
}
Please help me out!
when a new user join keep that socket id, and while sending message send that unique userid and emit message to that socket only,
const io = require('socket.io')(http)
var user={};
io.on('connection', (socket) => {
console.log('Connected...')
socket.on('join', (userid) => {
users[userid]=socket.id;
});
socket.on('privateMessage', (data) => {
io.sockets.socket(users[data.to]).emit('message', data.msg);
});
socket.on('publicMessage', (msg) => {
socket.broadcast.emit('message', msg)
});
});

socket.io random chatroom one to one

so there is my code i am trying to create chatroom with Socket.io with rooms (one-to-one random chatroom) but i think there is bug because when i connect first two sockets it works perfectly but when i connect third socket it does not work anymore i think it is because of "numb++" but i don't know how to correct it because i have not many experience in rooms. i wanna implement : chat between 2 socket and when third client joins i wanna create second room and wait for forth socket to join to chat and etc. , and u i know i wanna one to one random chatroom like Omegle.
var numb = 1;
var chnm = io.of('/STchat')
app.get('/STchat', function(req, res){
res.sendFile('C:\\Users\\tuna\\Desktop\\JUSTFOL\\views\\Tchat.html')
chnm.once('connect', function(socket){
socket.join("room" + numb)
console.log('made socket connection', socket.id);
chnm.in("room" + numb).clients(function(err, clients){
if(clients.length > 2) {
numb++
}
});
socket.on('chat', function(data) {
chnm.in("room" + numb).emit('chat',data)
})
socket.on('disconnect', function(){
console.log('socket disconnected')
});
}
});
app.get('/StvChat', function(req ,res){
res.sendFile(__dirname + '/views/VideoC.html');
});
var socket = io.connect('http://localhost:5000/STchat')
var message = document.getElementById('message');
handle = document.getElementById('handle');
btn = document.getElementById('send');
output = document.getElementById('output');
typing = document.createElement('p');
typing.className = "tycl";
input = document.getElementById("message");
$('form').submit(function(){
socket.emit('chat',{
message: message.value,
});
$('#message').val('');
return false;
});
input.addEventListener("keyup", function(event) {
event.preventDefault();
if (event.keyCode === 13) {
document.getElementById("send").click();
}
});
socket.on('chat', function(data){
output.innerHTML += '<p>' +' '+ data.message +'</p>'
});
This was too long to fit in the comments so I have added as an answer. To clarify, here is an issue I am seeing in the way you are emitting the chat event. Lets walk through a scenario.
var numb = 1;
Two users (User A, and User B) connect to the socket and are put into room "room1". Now, numb should be incremented by your code and now numb will be == 2.
Now, User A (who is in "room1") emits a "chat" event. Here is the issue I see:
socket.on('chat', function(data) {
chnm.in("room" + numb).emit('chat', data)
})
User A's chat event is emitted to "room2" but User A is in "room1". This happens because numb == 2 at this point in the scenario and you are using numb to emit that event, basically this is what happens chnm.in("room" + 2). User B does not get User A's emit, because it was sent to the wrong room (room2). If I misunderstood what you are trying to do please let me know.
EDIT: As requested here is some modifications to your code that I have tested locally with success. Two users per each room.
var numb = 1;
var chnm = io.of('/STchat');
app.get('/STchat', function(req, res) {
res.sendFile('C:\\Users\\tuna\\Desktop\\JUSTFOL\\views\\Tchat.html');
chnm.once('connect', function(socket) {
// store the room in a var for convenience
var room = "room" + numb;
// join socket to the next open room
socket.join(room);
// store room on socket obj for use later - IMPORTANT
socket.current_room = room;
// log some stuff for testing
console.log('made socket connection', socket.id);
console.log("Joined room: ", socket.current_room);
// Check room occupancy, increment if 2 or more
chnm.in(room).clients(function(err, clients) {
if (clients.length >= 2) {
numb++;
}
});
socket.on('chat', function(data) {
// emit to that sockets room, now we use that current_room
chnm.in(socket.current_room).emit('chat', data);
});
socket.on('disconnect', function() {
console.log('socket disconnected');
});
});
});
app.get('/StvChat', function(req, res) {
res.sendFile(__dirname + '/views/VideoC.html');
});

double sockets in room

When I press F5 repeatedly without stopping the same socket.username enters the room, generating a list of duplicate names. By spending time, this sockets duplicated vain automatically disconnecting .
I tried with these lines after disconnecting and not work!
delete io.sockets.adapter.sids[socket.id];
delete io.sockets.adapter.rooms[socket.id];
How can I avoid this? Tks.
// SERVER.JS
io.set('authorization', function (handshakeData, accept) {
if (handshakeData.headers.cookie) {
handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie['sec_session_id'], 'secret');
if (handshakeData.cookie['sec_session_id'] != handshakeData.sessionID) return accept('Cookie is invalid.', false);
} else return accept('No cookie transmitted.', false);
accept(null, true);
});
io.sockets.on('connection', function(socket) {
socket.on('adduser', function(name, room, picuser, codeuser, operation) {
var handshakeData = socket.request;
var cookies = cookie.parse(handshakeData.headers.cookie);
if(name.length > 0)
{
if (!io.sockets.adapter.sids[socket.id][room]) {
var newCookie = changeCookie(cookies.sec_session_id);
cookiesSockets[cookies.sec_session_id] = newCookie;
cookiesSockets2[newCookie] = socket.id;
socket.color = getRandomColor();
socket.username = name;
socket.room = room;
socket.newid = newCookie;
socket.picuser = picuser;
socket.codeuser = codeuser;
// add the client's username to the global list
usernames[name] = name;
// send client to room
socket.join(room);
// echo to client they've connected
socket.emit('updatechat', socket.picuser, socket.codeuser, socket.color, getClock(), 'You', 'join in '+room+'.', room, 0, 0, getClock2(), operation);
// echo to room 1 that a person has connected to their room
socket.broadcast.to(room).emit('updatechat', socket.picuser, socket.codeuser, socket.color, getClock(), name,'join in room.', room, 0, 0, getClock2(), 3);
//update id's private chats on socket disconnecting and connecting...
if (SocketsUsernames[name])
{
if (SocketsUsernames[name]!=cookies.sec_session_id)
{
var cookieReal = SocketsUsernames[name];
var cookieChanged = cookiesSockets[cookieReal];
delete cookiesSockets[cookieReal];
delete cookiesSockets2[cookieChanged];
socket.broadcast.to(room).emit('updatechatpvt', room, cookieChanged, newCookie);
}
}
SocketsUsernames[name] = cookies.sec_session_id;
updateUsers(room);
}
else
socket.emit('updatechat',null, null, null, null, null, null, room, null, null, null, 7);
}
});
socket.on('disconnect', function(){
if (socket.username)
{
// remove the username from global usernames list
delete usernames[socket.username];
socket.leave('myroom');
}
});
});
//CLIENT.JS
socket.on('connect', function (){
// Retrieve the object from storage CHATROOM!!
var retrievedRooms = localStorage.getItem('myRooms');
console.log('retrievedRooms: ', JSON.parse(retrievedRooms));
if (retrievedRooms && Object.keys(JSON.parse(retrievedRooms)).length>0)
Object.keys(JSON.parse(retrievedRooms)).forEach(function(key) {
if (key)
{
myRooms[key]=key;
socket.emit('checkuser', key, function(callback){
if(!callback) socket.emit('adduser', $('#modalPerfil').attr('data-username'), key, $('#modalPerfil').attr('data-picuser'), $('#modalPerfil').attr('data-userid'), 1);
});
}
});
});
// SOLUTION IN CLIENT.JS - setTimeout
socket.on('connect', function (){
// Retrieve the object from storage CHATROOM!!
var retrievedRooms = localStorage.getItem('myRooms');
console.log('retrievedRooms: ', JSON.parse(retrievedRooms));
if (retrievedRooms && Object.keys(JSON.parse(retrievedRooms)).length>0)
Object.keys(JSON.parse(retrievedRooms)).forEach(function(key) {
if (key)
{
myRooms[key]=key;
setTimeout(function(){
socket.emit('checkuser', key, function(callback){
if(!callback) socket.emit('adduser', $('#modalPerfil').attr('data-username'), key, $('#modalPerfil').attr('data-picuser'), $('#modalPerfil').attr('data-userid'), 1);
});
}, 1000);
}
});
});
This code is causing you a couple problems:
socket.onclose = function (reason) {
roomsToLeaveClient = socket.rooms;
Object.getPrototypeOf(this).onclose.call(this, reason);
});
First off, There's an extra ) at the end that is closing off your .on('connect') block.
That then causes your socket.on('disconnect', ...) code to be outside the scope of socket so it probably isn't getting called appropriately. I'd suggest you don't need to listen for a close event at all. You can just listen for the disconnect event and do any of your cleanup business there.
And, since you're off one set of parens in that code, there's probably a corresponding error somewhere else in your code that still allows things to parse properly. Putting your code in a linter like http://jshint.com shows these types of errors.
Then, if you really did want to override .onclose, you can't just call the prototype version. Instead, you have to save the version that was in place before you assign your override and call that one. That's the only way the is compatible with any other overrides. But, you really ought to just be using .on() with event names to watch socket lifecycle things, then you don't have to worry about proper overrides in any way.
I can't tell right away if there are other issues, but this is a glaring one that needs to be fixed first.
Also, you do not need your roomsToLeaveClient handling. socket.io will automatically remove a disconnecting client from any rooms that it is in. That is not something you need to do.
If you want to know what rooms a socket is in so you can tell the other members of that room that you are leaving, then socket.io has a data structure that tells you exactly what rooms the socket is in and you can just iterate that data structure. You don't have to keep track of that yourself.

Socket 1.0 repeating connect multiple times

I'm using Heroku + RedisToGo + Express 4.0 + socket.io 1.0.6.
I just recently upgraded to 1.0 from 0.9, and it's half working now. I've hacked together a app from tutorials but my lack of understanding socket.io is showing, so I'm taking a step back. The first question I have is that now socket.on('connect') is happening repeatedly, without stop, even when a connection is successful. My client-side console.log just keeps going and going. Here's client-side:
// Connect the user
socket.on('connect', function(){
var currentUserId = '<%= currentUser.id %>';
// Add user to redis
socket.emit('login', { userID: currentUserId});
// Retrieve presence info
socket.emit('presence');
});
// Show Presence
socket.on('presence', function(data) {
var userID = data.user;
var presence = data.presence;
if (presence) {
$('#red-dot-' + userID).css("display", "none");
$('#green-dot-' + userID).css("display", "inline");
// Show the hangout button
$('#hangout-' + userID).show();
$('#hangout-unavail-' + userID).hide();
}
else {
$('#red-dot-' + userID).css("display", "inline");
$('#green-dot-' + userID).css("display", "none");
}
});
And server-side:
io.sockets.on('connection', function (socket) {
var savedUserID;
socket.on('login', function(data){
var userID = data.userID;
savedUserID = userID;
// add first user
redis.sadd("users", userID);
redis.hmset("users:"+userID, "socketID", socket.id, "userID", userID);
});
socket.on('presence', function(){
// Get the list of online users and show Presence
redis.smembers("users", function(err,results) {
var onlineUsers = results;
for (var i in onlineUsers) {
var userID = redis.hget("users:"+onlineUsers[i],"userID", function(err,reply) {
var userID = reply;
// Emit presence
io.sockets.emit('presence', {
user: userID,
presence: "true"
});
});
}
});
});
As you can see I'm manually adding users to redis.
I found the answer in this Google Group thread.
The problem occurred because I had socket.emit('presence'); in my socket.on('connect', function(){}); client-side.
This in turn called socket.on('presence', function(){}); server-side, which was the problem.

Socket.io disconnects when refreshed?

I am trying to build a live chat, I am using mongodb and socket.io to store the messages and users.
When a new user is created that user is stored in the mongodb and in the socket object.
If a user refreshes the page the user is removed from the socket object, meaning now in order for that person to get back in they have to create a new username and that generates a new socket.
Here is what my server side code looks like
/*
|--------------------------------------------------------------------------
| Live socket communication with front end:
|--------------------------------------------------------------------------
|
|
*/
var users = {};
io.sockets.on('connection', function (socket) {
// Listen to a new user then emit the user to all clients
socket.on('new user', function (data) {
// Check mongodb to see if the user exists and emit proper message
models.Message.findOne({username:data},function(err,user){
if(err){
console.log('something went wrong')
}
else if(user){
socket.emit('username taken', 'something');
}
else{
socket.emit('create user', data);
socket.userName = data;
socket.connected = true;
users[socket.userName] = socket;
io.sockets.emit('user name', Object.keys(users));
}
});
});
socket.on('facebook id', function(data) {
models.User.findOne({username:data.name}, function(err, user) {
if (user) {
console.log('User already exists');
socket.userName = data.name;
socket.facebook_id = data.id;
socket.connected = true;
users[socket.userName] = socket;
io.sockets.emit('user name', Object.keys(users));
}
else {
var newUser = new models.User({
username: data.name,
facebook_id: data.id
});
newUser.save(function(err, user) {
console.log('successfully inserted user/user: ' + user._id);
});
}
});
});
// Listen to a new message then emit the message to all clients
socket.on('send message', function (data, callback) {
io.sockets.emit('new message', {message: data, username: socket.userName, facebook_id: socket.facebook_id});
});
// Logic when client disconnects
socket.on('disconnect', function (data) {
if(!socket.userName) return;
seeder.disconnect(socket.userName);
delete users[socket.userName]
io.sockets.emit('user disconnected', Object.keys(users));
});
});
You see in my disconnect I remove the socket from the users object.
My question would be is there a way to save the socket info on disconnect then if the same socket tries to connect have it recognize the user and continue?
Additonal: I am thinking maybe I need to focus on creating a user login with mongodb first, then using that log in session data and pass that to the socket, creating a socket object with current database details? Does that sound like something that makes more sense, is that possible?
You can use cookies to identify users. Generate a random hash, put it in cookies, and thus this data will be transferred when establishing connection between client and server.
The client code may look like:
function generateHash(len) {
var symbols = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
var hash = '';
for (var i = 0; i < len; i++) {
var symIndex = Math.floor(Math.random() * symbols.length);
hash += symbols.charAt(symIndex);
}
return hash;
}
if (!/\buser_id=/.test(document.cookie)) { //if no 'user_id' in cookies
document.cookie = 'user_id=' + generateHash(32); //add cookie 'user_id'
}
//here goes establishing connection to server via `io.connect`
On the server-side you can write:
io.sockets.on('connection', function (socket) {
var cookie = socket.handshake.headers.cookie;
var match = cookie.match(/\buser_id=([a-zA-Z0-9]{32})/); //parse cookie header
var userId = match ? match[1] : null;
//...
Thus you have userId variable which is unique for each user. Then you can comment this line:
delete users[socket.userName]
because you should keep the user data.
You may now store your users object with userId (not username) as a key, and on each connection check whether users[userId] != null. And if such user exists, use their socket info

Resources