how to create a room based on 2 users in socket.io? - node.js

My goal is to create a one to one chat based on two different users. The only way that I could think of is to use socket.io rooms
But the problem right now is that how do i create unique room?
For example
socket.on('join', function(room) {
socket.join(room);
});
Do i need to emit the room from the client, if so , how do I make it unique. Imagine there are thousands of users.
The chat application, is similar like facebook chat application. Where you can chat one on one.
Do i need redis or mongodb to store the room? Anyone of you who have experience using socket.io in scale, please do share your opinion
Thanks!

A room always will be unique, when you do socket.join('roomname') if the room not exist it will created and this socket will join it, if exist the socket just will join it.
So if you want that client A join in the room where is client B for example, from client A you can send a event like:
socket.emit('joinroom', roomname);
On sever:
socket.on('joinroom', function(data){
socket.join(data)
})
Anyway when a socket connect , it create and join his own room automatically , the name of this room will be the id of the socket, so i think is not neccessary create new rooms for a chat based on two different users.
Everything that you need is link the socket id with a permanent property of the user.
EDIT:
I leave you here a simple chat app example where you can open multiple conversations:
server.js: https://gist.github.com/pacmanmulet/b30d26b9e932316f54b2
index.html: https://gist.github.com/pacmanmulet/6481791089effb79f25f
You can test it here :https://chat-socket-io-example.herokuapp.com/
I did not use rooms, it have more sense when you want emit to a group of sockets, not to a only one.
Hope you can understand better my idea with that.

