Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Socket.io opening multiple connections with React-Native

I built a server with an API. It uses Axios for the un-logged calls and Socket.io for the logged calls. Then I have a website connected to it. And that works perfectly. But I have also an application built in react-native which has a strange behavior: it opens connections on every emit without closing them previous connections. As you can see below, I console.log the websocket.engine.clientsCount on the server. Every time I emit from the phone-application it opens a new connection, finding the server with an increasing number of it.

enter image description here

On server, I user following versions:

"connect-mongo": "^1.3.2",
"express": "^4.14.1",
"express-session": "^1.12.1",
"jwt-simple": "^0.5.1",
"mongodb": "^2.2.30",
"mongoose": "^4.11.5",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
"socket.io": "^1.7.3",
"socketio-jwt": "^4.5.0"

Here the code of the API. I removed some code for clarity.

const passport = require('passport');
const express = require('express');
const session = require('express-session');
const http = require('http');
const morgan = require('morgan');
const mongoose = require('mongoose');
const socketio = require('socket.io');
const bodyParser = require('body-parser');
const socketioJwt   = require("socketio-jwt"); // da commentare
const Users = require('../models/users');

const passportService = require('./services/passport');

const requireAuth = passport.authenticate('jwt', {session: false});
const requireLogin = passport.authenticate('local', {session: false});
const config = require('./config');

const app = express();
const socketRouter = require('./services/socketRouter');
const MongoStore = require('connect-mongo')(session);

const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost/blablabla';
mongoose.connect(mongoUri);
...
const server = http.Server(app);
const websocket = socketio(server);

// add authorization for jwt-passport when first connection -> https://github.com/auth0/socketio-jwt
websocket.use(socketioJwt.authorize({
  secret: config.secret,
  handshake: true
}));

const sessionMiddleware = session({
  store: new MongoStore({ // use MongoDb to store session (re-using previous connection)
    mongooseConnection: mongoose.connection,
    ttl: (1 * 60 * 60)
  }),
  secret: config.secretSession,
  httpOnly: true,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 86400000 }
});
app.use(sessionMiddleware);
...
websocket.on('connection', (socket) => {
    Users.findById(socket.decoded_token.sub, function(err, user) {
        if (err) { console.log('the user wasn\'t find in database', err); }
        if (user) {
            socket.join(user._id);
            console.log('Clients connected: ', websocket.engine.clientsCount);

            // ------ PROTECTED EVENTS ------ //
            ...
            // ------------------------------ //

        }

        socket.on('disconnect', ()=> {
          socket.leave(user._id);
          onsole.log('user disconnected');
        });
    });
});
...

I won't put the initialisation of the website because it works good.

On mobile application, I user following versions:

"react-native": "^0.41.0",
"react-native-keychain": "^1.1.0",
"socket.io-client": "^1.7.3",
"socketio-jwt": "^4.5.0"

Here is the innitialisation of the react-native application.

import * as Keychain from 'react-native-keychain';
import { BASIC_WS_URL } from '../api';

const io = require('socket.io-client/dist/socket.io');
const socketEvents = require('./events');

exports = module.exports = (store) => {
  Keychain.getGenericPassword().then((credentials) => {
    if (credentials && credentials !== false) {
      const { password } = credentials;
      const websocket = io(BASIC_WS_URL, {
        jsonp: false,
        transports: ['websocket'], // you need to explicitly tell it to use websockets
        query: {
          token: password
        }
      });

      websocket.connect();
      websocket.on('connect', (socket) => {
        console.log('Connected');
      });

      websocket.on('reconnect', (socket) => {
        console.log('Re-connected');
      });

      websocket.on('disconnect', (socket) => {
        console.log('Disconnected');
      });
      // all the events to listen
      socketEvents(websocket, store);
    }
  });
};

What al I doing wrong?

like image 872
Pibo Avatar asked Mar 03 '26 16:03

Pibo


1 Answers

So here I come with an answer. I'll try to leave an answer as the one I'd like to find. A sort of tutorial about how to include Socket.io in React-native. Please, if you know a better solution, write it down. As I wrote, the problem is that socket in React-Native is global and in this way I wrong implementation is just more evident. First of all, I initialized socket in the wrong place. The correct place I found is in App.js, where the router is. I remove some code for clarity.

