Most of the internet is built around the request/response abstraction.
But there is also other nice philosophies like pub/sub.
One of major benefits of pub/sub is the active role publishers have. On request/response paradigm, the server only answers when someone asks for something (a page, image, etc). Publishers on the other hand know who is interested in a certain topic and send updates to these subscribers whenever new content arrives at the topic.
On this post we'll make a small chat app game to demonstrate how to use
the socket.io node library which implements (and even
extends) the publish/subscribe pattern.
It's a simple npm project, see the package.json
:
{
"name": "sample-rtc-socket-io-server",
"version": "1.0.0",
"description": "sample project showcasing real time communication with socket.io",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"nodemon": "^1.18.10"
},
"dependencies": {
"randomcolor": "^0.5.4",
"socket.io": "^2.2.0"
}
}
As usual we adopt nodemon here for faster development purposes.
On socket.io idiom uses two concepts which represents the idea of the topic in the pub/sub architecture where subscribers can join and observe.
The first one is the namespace.
Every client will connect into the root ("/") namespace.
The second one is the room.
Then everything becomes an emit/observe architecture. Either someone is emitting an event or listening to it.
This is our game server:
// node index.js
const io = require("socket.io")(3001);
const rnd = require("randomcolor");
let guests = [];
io.of("/lobby").on("connect", socket => {
socket.on("new-guest", (guest, ack) => {
guest.sId = socket.id;
guest.score = 0;
guest.fill = rnd({luminosity: "dark"});
guest.x = Math.random() * 800;
guest.y = Math.random() * 600;
guest.radius = 15;
guest.stroke = "black";
guest.strokeWidth = 4;
guests.push(guest);
io.of("/lobby").emit("list-players", guests);
ack(guest);
});
socket.on("move-to", pos => {
const [guest] = guests.filter(e => e.sId == pos.sId);
if (guest) {
if (guest.x - pos.layerX > 20) guest.x -= 15;
if (guest.x - pos.layerX < 20) guest.x += 15;
if (guest.y - pos.layerY > 20) guest.y -= 15;
if (guest.y - pos.layerY < 20) guest.y += 15;
io.of("/lobby").emit("list-players", guests);
}
});
socket.on("bye-bye", _ => {
guests = guests.filter(e => e.sId != socket.id);
io.of("/lobby").emit("list-players", guests);
});
});
console.log("server online");
There is a namespace called /lobby and the rtc server is listening on port 3001. The server itself listens to connect events and once the subscriber connects three more custom events may happen:
Also it's possible to notice that the rtc server emits a custom event too:
Not your regular express server :-)
It's a simple npm project generated by vue-cli tool. See the package.json
:
{
"name": "sample-rtc-socket-io-client",
"version": "1.0.0",
"description": "sample project showcasing real time communication with socket.io",
"main": "index.js",
"scripts": {
"dev": "vue-cli-service serve --open",
"build": "vue-cli-service build"
},
"dependencies": {
"konva": "^2.6.0",
"socket.io-client": "^2.2.0",
"vue": "^2.6.6",
"vue-konva": "^2.0.2",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"vue-template-compiler": "^2.5.21"
},
"keywords": [],
"author": "",
"license": "ISC",
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
The vue cli offers first class support for development time and is quite flexible, allowing tweaks on every piece of configuration.
We adopt vuex there in order to better decouple data state management from the view component.
Also it has the plus to make rtc-ish interactions transparent to the remaining components, like the vue-konva, which we adopted to draw things more easily in our game.
The App.vue has no knowledge about the server side:
<template>
<div>
<div class="box">
<v-stage @click="handleClick" :config="{width,height}">
<v-layer>
<v-circle v-for="g in $store.state.players" :key="g.sId" :config="g"></v-circle>
</v-layer>
</v-stage>
</div>
</div>
</template>
<script>
export default {
name: "catch-game",
data: _ => ({
width: 800,
height: 600
}),
created() {
this.res();
window.addEventListener("resize", this.res);
},
methods: {
res() {
this.width = window.innerWidth;
this.height = window.innerHeight;
},
handleClick(e) {
const { layerX, layerY } = e.evt;
const { sId, x, y } = this.$store.state.guest;
this.$store.commit("moveTo", { sId, layerX, layerY, x, y });
}
}
};
</script>
It's a very ordinary vue component.
Now let's see our store.js:
import Vue from "vue";
import Vuex from "vuex";
import io from "socket.io-client";
Vue.use(Vuex);
export const sock = io("127.0.0.1:3001/lobby");
const guest = {name: `Guest_${new Date().getTime()}`};
const players = [];
const store = new Vuex.Store({
state: {sock, guest, players},
mutations: {
updateGuest(state, guest) {
state.guest = guest;
},
updatePlayers(state, players) {
state.players = players;
},
moveTo(state, pos) {
sock.emit("move-to", pos);
},
},
});
sock.emit("new-guest", guest, g => {
store.commit("updateGuest", g);
});
sock.on("list-players", players => {
store.commit("updatePlayers", players);
});
window.addEventListener("beforeunload", _ => {
sock.emit("bye-bye");
});
export default store;
What is happening here:
The connection with the rtc server and it's namespace happens on the following line:
export const sock = io("127.0.0.1:3001/lobby");
on socket.io, connection establishment is automatic, once it connects it emits the connect event which the server already awaits.
Just after the connection gets created, the client emits the new-guest custom event:
sock.emit("new-guest", guest, g => {
store.commit("updateGuest", g);
});
Emitters can hold a third argument which is the ack function, if you observe the server counterpart of this event, the ack function is used back to return an augmented version of the newly-created guest.
Store and rtc client play quite well each other and the store mutations are used to propagate data easily to all components accessible to it.
Real time communications are quite simple when using socket.io and very understandable after reading the docs.
Hope you find this article useful, and as usual the source code can be found here.