DEV Community

Jen C.
Jen C.

Posted on

Set Up an Express.js Server with Socket.IO for Real-Time Client Communication

Server side

Features

  1. Express.js Application Setup with CORS enabled, HTTP server object to serve the Express.js application
  2. Event Handling for Disconnections
  3. Room Selection Handling
  4. Connection Event Listening
  5. Connection Error Handling

Explanations

apps\backend\src\index.ts

Create an Express.js application with CORS enabled, running on a specified port and served by an HTTP server:

const app = express();
const port = env.port;

app.use(cors());

// create a HTTP server object
const server = http.createServer(app);
Enter fullscreen mode Exit fullscreen mode

Initialize a new instance of socket.io:

const io = new Server(server, {
    cors: {
        origin: env.clientUrl,
        // credentials: true
    }
});
Enter fullscreen mode Exit fullscreen mode

Define the socket object type:

type SocketType = Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, Record<string, never>>;
Enter fullscreen mode Exit fullscreen mode

Declare custom rooms (an arbitrary channel that sockets can join and leave):

const customRooms = ["Room A", "Room B", "Room C"];
Enter fullscreen mode Exit fullscreen mode

Event handler for managing disconnections. Use io.of("/").sockets.size to get The number of currently connected clients, and emit the receive_online_people_count event to all connected clients, including the current one:

const handleDisconnect = (socket: SocketType) => () => {
    console.log('A user disconnected:', socket.id, 'io.of("/").sockets.size:', io.of("/").sockets.size);
    try {
        io.emit("receive_online_people_count", io.of("/").sockets.size);
    } catch (error) {
        console.error(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

A generic function to handle socket emit events: check if a client has joined an additional room beyond the default room (public channel) using socket.rooms.size > 1; if so, emit the event to that specific room, otherwise, emit the event to all connected clients, including the current one:

const handleEmit = <T extends Record<string, unknown> | string | number | boolean>(socket: SocketType, eventName: string) => (value: T) => {
    try {
        // NOTE: `socket.rooms.size > 1` because a socket is always in the public channel
        if (socket.rooms.size > 1) socket.rooms.forEach(room => socket.to(room).emit(eventName, value));  // TODO: `socket.to(room).emit` this is not able to send to the sender itself
        else io.emit(eventName, value);
    } catch (error) {
        console.error(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

Handle the clientโ€™s "choose room" event by defining a leaveRooms function that allows the current socket to leave all custom rooms (while remaining in the default public channel), and emit the receive_room_selected_value event with the rooms the current socket joins to the socket itself:

const handleSelectRoom = (socket: SocketType) => (roomId: string) => {
    console.log('user enter the room:', socket.id, `roomId: ${roomId}`);

    const leaveRooms = () => {
        socket.rooms.forEach(room => { if (customRooms.includes(room)) { socket.leave(room) } });
    }

    try {
        leaveRooms();  // NOTE: allow this socket to join only one room at a time
        if (roomId !== "public channel") socket.join(roomId);
        socket.emit("receive_room_selected_value", roomId, `socket: ${socket.id} is in the rooms: ${Array.from(socket.rooms).join(',')}`);
    } catch (error) {
        console.error(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

Listen for the connection event to handle incoming sockets, then use io.emit("receive_online_people_count", io.of("/").sockets.size); to broadcast the current number of connected clients:

io.on('connection', (socket: SocketType) => {
    console.log('a user connected', socket.id);

    io.emit("receive_online_people_count", io.of("/").sockets.size);

    socket.on("send_msg", handleEmit(socket, 'receive_msg'));
    socket.on("dropdown_selected_value", handleEmit(socket, 'receive_dropdown_selected_value'));
    socket.on("checkbox_is_checked", handleEmit(socket, 'receive_checkbox_is_checked'));
    socket.on("radio_selected_value", handleEmit(socket, 'receive_radio_selected_value'));
    socket.on("textarea_value", handleEmit(socket, 'receive_textarea_value'));
    socket.on("map_position", handleEmit(socket, 'receive_map_position'));
    socket.on("room_selected_value", handleSelectRoom(socket));

    socket.on("disconnect", handleDisconnect(socket));
});
Enter fullscreen mode Exit fullscreen mode

Listen to connection errors and handle them:

io.engine.on("connection_error", (err) => {
    console.log(err.req);      // the request object
    console.log(err.code);     // the error code, for example 1
    console.log(err.message);  // the error message, for example "Session ID unknown"
    console.log(err.context);  // some additional error context
});
Enter fullscreen mode Exit fullscreen mode

Expose the port:

server.listen(port, () => {
    console.log(`Listening on port ${port}...`);
});
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ‰ Check out the code on GitHub

Top comments (0)