// important to access the context of React
import PropTypes from 'prop-types';
// Library used to save encrypted token
import * as Keychain from 'react-native-keychain';
// url to address
import { BASIC_WS_URL } from '../../api';// library to encrypt and decrypt data
const io = require('socket.io-client/dist/socket.io');

prepare this function within contructor and componentDidMount:

  state = {}
  setStateAsync(state) {
    return new Promise((resolve) => {
      this.setState(state, resolve)
    });
  }

keichain is a promise, so it won't work in componentDidMount. To make it work, you have to do the following, so every step will wait for the previous to be done:

async componentWillMount() {
  const response = await Keychain.getGenericPassword();
  const websocket = await io(BASIC_WS_URL, {
    jsonp: false,
    // forceNew:true,
    transports: ['websocket'],
    query: {
      token: response.password
    }
  });
  await websocket.connect();
  await websocket.on('connect', (socket) => {
    console.log('Sono -> connesso!');
  });
  await websocket.on('reconnect', (socket) => {
    console.log('Sono riconnesso!');
  });
  await websocket.on('disconnect', (socket) => {
    console.log('Sono disconnesso!');
  });
  await websocket.on('error', (error) => {
    console.log(error);
  });
// a function imported containing all the events (passing store retrieved from context declare at the bottom)
  await socketEvents(websocket, this.context.store);

  // then save the socket in the state, because at this point the component will be already rendered and this.socket would be not effective
  await this.setStateAsync({websocket: websocket});
}

Remember to remove the console.logs then. They are there just for verification. Right after this, remeber to disconnect when unmounting:

  componentWillUnmount() {
    this.state.websocket.disconnect()
  }

And right after this, save the socket in the context:

  getChildContext() {
    return {websocket: this.state.websocket};
  }

Remeber to declare the context at the bottom of the component:

App.childContextTypes = {
  websocket: PropTypes.object
}
// access context.type to get the store to pass to socket.io initialization
App.contextTypes = {
  store: PropTypes.object
}

So, final result is this:

  ...
    // important to access the context of React
    import PropTypes from 'prop-types';
    // Library used to save encrypted token
    import * as Keychain from 'react-native-keychain';
    // url to address
    import { BASIC_WS_URL } from '../../api';// library to encrypt and decrypt data
    const io = require('socket.io-client/dist/socket.io');
...


class App extends Component {
  constructor() {
    super();
    ...
  }
  state = {}
  setStateAsync(state) {
    return new Promise((resolve) => {
      this.setState(state, resolve)
    });
  }
  // set the function as asynchronous
  async componentWillMount() {
    //retrieve the token to authorize the calls
    const response = await Keychain.getGenericPassword();
    // initialize the socket connection with the passwordToken (wait for it)
    const websocket = await io(BASIC_WS_URL, {
      jsonp: false,
      // forceNew:true,
      transports: ['websocket'], // you need to explicitly tell it to use websockets
      query: {
        token: response.password
      }
    });
    // connect to socket (ask for waiting for the previous initialization)
    await websocket.connect();
    await websocket.on('connect', (socket) => {
      console.log('Sono -> connesso!');
    });
    await websocket.on('reconnect', (socket) => {
      console.log('Sono riconnesso!');
    });
    await websocket.on('disconnect', (socket) => {
      console.log('Sono disconnesso!');
    });
    await websocket.on('error', (error) => {
      console.log(error);
    });
// a function imported containing all the events
    await socketEvents(websocket, this.context.store);
    await this.setStateAsync({websocket: websocket});
  }
  componentWillUnmount() {
    this.state.websocket.disconnect()
  }
  getChildContext() {
    return {websocket: this.state.websocket};
  }
  render() {
    return (
    ... // here goes the router
    );
  }
}
App.childContextTypes = {
  websocket: PropTypes.object
}
// access context.type to get the store to pass to socket.io initialization
App.contextTypes = {
  store: PropTypes.object
}
export default App;

then, in any page/container, you can do like this. -> declare the context in the bottom of the component:

Main.contextTypes = {
  websocket: PropTypes.object
}

And when you dispatch an action, you will be able then to emit:

this.props.dispatch(loadNotif(this.context.websocket));

In the action creator, you will emit like this:

exports.loadNotif = (websocket) => {
  return function (dispatch) {
    // send request to server
    websocket.emit('action', {par: 'blablabla'});
  };
};

I hope it will help somebody.

like image 92
Pibo Avatar answered Mar 05 '26 06:03

Pibo