you need to store the room number somewhere(any database).You have to do this because you have to keep your server stateless.
Let us assume that you are creating a private chat only for two people.The room number has to be unique. so one approach is to use the user's email id and join them to create a new string and emit it back to the users.this is tricky because we don't know the order in which the strings are joined. so we join them by a string not used in normal email name(eg :'"#!#!#!!#!#!#!').we can split it on the server side and compare emit the results.
The actual message body will be
{
room:a#gmail.comb#gmail.com,
from:a,
message:'hi buddy how are you?'
}
CLIENT side code
const roomName = a#gmail.com+'#!#!2!#!#"+b#gmail.com
socket.emit('room', { room: roomName });
this.socket.on('joined', data => {
console.log('i have joined', data.room)
store the room name room: data.room })
})
socket.on('chat',data=>console.log(`received chat from ${data.from} from the message room ${data.room}`)
used '#!#!2#!#' just because we can separate them on the server side and check if the room already exists.
SERVER side code
const room =[]//this variable you have store in database and retrieve it when needed.
socket.on('room',data=>{
if(room.length!=0){
const temp = data.room.split('!#!#2#!#!').reverse().join('!#!#2#!#!');
if(room.includes(temp)){
socket.join(temp)
console.log('joined room',temp)
socket.emit('joined',{room:temp})
console.log(room);
} else if(room.includes(data.room)){
socket.join(data.room)
console.log('joined room', data.room)
socket.emit('joined', { room: data.room})
console.log(room);
}
}else{
socket.join(data.room);
room.push(data.room)
console.log('joined room',data.room);
socket.emit('joined', { room: data.room })
console.log(room);
}
})

I tried to do a minimal example of where you can only be in one room at a time (apart from your default socket.id room) and only other sockets in the same room as you will receive your messages. Also you can change rooms.
The basic premise is, if socket A is in room 'xyz' and so is socket B, on the server side you can do socket.to('xyz').emit('message', 'hello') for socket A, and socket B will receive the message, but another connected socket C which isn't in room 'xyz' won't.

You can create room at server runtime, I used both users id as room id, Ex : '100-200' for demo purpose. May be you can use some more complex approach.

Related

Socket.io not sending to one socket in a room

I'm using socket.io to handle some of the server-client communication and matchmaking for a simple multiplayer game.
(for now) im automatically joining 2 players together by adding them into a socket.io room. When a room has 2 players in it I emit a "startGame" event to the room using socket.to(ROOM).emit(EVENT, EVENT_MSG) after doing a check
server side nodeJS:
game_start_state = checkRooms(socket, freeRooms);
if (game_start_state){
console.log("told room", game_start_state, "to start their game!")
socket.to(game_start_state).emit("startGame", game_start_state);
}
but so far only the first socket that gets connected to the room receives the "startGame" event message, I've looked around and havent seen anyone else with the same problem. Below is the code that is fired after the client emits an event saying it wants to join a room.
server side nodeJS:
function checkRooms(socket, roomArray) {
// auto-matchmaking logic
if(!roomArray || !roomArray.length){
//if there is no room with space create a new one
const room = uuid();
r_list.push(room); // r_list is just an array used to keep track of opened rooms for displaying to the user through some html
freeRooms.push(room); // freeRooms is an array with rooms with just 1 socket connected
joinRoom(socket, room);
return(null);
} else {
// if there is a room with a space, try to connect the client to it
const room = freeRooms[0];
console.log(socket.id, "wants to join", room);
// connect client to rooms
joinRoom(socket, room);
// room is now full so start the game
freeRooms.pop(room);
return(room);
}
}
because for now there is only auto matchmaking, there will only be 1 room in the freeRooms array so I'm not worries about this.
Does anyone know where I could be messing up? Can provide more code examples if necessary.
socket.to(room).emit(...)
sends to every member of the room EXCEPT the referenced socket. You can see that documented here.
If you want to send to everyone in the room, then use:
io.to(room).emit(...)

Why use redis in a chat application? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 years ago.
Improve this question
I just recently built a chat, it's working pretty well, but I think I need to hook it up to redis.
From what I understand I need redis for scaling and holding some data if a client refreshes or a server goes down.
A core component of the 1on1 chat is that I store the users, and associate a socket.id to those users
var users = {};
io.sockets.on('connection', function (socket) {
// store the users & socket.id into objects
users[socket.handshake.headers.user.username] = socket.id;
});
Now on the client side I can say hey I want to chat with "Jack", as long as that is a valid user then I can pass that data to the server, i.e the user name and message just to jack like so.
var chattingWith = data.nickname; // this is Jack passed from the client side
io.to(users[chattingWith]).emit();
My question is, why should I use redis? What should I store in redis? How should I interact with that data?
I am using an io.adapter
io.adapter(redisIo({
host: 'localhost',
port: 6379,
pubClient: pub,
subClient: sub
}));
Also reading code from an example app I see when a socket connects they save the socket data into redis like so.
// store stuff in redis
redisClientPublish.sadd('sockets:for:' + userKey + ':at:' + room_id, socket.id, function(err, socketAdded) {
if(socketAdded) {
redisClientPublish.sadd('socketio:sockets', socket.id);
redisClientPublish.sadd('rooms:' + room_id + ':online', userKey, function(err, userAdded) {
if(userAdded) {
redisClientPublish.hincrby('rooms:' + room_id + ':info', 'online', 1);
redisClientPublish.get('users:' + userKey + ':status', function(err, status) {
io.sockets.in(room_id).emit('new user', {
nickname: nickname,
provider: provider,
status: status || 'available'
});
});
}
});
}
});
They use it when entering a room, to get information about the room.
app.get('/:id', utils.restrict, function(req, res) {
console.log(redisClientPublish);
utils.getRoomInfo(req, res, redisClientPublish, function(room) {
console.log('Room Info: ' + room);
utils.getUsersInRoom(req, res, redisClientPublish, room, function(users) {
utils.getPublicRoomsInfo(redisClientPublish, function(rooms) {
utils.getUserStatus(req.user, redisClientPublish, function(status) {
utils.enterRoom(req, res, room, users, rooms, status);
});
});
});
});
});
So again, I am asking because I am kind of confused if I need to store anything inside redis/why I need to, for instance we may have a few hundred thousand users and the node.js server "Jack" and "Mike" are chatting on goes down, it then changes to point to a new node.js instance.
Obviously I want the chat to still remember "Jack's" socket id is "12333" and "Mike's" socket id is "09278" so whenever "Jack" says hey I want to send "Mike/09278" a message the server side socket will direct it properly.
Would storing the username as a key and socket ID as a value be a wise use case for redis, would that socket.id still work?
Redis is a pretty good choice as a database for a chat as it provides a couple of data structures that are not only very handy for various chat use cases but also processed in a really performant way. It also comes along with a PubSub messaging functionality that allows you to scale your backend by spawning multiple server instances.
Scaling socket.io with the socket.io-redis adapter
When you want to run multiple instances of your server - be it because of one server not being able to handle increasing users any more or for setting up a high availablility cluster - then your server instances must communicate with each other in order to be able to deliver messages between users who are connected to different servers. The socket.io-redis adapter solves this by using the redis PubSub feature as a middleware. This won't help you if you are using only a single server instance (in fact I assume it will be slightly less performant) but as soon as you spawn a second server this will work out just fine without any headaches.
Want to get a feeling and some insight on how it's working? Monitor your dev redis while using it and you'll see the internal socket.io messages that are pushed through redis.
redis-cli
monitor
Use cases and their according redis data types
Save active conversations in a SET
A redis set is a collection of unique strings. I don't think storing socket.io id's would work out well as you can't assume that a user will get the same id on a reconnect. Better store his rooms and rejoin him on connect. You add every chat room (btw. direct messages can be defined as a room with two participiants so the handling is the same in both cases) that a user enters to their room set. On a server restart, a client reconnect or second client instance you can retrieve the whole set and rejoin users to their rooms.
/* note: untested pseudo code just for illustration */
io.sockets.on('connection', function (socket) {
rooms = await redis.smembers("rooms:userA");
rooms.foreach (function(room) {
socket.join(room);
}
socket.on('leave', room) {
socket.leave(room);
redis.srem("rooms:userA", room);
}
socket.on('join', room) {
socket.join(room);
redis.sadd("rooms:userA", room);
}
}
Save the last 10 messages of a conversation using a redis LIST
A redis list is somewhat of an persistent array of strings. You push a new message into a list and pop the oldest when the list size reaches your threshold. Conveniently the push command returns the size right away.
socket.on('chatmessage', room, message) {
if (redis.lpush("conversation:userA:userB", "Hello World") > 10) {
redis.rpop("conversation:userA:userB");
}
io.to(room).emit(message);
}
To get the message history use lrange:
msgHistory = redis.lrange("conversation:userA:userB", 0, 10)
Save some basic user details in a HASH
A hash is a key/value collection. Use it to store the online status along with avatar urls or whatever.
io.sockets.on('connection', function (socket) {
redis.hset("userdata:userA", "status", "online");
socket.on('disconnect', function () {
redis.hset("userdata:userA", "status", "offline");
}
}
Maintain a "recent conversations" list in a SORTED LIST
Sorted sets are similar to SETs but you can assign a score value to every element and retrieve the set ordered by this value. Simply use a timestamp as score whenever there is an interaction between two users and that's it.
socket.on('chatmessage', room, message) {
io.to(room).emit(message);
redis.zadd("conversations:userA", new Date().getTime(), room);
}
async function getTheTenLatestConversations() {
return await redis.zrange("conversations:userA", 0, 10);
}
References
socket.io-redis: https://github.com/socketio/socket.io-redis
redis PubSub docs: https://redis.io/topics/pubsub
redis data types: https://redis.io/topics/data-types-intro

Rooms are not removed after everyone leaves

As far i read from the doc
that Rooms are left automatically upon disconnection and they are automatically removed when everyone leaves. But this is not the case of my actual code:
io.on('connection', function(socket) {
socket.join(MainRoom);
io.sockets.adapter.rooms[socket.id].owner = socket.username;
//send the list of available rooms on connection
socket.to(MainRoom).emit('updateList',io.sockets.adapter.rooms);
socket.on('getUpdateList',function() {
io.to(MainRoom).emit('updateList',io.sockets.adapter.rooms);
});
socket.on('msg', function(msg) {
io.to(MainRoom).emit('msgFront',msg);
});
socket.on('disconnect', function() {
console.log('leaving '+socket.id);
io.to(MainRoom).emit('updateList',io.sockets.adapter.rooms);
});
});
Notice that I'm using a MainRoom where all client are forced to join it just to make sure that everyone can talk to each other.
By default Each Socket in Socket.IO is identified by a random, unguessable, unique identifier Socket#id. For your convenience, each socket automatically joins a room identified by this id.
My problem is that after closing/refreshing the browser tab, all previously joined rooms are still there, and the number of rooms is incremented(on connection the socket join new rooms automatically..)
Anyone can explain this behavior ?
Solved:
The problem is that i extended the rooms object bu adding owner attribute :
io.sockets.adapter.rooms[socket.id].owner = socket.username;
So that the extended room can't be removed. the solution that i found is to store owner attribute outside within an associative array, that's it.

node.js joining and leaving chat room

I am relatively new to node.js and have been looking it up for quite some time now. I have a problem regarding the handling of multiple chat rooms.
When a user connects, they automatically join the room that I have setup. However, the problems is that the connection is persistent and I wish to address the problem by using the socket.leave(room) method.
My question is, how can this be handled on the client side?
Am I right in putting this on my script?
server.js
io.sockets.on( 'connection', function( socket ) {
socket.on('join', function(room) {
socket.join(room);
console.log("User joined "+room);
});
socket.on('disconnect', function(room) {
socket.leave(room);
console.log("User left "+room);
});
On the client, how can I trigger the disconnect event? Should I have it called on Page Unload?
EDIT: Additional Info
User 1 connects to room 1
User 2 connects to room 1
This is a good and great room connection.
However, once there's a switch of rooms
User 1 connects to room 2
User 3 connects to room 2
User 1 is not able to receive the messages from User 3 because he is still listening to room 1
This is the reason why I wish to manage the rooms that the user wishes to join to, hence my question on how to force a User to leave a specific room.
Any advice would be greatly appreciated.
If you emit some event like change room from your client code when user changes room like this:
socket.emit("change room", {newroom: "room name"});
Let's suppose a client has already joined a room as:
socket.on('join', function(room){
socket.room = room;
socket.join(room);
});
Then you can do this in server to change the room of that client:
socket.on("change room",function(data){
socket.leave(socket.room);
socket.join(data.newroom);
});
You don't have to call socket.leave() in disconnect event. You can do it in your own logic. Hope you can understand this!

How to handle user and socket pairs with node.js + redis

Straight to the point:
I am using node.js, socket.io and redis for a private chat system.
On connect user passes his website id (userID) to node.js server. He may have multiple connections so I have to pair socketID (of each connection) and userID somehow. I has thinking about using redis to store userID->sessionID pairs. However, when user disconnects I need to remove that pair from redis.. but I have only socketID not userID so I can't select by that key..
Now, am I approaching this the wrong way or should I store both userID->socketID and socketID->userID pairs? Maybe someone could offer more elegant solution?
A more elegant solution would be to make each socket connect to the channel userID, for example:
io.sockets.on('connection', function (socket) {
socket.join(userID);
});
// when you want somebody to send a message to userID you can do:
io.sockets.in(userID).emit(message);
There are two things you need to take care of here:
Make sure that only userID can connect to his channel, thus verify the session ( read more here: http://www.danielbaulig.de/socket-ioexpress/ )
On connection increase the value for userID in redis (so that you know a new connection for that user is listening) and on disconnect decrease the userID value (so that you know the number of connections still listening). If the value is 0 then you emit a message to the chat stating that userID has left (since the number of connections listening to the userID channel is 0).
When other users will want to send a message to userID, they don't need to connect to the userID channel, they can send a message to the chat channel and pass userID as a property. For example:
var chat = io
.of('/chat')
.on('connection', function (socket) {
// connected to public chat
})
.on('message', function (data) {
if (data.userID && data.message) {
io.sockets.in(userID).emit('UserX: ' + data.message);
}
});

Resources