socketio / socket.io

Realtime application framework (Node.JS server)
https://socket.io
MIT License
61.16k stars 10.11k forks source link

socket.broadcast.emit calling itself also #4688

Closed ismailsemihsenturk closed 1 year ago

ismailsemihsenturk commented 1 year ago

Describe the bug socket.broadcast.emit("remoteAnswerForLocal", remoteSdp); The data is sent back to itself also.

To Reproduce Here's my repo for the reproduce: https://github.com/ismailsemihsenturk/React_Native_WebRTC_ChatApp you need at least 2 phone (1 emulator and 1 physical phone would do the work too). Please fill the following code example:

Socket.IO server version: x.y.z

Server

const express = require("express");
const app = express();
const server = require("http").Server(app);
const io = require("socket.io")(server);
const port = 3000;
const cors = require("cors");

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

// const server = createServer(app);
// const io = new Server(server, {
//     cors: {
//         origin: "http://localhost:3000"
//     }
// });

let localSdp = {
    id: "",
    localOffer: {
        offer: {
            sdp: "",
            type: "",
        },

    },
    remoteOffer: {
        offer: {
            sdp: "",
            type: "",
        },
    },
};

let remoteSdp = {
    id: "",
    localOffer: {
        offer: {
            sdp: "",
            type: "",
        },

    },
    remoteOffer: {
        offer: {
            sdp: "",
            type: "",
        },
    },
};

// socket.emit 1v1
// socket.broadcast.emit 1 to many except itself
// io.sockets.emit every client

