I'm developing a chat app and i have some trouble with socket.io.
Stuff works great and quick. But as soon as more people are logged in - everyone see's all the messages and rendering gets messy.
I'm sending a unique ID with each message, which i would love to create rooms with where people can chat with each other (1 on 1)
THis happens first:
const socketMessage = {
type: type,
body: body,
author: author,
date: Date.now(),
id: chatId
};
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('message-sent', socketMessage);
THis goes to the server where this happens:
socket.on('message-sent', data => {
socket.broadcast.emit('new-chat-message', data);
});
WHich then goes into a custom hook and adds to the messages-array.
export default function useGetChatMessages(_id) {
const [chatMessages, setChatMessages] = React.useState([]);
React.useEffect(() => {
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('send-chat-id', _id);
socket.on('chat-messages', data => {
setChatMessages(data.messages);
});
socket.on('new-chat-message', message => {
setChatMessages(messages => [...messages, message]);
notification.play();
});
}, [_id]);
return chatMessages;
}
I know the broadcast part is wrong on my server but i have tried lots of things:
Sending the chatId with the message and then do socket.join(chatId) at several stages.
Then socket.to(chatId).emit(...) and so on.
But i can not get it right.
Can someone help?
Thanks a lot in advance!! :-)
Actually, you could use the socket io feature called namespaces.
Look an pretty simple example:
In the client side you would do something like this:
const socket = io.connect(socket_url, { query: "chatRoomId=123&userId=983912" });
And in the server side something like this:
io.on('connection', (socket) => {
const chatRoomId = socket.handshake.query['chatRoomId'];
const userId = socket.handshake.query['userId'];
const user = find(userId) // Pretend :)
socket.join(chatRoomId);
socket.on('new_message', (id, msg) => {
io.to(chatRoomId).emit(`${user.name} says ${msg}`);
});
socket.on('disconnect', () => {
socket.leave(chatRoomId)
console.log('user disconnected');
});
});
Related
I have been encountering an issue for a long time but I haven't found any solution yet, despite reading the protocol version 5 standard and the emqx documentation.
I want to publish messages with a time limit to simulate a situation where my device is unavailable so that, after the time limit has expired, the broker will delete the message and my device will not receive it.
I want the reason that my device will not be available (and therefore will not receive messages), to be because it is in a closed area, like a tunnel, or in a area with no cellular cove-range and not because it initiated a disconnect from the broker or because the “keepalive” value has expired.
My understanding is that I can use the “messageExpiryInterval” property (in protocol 5) to implement my goal.
I used EMQX broker as follows:
const connectUrl = 'mqtt://broker.emqx.io:1883';
Along with the following connection configuration:
const client = mqtt.connect(connectUrl, {
clientId: 'mqtt_dani_pub',
protocolVersion: 5,
keepalive: 1800,
clean: true
});
And when sending a message, I put the following values:
const options = {
qos: 0,
retain: false,
properties: { messageExpiryInterval: 30 }
};
As you can see, I used a high value, 1800, for “keepalive” to ensure that the device will be connected to the broker for a long time.
To simulate this situation, I used one publisher on one PC, and one subscriber on another PC.
The scenario is as follows:
Connect publisher and subscriber to emqx broker
a. Verify MQTT v5 protocol.
Publish the message (when the subscriber is connected) to emqx with MessageExpiryInterval: 30
a. Subscribe receive the message
Turn OFF the Wi-Fi on the subscriber computer.
Publish the message to emqx with MessageExpiryInterval: 30
Wait for 120 seconds
Turn ON the Wi-Fi on the subscriber computer.
a. Subscriber does not receive the message ==> but it does get the message!
In addition to this, I saw in the standard of protocol 5 section 3.3.2.3.3 (Message Expiry Interval - https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.pdf ) that the PUBLISH packet sent to a Client by the Server MUST contain a Message Expiry Interval set to the received value minus the time that the Application Message has been waiting in the Server, so this may be the problem.
my publisher js code:
import mqtt, { MqttClient } from 'mqtt';
import * as readline from 'node:readline'
import { stdin, stdout } from 'process';
const connectUrl = 'mqtt://broker.emqx.io:1883';
const clientId = 'mqtt_dani_pub';
const topic = 'dani/test';
const subject = 'Publisher';
const rl = readline.createInterface({
input:stdin,
output:stdout
});
const client = mqtt.connect(connectUrl, {
clientId,
protocolVersion: 5,
keepalive: 1800,
clean: true
});
client.on('connect', () => {
console.log(`${subject} client connected..`)
client.subscribe([topic], () => {
console.log(`Subscribe to topic '${topic}'`);
})
});
const options = {
qos: 0,
retain: false,
properties: { messageExpiryInterval: 30 }
};
const publishMsg = (message) => {
client.publish(topic,
`${clientId} - ${Date.now()} - ${message}`,
options,
(error) => {
if (error) {
console.error(error)
}
}
);
};
const input2topic = () => {
return new Promise(resolve => {
rl.question(`send message to topic ${topic}: `,
(input) => {
if(input !== 'exit'){
console.log(`writing to topic ${topic}..`);
publishMsg(input);
resolve(true);
} else{
console.log('exit...');
resolve(false);
}
});
});
}
const main = async () => {
publishMsg('first message');
let flag = true;
while(flag){
await new Promise(resolve => setTimeout(resolve, 1000));
flag = await input2topic();
}
rl.close();
client.end();
}
main();
my subscriber js code:
import mqtt, { MqttClient } from 'mqtt';
const connectUrl = 'mqtt://broker.emqx.io:1883';
const clientId = 'mqtt_dani_sub';
const topic = 'dani/test';
const subject = 'Subscriber';
const client = mqtt.connect(connectUrl, {
clientId,
keepalive: 1800,
protocolVersion: 5,
})
client.on('connect', () => {
console.log(`${subject} client connected`)
client.subscribe([topic], {qos: 0}, () => {
console.log(`Subscribe to topic '${topic}'`)
})
});
client.on('message', (topic, payload, packet) => {
console.log('\nReceived Message:', {
...packet,
message: payload.toString(),
msg_length: payload.toString().length,
time: new Date(),
});
});
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.
so I wanted to dive into Socket.io and I am a complete beginner in it. I am a react developer and decided to test it with react, this is my client side:
import { io } from "socket.io-client";
function App() {
let [message, setMessage] = useState("");
let [messages, setMessages] = useState([]);
const socket = io("http://localhost:4000/", {
withCredentials: true,
cors: {
origin: "http://localhost:4000",
},
});
const sendMessage = (msg) => {
socket.emit("messageToServer", msg);
};
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
return (
<div className="App">
{messages}
<input
id="input"
autoComplete="off"
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={() => sendMessage(message)}>Send</button>
</div>
);
}
and this is my server side(Node, Express):
const app = require("express")();
const http = require("http").Server(app);
const io = require("socket.io")(http, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
});
io.on("connection", (socket) => {
let messages = [];
socket.on("messageToServer", (msg) => {
io.emit("messageToClient", msg);
console.log(msg);
});
});
http.listen(4000, () => {
console.log("listening on *:3000");
});
So basicly when I write something on the input and click the button, on the node server everything seems fine and the message logs only once, but when I open the tab that I send the message from and for example I typed 'First message' it will print something like: 'First messageFirst messageFirst messageFirst message' and so on...
But apparently, the duplication is happening only from the tab I am sending it from, because when I open a new one and test it, on the sender tab I get multiple like i said, but on the new one, I recieve it only one, just like it should be.
Thank you in advance!
The below snippet of code will keep App component being rendered multiple times, thus the same event handler for messageToClient will be registered multiple times and cause unexpected behavior in React.
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
You should wrap socket on event with useEffect, so that we can make sure nothing is duplicating.
useEffect(()=>{
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
// unbind the event handler when the component gets unmounted
return () => {
socket.off('messageToClient')
}
},[])
I want to implement a simple notification system. When user1 likes user2's post, user2 should get a real-time notification from user1.
Here is a part of the client function (Redux action) where someone likes a post:
.then(() => {
const socket = require("socket.io-client")(
"http://localhost:5000"
);
socket.emit("like", user, post);
});
Here is the server socket function where a notification is created after user1 likes user2's post:
io.on("connection", (socket) => {
socket.on("like", async (user, post) => {
if (!post.post.likedBy.includes(user.user._id)) {
const Notification = require("./models/Notification");
let newNotification = new Notification({
notification: `${user.user.username} liked your post!`,
sender: user.user._id,
recipient: post.post.by._id,
read: false,
});
await newNotification.save();
io.emit("notification");
}
});
});
Here is the client function after the notification is created:
socket.on("notification", () => {
console.log("liked");
});
Now the problem with this is the console.log('liked') appears for both user1 and user2. How can I emit to only that user that receives the notification? How can socket.io find this specific user2 that receives the notification from user1?
You should store a list (array or object) of all users like this :
(note that the list has to be updated when a user connects or leaves the socket server)
// an example of structure in order to store the users
const users = [
{
id: 1,
socket: socket
},
// ...
];
And then you can target the post owner and send him a notification like this :
// assuming the the 'post' object contains the id of the owner
const user = users.find(user => user.id == post.user.id);
// (or depending of the storage structure)
// const user = users[post.user.id]
user.socket.emit('notification');
Here an example :
const withObject = {};
const withArray = [];
io.on('connection', socket => {
const user = { socket : socket };
socket.on('data', id => {
// here you do as you want, if you want to store just their socket or another data, in this example I store their id and socket
user.id = id;
withObject[id] = user;
withArray[id] = user;
// or withArray.push(user);
});
socket.on('disconnect', () => {
delete withObject[user.id];
delete withArray[user.id];
// or let index = users.indexOf(user);
// if(index !=== -1) users.splice(index, 1);
});
});
There is plenty way of achieving what i'm trying to explain but the main idea is to link the socket with an index (user id for example) in other to retrieve it later in the code.
I am building a chat app with React, Node/Express and socket.io. I have my sockets successfully set to my express server via http.createServer. I have a listener on client and server listening for new messages coming into the chat room. Ideally, I want each instance of the chat to be updated when there is an additional message, like any chat room that ever existed :)
Now I have a successful listen between client and server. I know because of a console.log server-side. However, I am not re-rendering the chat component when I submit a new message from a different instance.
So my code in my client-side (again React) component is as follows and I am using the socket CDN with script tags in my index.html (script tags not shown):
Socket CDN here
var socket = io('')
So that is the socket you see client side :
componentDidMount() {
return axios.get(`api/messages`)
.then((result) => {
if (result.data.length) {
this.setState({
messages: [ ...this.state.messages, ...result.data]
} , () => {
console.log("The state after messages are mounted : ", this.state)
})
}
})
.catch((err) => { throw err})
socket.on('new message', msg => {
this.newMessage(msg);
})
};
newMessage(msg) {
this.setState({
messages: [...this.state.messages, msg]
}, () => {
this.setState({ message: '' })
return this.scrollToBottom()
});
};
onSubmitMessage(event) {
event.preventDefault();
const content = this.state.message;
const msg = {
content,
createdAt : new Date(),
userId : "one",
chatRoomId : "two"
}
axios.post(`api/messages/`, msg)
.then(() => {
this.newMessage(msg);
socket.emit('new message', msg); //HERE'S THE SOCKETS IN ACTION
})
};
Here is the server-side code Node/Express:
//in server.js
const io = new socketIo(server)
require('./socketEvents')(io);
const connections = [];
Then a separate file for my socket events
//in socketEvents.js
module.exports = (io) => {
io.on('connection', (socket) => {
console.log("Beautiful sockets are connected")
socket.once('disconnect', () => {
console.log("socket is disconnected");
});
//DOESN'T DO ANYTHING YET
socket.on('join global', (username) => {
socket.join(username);
console.log("New user in the global chat : ", username)
});
socket.on('new message', (msg) => {
console.log("The new message from sockets : ", msg);
socket.emit('new message', msg.content);
});
});
}
My sockets server side are linked up with the client. I'm just not seeing new messages in different instances. Is it because I'm not re-rendering after the server receives the message?
Thanks in advance, please let me know if you need me to clarify anything.
Cheers!
I figured it out... I'm going to leave this post up with a walkthrough in an attempt to help others who are having trouble with sockets. I may post a blog about it. Will update if I do.
So the code listens on the client side for a message to be sent inside of my onSubmitMessage function.
onSubmitMessage(event) {
event.preventDefault(); //prevents HTML <form> from going on its own post
const content = this.state.message;
//Create message object
const msg = {
content,
createdAt : new Date(),
userId : "one",
chatRoomId : "two"
}
//HERE'S THE IMPORTANT PART!!!
axios.post(`api/messages/`, msg)
.then(() => {
// wrapped in a promise, send a handler to server called
// ('new message') with the message object
this.newMessage(msg);
socket.emit('new message', msg);
})
.then(() => {
//Another promise then waits for the handler to come back from server
//*****IMPORTANT*************
//Then invoke newMessage function to get the post on all sockets
socket.on('message', (msg) => {
this.newMessage(msg);
})
})
};
Now on the server side this is what's happening:
// This is where the listener is for the client side handle
socket.on('new message', (msg) => {
// broadcast.emit will send the msg object back to client side and
// post to every instance expcept for the creator of the message
socket.broadcast.emit('message', msg);
});
SO the data path is (C) for client, (S) for server:
receive message object from user and -------->
(C)socket.emit('new message') -----> (S) socket.on('new message') -------> (S) socket.broadcast.emit('message') --------> (C)socket.on('message')
Back in the client side, I can invoke my newMessage function, which will set the message to state so I can display it.
I hope someone finds this useful! Surprisingly, this seems to go relatively unanswered on Stack. If anyone has any questions feel free to ask!