Socket.IO provides communication between web clients and Node.js servers in real time. For many use cases today, developers need to constantly update information for their applications in real time, which necessitates the use of a bi-directional communication tool that can keep data updated instantly.
In this article, we’ll look at how you can use Socket.IO with Node.js for real-time communication of data for your application for use cases like this.
Jump ahead:
- REST API vs WebSockets
- Setting up an Express.js server
- Setting up WebSockets with Socket.IO
- Authentication and authorization
- Sharing location between users
- Emitting and receiving events
REST API vs WebSockets
Traditional REST APIs are at their most useful when we want to retrieve a resource and don’t need constant ongoing updates.
If you look at a crypto trade, for example, when we place a bid to buy a coin, the bid is unlikely to change very often, so we can model this behavior accurately using a traditional REST API.
However, the actual price of a coin itself is very volatile, as it responds to market trends, like any asset. For us to get the most recent price, we would need to initiate a request and it’s highly probable that it will have changed again just as our response arrives!
In such a scenario, we need to be notified as the price of the asset changes, which is where WebSockets shine.
The resources used to build traditional REST APIs are highly cacheable because they are rarely updated. WebSockets, meanwhile, don’t benefit as much from caching, as this may have negative effects on performance.
There are extensive guides that highlight the different use cases for both traditional REST APIs and WebSockets.
In this guide, we be building a real-time, location-sharing application using Node.js and Socket.IO as our use case.
Here are the technologies we will use :
- Node.js/Express: For our application server
- Socket.IO: Implements WebSockets under the hood
- Postgres: The database of choice to store user location data
- Postgis: This extension makes it possible to work with locations in the database and provides additional functions, like calculating the distance around a location
Setting up an Express.js server
We will start by setting up an Express application.
For this, I have modified a boilerplate that we will use. Let’s follow these instructions to get started:
- Clone this GitHub repository
cd socket-location/
npm install
- Finally, create an
.env
file in the root and copy the contents of theenv.
sample file. To get the values, we would actually need to set up a local database or use an online Postgres database platform like Elephant or Heroku. Visit the platform and create a database for free, get the URL, fill in the credentials, and start the application
Setting up WebSockets with Socket.IO
To set up Sockets.IO with our existing starter files, it’s important for us to use JavaScript Hoisting, which enables us to make the socket instance available across different files.
We will start by installing Socket.IO
npm install socket.io
The next thing we need to do is integrate it with our express application. Open the app.js
file in the root of the project directory and edit it in the following way:
const express = require("express"); const { createServer } = require("http"); const { Server } = require("socket.io"); const { initializeRoutes } = require("./routes"); let app = express(); const port = 3000; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app = initializeRoutes(app); app.get("/", (req, res) => { res.status(200).send({ success: true, message: "welcome to the beginning of greatness", }); }); const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"], }, }); io.on("connection", (socket) => { console.log("We are live and connected"); console.log(socket.id); }); httpServer.listen(port, () => { console.log(`Example app listening on port ${port}`); });
We have successfully wired our server to work with sockets; now we need to wire our client to respond to this connection so that there can be communication back and forth.
Our client can be a web application, a mobile application, or any device that can be configured to work with WebSockets.
We will be mimicking the client behavior in this application with POSTMAN for the sake of brevity and simplicity.
To do this, do the following:
- Open up Postman and click on the “New” button in the top-left corner
- Then click the “Websocket Request” button on the pop-up
- There should be text that says “raw” — use the drop down and change it to “Socket.IO”
- Paste your connection string on the tab — in this case, localhost:3000 — and click the connect button
You should see a unique ID printed out to the console and the Postman tab should say “Connected”.
Here is a video that shows where we are and a recap of what we have done so far.
Library | Loom – 10 October 2022
No Description
Authentication and authorization
Now that we have successfully connected to our Socket.IO instance, we need to provide some form of authentication and authorization to keep unwanted users out. This is especially the case when you consider that we are allowing connections from any client, as specified by the *
in our CORS option when connecting.
To do this, we will be using this middleware:
io.use((socket, next) => { if (socket.handshake.headers.auth) { const { auth } = socket.handshake.headers; const token = auth.split(" ")[1]; jwt.verify(token, process.env.JWT_SECRET_KEY, async (err, decodedToken) => { if (err) { throw new Error("Authentication error, Invalid Token supplied"); } const theUser = await db.User.findByPk(decodedToken.id); if (!theUser) throw new Error( "Invalid Email or Password, Kindly contact the admin if this is an anomaly" ); socket.theUser = theUser; return next(); }); } else { throw new Error("Authentication error, Please provide a token"); } });
First, we are checking if a token is provided in the request header. We are also checking if it is a valid token and that the user is present in our database before allowing them to connect — this ensures only authenticated users have access.
Sharing location between users
To share locations between users, we will be using socket connections. We will also need to prepare our database to accept geometry objects, and to do this we will install the PostGIS extension on our database. On the client, we will be using the JavaScript Geolocation API.
First, we will install the PostGIS extension on our database.
Run this command at the root of the project:
npx sequelize-cli migration:generate --name install-postgis
Then, open the migration file generated and paste the following:
"use strict"; /** @type {import('sequelize-cli').Migration} */ module.exports = { up: (queryInterface, Sequelize) => { return queryInterface.sequelize.query("CREATE EXTENSION postgis;"); }, down: (queryInterface, Sequelize) => { return queryInterface.sequelize.query("DROP EXTENSION postgis CASCADE;"); }, };
The above code snippet will install the postgis
extension on our database instance when we run migration.
npm run migrate
(Note: As of writing, I couldn’t successfully run migration to install PostGIS extension on the Postgres instance provided by ElephantSQL as I had permission issues, but I was able to successfully run the same on a Heroku instance)
Next, we need to prepare our database to store user location information, and to do this, we will create another migration file:
npx sequelize-cli model:generate --name Geolocation --attributes socketID:string,location:string
Delete the contents and paste the following:
"use strict"; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up(queryInterface, Sequelize) { await queryInterface.createTable("Geolocations", { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER, references: { model: "Users", key: "id", as: "id", }, }, socketID: { type: Sequelize.STRING, unique: true, }, location: { type: Sequelize.GEOMETRY, }, online: { type: Sequelize.BOOLEAN, }, trackerID: { type: Sequelize.INTEGER, references: { model: "Users", key: "id", as: "id", }, }, createdAt: { allowNull: false, type: Sequelize.DATE, }, updatedAt: { allowNull: false, type: Sequelize.DATE, }, }); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable("Geolocations"); }, };
…and then we run migrations again:
npm run migrate
We also want to create a relationship between the two models, so in models/user.js, add the following code. Here we are creating a one-to-one relationship between the user model and the Geolocation model.
... static associate(models) { // define association here this.hasOne(models.Geolocation, { foreignKey: "id" }); } ...
Then, in models/geolocation.js, add:
.... static associate(models) { // define association here this.belongsTo(models.User, { foreignKey: "id" }); } ....
Next, we need to build the actual route that facilitates the connection. To do this, go into routes
and create /track/track.js
. Then, add the following:
const { Router } = require("express"); const db = require("../../models"); const { handleJwt } = require("../../utils/handleJwt"); const trackerRouter = Router(); trackerRouter.post( "/book-ride", handleJwt.verifyToken, async (req, res, next) => { // search for user that is offline // assign the booker id to the const { user, body: { location }, } = req; //returns the first user that meets the criteria const user2 = await db.User.findOne({ where: { role: "driver" }, }); db.Geolocation.update( { trackerID: user2.id, online: true, }, { where: { id: user.id }, returning: true } ); db.Geolocation.update( { trackerID: user.id, location: { type: "Point", coordinates: [location.longitude, location.latitude], }, online: true, }, { where: { id: user2.id }, returning: true } ); if (!user2) return res.status(404).send({ success: false, message, }); return res.status(200).send({ success: true, message: "You have successfully been assigned a driver", }); } ); module.exports = { route: trackerRouter, name: "track" };
In addition, we also want to make sure that we every time we sign up a user, we also create a geolocation object for them.
Add this just before you send a response when creating a user in the routes/user/user.js file:
.... await db.Geolocation.create({ id: user.id }); const token = handleJwt.signToken(user.dataValues); .....
if you now open POSTMAN up and send the request, you should get the response that you have successfully been assigned a driver.
Emitting and receiving events
We will be sending and receiving objects that represent the geographical position of a user, which we will do on the web via the Geolocation API.
It is quite simple to work with and it is very accurate for the vast majority of modern consumer devices like smartphones and laptops, as they have GPS built in.
When we log out the object that captures this information, we have:
position: { coords: { accuracy: 7299.612787273156 altitude: null altitudeAccuracy: null heading: null latitude: 6.5568768 longitude: 3.3488896 speed: null }, timestamp: 1665474267691 }
If you visit w3schools with your mobile phone and walk around while looking at the longitude and latitude information, you will discover that your location is constantly changing to reflect where you are in real time.
This is because your new location details are being fired as you are moving! There is other information captured in the position object, which includes speed as well as altitude.
Now, let’s create socket handlers that will actually emit and listen to these events between the user and drivers.
in our app.js, edit the following line to look like this:
const { onConnection } = require("./socket"); .... io.on("connection", onConnection(io)); ....
Then, we will create a socket directory in the root of our application and add the following files:
driversocket.js
const { updateDbWithNewLocation } = require("./helpers"); const db = require("../models"); const hoistedIODriver = (io, socket) => { return async function driverLocation(payload) { console.log(`driver-move event has been received with ${payload} `); const isOnline = await db.Geolocation.findByPk(payload.id); if (isOnline.dataValues.online) { const recipient = await updateDbWithNewLocation(payload, isOnline); if (recipient.trackerID) { const deliverTo = await db.Geolocation.findOne({ where: { trackerID: recipient.trackerID }, }); const { socketID } = deliverTo.dataValues; io.to(socketID).emit("driver:move", { location: recipient.location, }); } } }; }; module.exports = { hoistedIODriver };
The above code handles updating the database with the driver location and broadcasting it to the listening user.
usersocket.js
const { updateDbWithNewLocation } = require("./helpers"); const db = require("../models"); const hoistedIOUser = (io, socket) => { return async function driverLocation(payload) { console.log( `user-move event has been received with ${JSON.stringify(payload)} ` ); const isOnline = await db.Geolocation.findByPk(payload.id); if (isOnline.dataValues.online) { const recipient = await updateDbWithNewLocation(payload, isOnline); if (recipient.trackerID) { const deliverTo = await db.Geolocation.findOne({ where: { trackerID: recipient.trackerID }, }); const { socketID } = deliverTo.dataValues; io.to(socketID).emit("user:move", { location: recipient.location, }); } } }; }; module.exports = { hoistedIOUser };
The above code handles updating the database with the user location and broadcasting it to the listening driver.
index.js
const { hoistedIODriver } = require("./driversocket"); const { hoistedIOUser } = require("./usersocket"); const configureSockets = (io, socket) => { return { driverLocation: hoistedIODriver(io), userLocation: hoistedIOUser(io), }; }; const onConnection = (io) => (socket) => { const { userLocation, driverLocation } = configureSockets(io, socket); socket.on("user-move", userLocation); socket.on("driver-move", driverLocation); }; module.exports = { onConnection };
… and finally:
helpers.js
const db = require("../models"); const updateDbWithNewLocation = async (payload, oldGeoLocationInfo) => { const { id, socketID } = payload; const [, [newLocation]] = await db.Geolocation.update( { online: oldGeoLocationInfo.online, socketID, trackerID: oldGeoLocationInfo.trackerID, location: { type: "Point", coordinates: [payload.coords.longitude, payload.coords.latitude], }, }, { where: { id }, returning: true } ); return newLocation; }; module.exports = { updateDbWithNewLocation };
To get the working code sample, kindly visit the GitHub repository and clone and navigate between the branches.
Alright, let’s take this for a spin and see this in action!
Library | Loom – 11 October 2022
No Description
Conclusion
Thanks for following along with this tutorial on using Node.js and Socket.IO to build a real-time location app.
We were able to demonstrate how with basic but common tools we can begin to implement the functions of a location tracking app with WebSockets.
The post Building a real-time location app with Node.js and Socket.IO appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/SZ1CTlp
Gain $200 in a week
via Read more