io.on("connection", (socket) => {
    console.log("hello world!")

    socket.on("remote", (offerSdp) => {
        if (localSdp.id === offerSdp.id) {
            remoteSdp.id = offerSdp.id;
            remoteSdp.remoteOffer = localSdp.localOffer;
            remoteSdp.localOffer.offer = offerSdp.offer;
            localSdp.remoteOffer = remoteSdp.localOffer;
            socket.broadcast.emit("remoteAnswerForLocal", remoteSdp);
            console.log("remote: " + JSON.stringify(offerSdp, 0, 4));
            console.log("remoteAnswerForLocal:" + JSON.stringify(remoteSdp, 0, 4));
        }
    }); 

server.listen(port, () => console.log("server running on port:" + port));`

*Client*

```js
import { io } from "socket.io-client";

  const socket = useRef();
  const connectionConfig = {
    jsonp: false,
    reconnection: true,
    reconnectionDelay: 100,
    reconnectionAttempts: 100000,
    transports: ['websocket'],
  };
  socket.current = io(WEBSOCKET_URL, connectionConfig);

  socket.current.on("remoteAnswerForLocal", async (arg) => {
    console.log("remoteAnswerForLocal girdi");
    console.log("remoteAnsForLocal: " + JSON.stringify(arg, 0, 4));
    const offerDescription = new RTCSessionDescription(arg.localOffer);
    await peerConnection.current.setRemoteDescription(offerDescription);
  });

Expected behavior socket.broadcast.emit() shouldn't send the event to the itself.

Platform:

Additional context "dependencies": { "expo": "~48.0.10", "expo-crypto": "^12.2.2", "expo-dev-client": "^2.1.6", "expo-splash-screen": "~0.18.1", "expo-status-bar": "~1.4.4", "react": "18.2.0", "react-native": "0.71.6", "react-native-dotenv": "^3.4.8", "react-native-webrtc": "^111.0.0", "socket.io-client": "^4.6.1" }, "devDependencies": { "@babel/core": "^7.20.0" },

ismailsemihsenturk commented 1 year ago

I can workaround like that: Server socket.broadcast.emit("remoteAnswerForLocal", { remoteSdp: remoteSdp, socket: socket.id });

Client socket.current.on("remoteAnswerForLocal", async (arg) => { if (arg.socket !== socket.current.id) { } });

But a permanent solution would be better.

OlliePugh commented 1 year ago

I have taken a quick look at your repository, Socket.IO is working as expected, it isnt emitting back to the socket that sent it, the issue is due to the way you are connecting to the Socket.IO server, you make the connection in the component, but not inside of a useEffect, meaning that, everysingle time your app re-renders, it connects again with a new socket, and therefore new socket ID.

So you are emitting the event, which most likely triggers an app re-render (unsure, I haven't dived deep into your code)

Your app then creates a new socket, and your server responds to all sockets that didnt send the socket, which includes the new one you have just made.

Take a look at How to use with React, doesn't matter that youre using React Native, the principle is still the same.

Happy to give any further explinations.

image

ismailsemihsenturk commented 1 year ago

@OlliePugh thank you for your answer. I tried to implement the flow as the documentation suggest. Seems like issue is gone for now. I can work with the rooms and emit events to the clients except the sender. But this new implementation has caused another issue. Now the "socket.on" listeners in the client side triggers multiple times. Since i'am trying to build a webrtc chat app this cause triggering the pc.setLocalDescription(offer) function multiple times and i get this error "[Unhandled promise rejection: Error: Failed to set local offer sdp: The order of m-lines in subsequent offer doesn't match order from previous offer/answer.]" error. I know this error probably has nothing with socket.io but i wonder why the client side listeners fires multiple times. Did i implement the structure wrong? I provide some code and console output:

Server

const app = express();
const server = require("http").Server(app);
const io = require("socket.io")(server);
const port = 3000;
const cors = require("cors");`

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

io.sockets.on('connection', function (socket) {

    socket.on("create", (room) => {
        socket.join(room);
    });

    socket.on("start", (arg) => {
        console.log("room: " + JSON.stringify(arg, 0, 4));
        socket.to(arg.room).emit("setLocalOffer");
    });

    socket.on("getLocalOffer", (arg) => {
        console.log("getLocalOffer: " + JSON.stringify(arg, 0, 4));
        socket.to(arg.room).emit("setRemoteAnswer", { offerSdp: arg.offerSdp, room: arg.room });
    });

    socket.on("getRemoteAnswer", (arg) => {
        console.log("getRemoteAnswer: " + JSON.stringify(arg, 0, 4));
        socket.to(arg.room).emit("setLocalAnswer", { answerSdp: arg.offerSdp, room: arg.room });
    });

    socket.on("exchangeICECandidates", (arg) => {
        console.log("exchangeICECandidates: " + JSON.stringify(arg, 0, 4));
        socket.to(arg.room).emit("getICECandidates", { candidate: arg.candidate, room: arg.room });
    });

});

server.listen(port, () => console.log("server running on port:" + port));

Socket.js

import { io } from 'socket.io-client';
import { WEBSOCKET_URL } from '@env'

const connectionConfig = {
    jsonp: false,
    reconnection: true,
    reconnectionDelay: 100,
    reconnectionAttempts: 100000,
    transports: ['websocket'],
};
export const socket = io(WEBSOCKET_URL, connectionConfig);

Client

// Initiate The Call
  const startCall = async () => {
    let hostId = Crypto.randomUUID();
    roomTextInputRef.current.value = hostId;
    socket.emit("create", hostId);
    setRoomId(hostId);
    setIsCallCreated(!isCallCreated);
  };

  // Join The Remote Call
  const joinCall = async () => {
    setIsCallJoined(!isCallJoined);
    // Get the offer from signaling server and answer it
    socket.emit("create", roomId);
    socket.emit("start", { room: roomId });
  };

  socket.on("setLocalOffer", async (arg) => {
    console.log("setLocalOffer");

    const offerDescription = await peerConnection.current.createOffer();
    await peerConnection.current.setLocalDescription(offerDescription);

    socket.to(roomId).emit("getLocalOffer", { offerSdp: offerDescription, room: roomId });
  });

  socket.on("setRemoteAnswer", async (arg) => {
    console.log("setRemoteAnswer");

    const offerDescription = new RTCSessionDescription(arg.offerSdp);
    await peerConnection.current.setRemoteDescription(offerDescription);
    const answerDescription = await peerConnection.current.createAnswer();
    await peerConnection.current.setLocalDescription(answerDescription);

    socket.to(roomId).emit("getRemoteAnswer", { answerSdp: answerDescription, room: roomId });
  });

  socket.on("setLocalAnswer", async (arg) => {
    console.log("setLocalAnswer");
    const answerDescription = new RTCSessionDescription(arg.answerSdp);
    await peerConnection.current.setRemoteDescription(answerDescription);
  });

  socket.on("getICECandidates", (arg) => {
    console.log("getICECandidates");
    peerConnection.current.addIceCandidate(arg.candidate);
  });

CONSOLE OUTPUT error1

error2

OlliePugh commented 1 year ago

I see the rn-webrtc is debugging those messages, you have a similar issue with how you are adding event listeners, all of your peerConnection.current.addEventListeners are being ran for every render cycle, most likely adding multiple listeners, yuou are also creating a new new RTCPeerConnection(peerConstraints); every single render, I would advise having a read over the react life cycle docs as this you will need a fundamental understanding of this process before being able to work in React.

For the specifics of the issue you are facing now it may be what I have just mentioned, but it could be something else, your app.js file is doing so much its really quite hard to debug, try and make use of React's component style and break out all the different functionalities into their own components.

ismailsemihsenturk commented 1 year ago

Thanks. I turned back to the basics and its paid off. Now the program is working just fine.