Do you want to see WebSockets and Socket.IO in action, or do you just want to play Snake?click here!

What Are WebSockets?

WebSockets are a communication protocol that enables real-time, bidirectional communication between a client (e.g., a web browser) and a server. Unlike traditional HTTP requests, which follow a request-response pattern, WebSockets allow a persistent connection where both client and server can send and receive data at any time. This makes WebSockets ideal for applications that require low latency and real-time updates.

Why Use WebSockets?

WebSockets are commonly used in scenarios where instant data exchange is required. Some practical applications include:

  • Online multiplayer games – Keep players synchronized in real time.
  • Chat applications – Enable seamless communication between users.
  • Live stock market updates – Push real-time price changes.

With WebSockets, there is no need for constant HTTP requests (polling), which reduces latency, improves performance, and saves bandwidth.

How Do WebSockets Work?

WebSockets operate over the ws:// (or wss:// for secure connections) protocol. They start as a standard HTTP connection but leverage the HTTP Upgrade mechanism to switch from an HTTP request-response cycle to a persistent WebSocket connection.

The WebSocket Handshake Process

  1. The client sends an HTTP request with an Upgrade header:

    GET /chat HTTP/1.1
    Host: mojalab.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13
    
  2. The server responds with:

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    
  3. Once established, the connection remains open, and both client and server can send messages freely.

What Is Socket.IO?

While WebSockets provide the underlying protocol, Socket.IO is a popular library that simplifies working with WebSockets. However, it is not a direct WebSocket implementation. If you use Socket.IO on the client side, you must also use it on the server side.

Socket.IO provides several additional features, such as:

  • Automatic reconnections – If the connection is lost, Socket.IO attempts to reconnect automatically.
  • Fallback to HTTP long polling – If WebSockets are not supported, it gracefully falls back to another method.
  • Simple event-based communication – Define custom events to structure your communication.
  • Room and namespace support – Easily manage multiple WebSocket connections within a single server.
  • WebTransport support – A protocol framework that enables efficient data transfer over HTTP/3, offering more advanced capabilities than WebSockets.

Socket.IO has both server-side and client-side implementations available in multiple programming languages.

How Does Socket.IO Help?

Instead of writing complex WebSocket code manually, Socket.IO allows developers to work with WebSockets in an easier, more structured way.

Building a WebSocket-Based Application with Flask-SocketIO and Nginx

Now that we start having a clear understanding of what WebSockets and Socket.IO are, let’s dive into a real-world example.

We will create a real-time WebSocket-based Snake game where the player competes against a bot. The architecture consists of:

  • Flask – A lightweight Python web framework.
  • Flask-SocketIO – WebSockets support for Flask applications.
  • Eventlet – A WSGI server used for async networking in Flask-SocketIO.
  • Nginx – Serves static files and acts as a reverse proxy.
  • Docker & Docker Compose – For containerized deployment.

If you want to explore the complete code and set up this WebSocket-based project yourself, you can find it on GitHub:

👉 GitHub Repository

Don’t want to set up the project? No problem! You can play with a live example directly here:

👉 WebSockets in Action

Directory Structure

websocket-example/
├── backend/              # Flask + Socket.IO WebSocket server
│   ├── app.py
│   ├── requirements.txt
│   ├── Dockerfile
├── nginx/                # Nginx configuration + Frontend
│   ├── default.conf
│   ├── index.html
├── docker-compose.yml    # Manages backend + nginx containers
├── start.sh              # Start script
├── stop.sh               # Stop script
├── README.md            	

Step 1: Implementing the Flask WebSocket Backend

The backend of our Socket.IO-based Snake game is built using Flask and Flask-SocketIO.

This implementation allows real-time bidirectional communication between the client (the browser) and the server, allowing each connected player to update the game state dynamically.

We use Flask-SocketIO with Eventlet to handle WebSocket connections efficiently in an asynchronous environment. The backend is responsible for:

  • Managing multiple game sessions independently
  • Handling player and AI movements
  • Detecting collisions (wall, self, and opponent)
  • Sending real-time updates to the frontend

Initializing Flask and Socket.IO

We start by creating a Flask application and initializing Flask-SocketIO with eventlet as the async mode:

import eventlet
eventlet.monkey_patch()  # Patch standard library for async support
from flask import Flask, request, Blueprint
from flask_socketio import SocketIO, emit
import random
import time
import threading
from logger import logging

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")

Game Logic: Managing Game Sessions

Each connected player gets their own independent game session. The game state is stored in a dictionary, with the session ID as the key:

games = {}  # { session_id: game_state }
game_threads = {}  # { session_id: game_loop_thread }

Initializing a New Game

When a new player connects, the server initializes a game session:

def reset_game(session_id):
    logging.info(f"🔄 Resetting Game State for {session_id}")
    games[session_id] = {
        "player_snake": [(5, 5)],
        "ai_snake": [(10, 10)],
        "food": (random.randint(0, 19), random.randint(0, 19)),
        "player_direction": "right",
        "ai_direction": "left",
        "running": False
    }

Handling Player and AI Movement

Each frame, the snakes move based on their current direction. The server checks for collisions and updates the game state accordingly.

def move_snake(snake, direction, game_state, session_id, is_player=False):
    dx, dy = {"up": (0, -1), "down": (0, 1), "left": (-1, 0), "right": (1, 0)}[direction]
    new_head = (snake[-1][0] + dx, snake[-1][1] + dy)

    # Wall collision
    if new_head[0] < 0 or new_head[0] >= 20 or new_head[1] < 0 or new_head[1] >= 20:
        socketio.emit("game_over", {"winner": "AI" if is_player else "Player"}, room=session_id)
        reset_game(session_id)
        return

    # Self-collision
    if new_head in snake:
        socketio.emit("game_over", {"winner": "AI" if is_player else "Player"}, room=session_id)
        reset_game(session_id)
        return

    # Collision with the opponent
    opponent_snake = game_state["ai_snake"] if is_player else game_state["player_snake"]
    if new_head in opponent_snake:
        socketio.emit("game_over", {"winner": "AI" if is_player else "Player"}, room=session_id)
        reset_game(session_id)
        return
    
    snake.append(new_head)
    if new_head != game_state["food"]:
        snake.pop(0)

WebSocket Events

Handling Player Actions

When a player moves, the server updates their direction:

@socketio.on("player_move", namespace="/snake_game")
def handle_player_move(data):
    session_id = request.sid
    if session_id in games and data["direction"] in ["up", "down", "left", "right"]:
        games[session_id]["player_direction"] = data["direction"]

Starting the Game Loop

Once the player starts the game, a separate thread updates the game state in real time:

@socketio.on("start_game", namespace="/snake_game")
def start_game():
    session_id = request.sid
    if session_id in games and not games[session_id]["running"]:
        games[session_id]["running"] = True
        game_threads[session_id] = threading.Thread(target=game_loop, args=(session_id,), daemon=True)
        game_threads[session_id].start()

Updating the Game State

The game loop runs continuously, moving both the player and AI snakes:

def game_loop(session_id):
    while session_id in games and games[session_id]["running"]:
        move_snake(games[session_id]["player_snake"], games[session_id]["player_direction"], games[session_id], session_id, is_player=True)
        move_snake(games[session_id]["ai_snake"], "right", games[session_id], session_id, is_player=False)
        socketio.emit("game_update", games[session_id], room=session_id)
        eventlet.sleep(0.5)

Running the Server

Finally, we start the Flask-SocketIO server:

if __name__ == "__main__":
    logging.info("🔥 Starting Flask WebSocket Server on port 5050...")
    socketio.run(app, host="0.0.0.0", port=5050, debug=False)

So, right now, we had:

  • Set up a Flask and Flask-SocketIO server
  • Implemented real-time player and AI movement
  • Handled collision detection and game state updates
  • Sent WebSocket events to communicate with the frontend

Step 2: Implementing the Frontend

Now that our backend is running running with Flask and Socket.IO, it's time to build the frontend that will communicate with it. Our frontend will be a simple HTML5 + JavaScript page that connects to our WebSocket server and allows users to play a game of Snake against a basic bot.

Setting Up the HTML Structure

The core of our frontend is a simple HTML page. It includes:

  • A canvas element where the game will be drawn.
  • A set of controls for starting and resetting the game.
  • A score display to show the length of the snakes.
  • A legend to indicate what each color represents.

Here’s the basic structure:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Snake - WebSocket Demo</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.2/socket.io.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            background-color: #f4f4f4;
        }
        canvas {
            background-color: white;
            border: 2px solid black;
        }
        .game-controls {
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <h2>🐍 Play Snake vs "AI"</h2>
    <p>Use arrow keys to move the snake and compete against the AI in real-time.</p>
    <canvas id="gameCanvas" width="400" height="400"></canvas>
    <div class="game-controls">
        <button onclick="startGame()">▶️ Start Game</button>
        <button onclick="resetGame()">🔄 Reset Game</button>
    </div>
</body>
</html>

Connecting to the WebSocket Server

We must establish a connection between our frontend and the WebSocket server running on Flask. This is done using Socket.IO:

const socket = io("ws://localhost:8080/snake_game", {
    path: "/socket.io/",
    transports: ["websocket"]
});

This connection allows the client to send and receive real-time updates.

Handling Game Updates

When the server sends a game update, we need to redraw the game state on the canvas:

socket.on("game_update", (state) => {
    console.log("🎮 Game Update Received:", state);
    gameState = state;
    drawGame();
    updateScore();
});

The drawGame() function will clear the canvas and redraw the player snake, AI snake, and food based on the received game state.

Handling Player Movement

We need to listen for keyboard events to capture player movement and send it to the server:

document.addEventListener("keydown", (event) => {
    const direction = {
        "ArrowUp": "up",
        "ArrowDown": "down",
        "ArrowLeft": "left",
        "ArrowRight": "right"
    }[event.key];

    if (direction) {
        console.log(`➡️ Sending Move: ${direction}`);
        socket.emit("player_move", { direction });
    }
});

Drawing the Game on Canvas

Every time the game state updates, we need to redraw everything:

function drawGame() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // Draw player snake
    ctx.fillStyle = "lime";
    gameState.player_snake.forEach(([x, y]) => {
        ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
    });
    
    // Draw AI snake
    ctx.fillStyle = "red";
    gameState.ai_snake.forEach(([x, y]) => {
        ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
    });
    
    // Draw food
    ctx.fillStyle = "yellow";
    let [fx, fy] = gameState.food;
    ctx.fillRect(fx * gridSize, fy * gridSize, gridSize, gridSize);
}

Resetting and Starting the Game

We need buttons to control the game:

function resetGame() {
    console.log("🔄 Resetting Game");
    socket.emit("reset_game");
}

function startGame() {
    console.log("▶️ Starting Game");
    socket.emit("start_game");
}

7. Updating the Score

To keep track of the player and AI snake lengths:

function updateScore() {
    document.getElementById("playerLength").innerText = gameState.player_snake.length;
    document.getElementById("aiLength").innerText = gameState.ai_snake.length;
}

Step 3: Setting Up Nginx as Reverse Proxy and Static File Server

In this step, we set up Nginx to serve static files and proxy WebSocket connections to our Flask backend. Nginx acts as a reverse proxy, handling requests for both the frontend and WebSocket communication.

Why Use Nginx?

Nginx provides:

  • Efficient static file serving: It directly serves index.html and other frontend assets.
  • WebSocket support: It forwards WebSocket traffic to the Flask backend.
  • Better performance: Offloading static file delivery from Flask reduces backend load.

Nginx Configuration

Below is our Nginx configuration file (default.conf):

server {
    listen 8080;

    # Serve static files (index.html)
    location / {
        root /usr/share/nginx/html;
        index index.html;
    }

    # Proxy WebSocket connections to the Flask backend
    location /socket.io/ {
        proxy_pass http://backend:5050;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_http_version 1.1;
    }
}

Breaking Down the Configuration

1. Serving Static Files

  • The location / block tells Nginx to serve files from /usr/share/nginx/html.
  • The index index.html; directive sets the default file to load when visiting the site.

2. Handling WebSocket Traffic

  • The location /socket.io/ block proxies WebSocket connections to http://backend:5050 (our Flask app running on port 5050 inside Docker).
  • proxy_set_header Upgrade $http_upgrade; ensures WebSocket connections are appropriately upgraded.
  • proxy_set_header Connection "Upgrade"; is required for persistent WebSocket communication.
  • proxy_http_version 1.1; ensures compatibility with WebSockets.

Step 4: Dockerizing the Application

In this step, we will containerize our WebSocket application using Docker and Docker Compose. This will allow us to run our backend (Flask + Socket.IO) and frontend (served via Nginx) in isolated environments, ensuring consistency across different systems.

Why Use Docker?

Docker simplifies deployment by packaging the application and its dependencies into containers. This ensures that our WebSocket application behaves the same way across various environments without needing to manually install dependencies.

However, explaining Docker in depth is beyond the scope of this tutorial. If you're new to Docker, consider reviewing its official documentation to understand concepts like images, containers, and networking.

Docker Compose Setup

We use Docker Compose to define and manage multiple services. Our docker-compose.yml file includes two services:

  • backend: The Flask + Socket.IO server, running on port 5050.
  • nginx: A web server that serves the frontend and proxies WebSocket connections to the backend.

docker-compose.yml

services:
  backend:
    build: ./backend
    ports:
      - "5050:5050"
    networks:
      - snake_network

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/index.html:/usr/share/nginx/html/index.html
    ports:
      - "8080:8080"
    depends_on:
      - backend
    networks:
      - snake_network

networks:
  snake_network:

Breaking Down the Configuration

  • backend:

    • Builds the Flask server from the backend directory.
    • Exposes port 5050 to handle WebSocket connections.
    • Uses a custom network (snake_network) to communicate with Nginx.
  • nginx:

    • Uses the latest Nginx image.
    • Maps default.conf for configuring Nginx as a reverse proxy.
    • Serves index.html as the frontend.
    • Exposes port 8080 to the host machine.
    • Depends on the backend service to ensure the Flask app is available before Nginx starts.
  • snake_network:

    • A dedicated network that allows communication between the backend and Nginx services.

Running the Application with Docker Compose

Start the Project

Then Run the following command:

./start.sh

This will:

  • Build the Docker containers (if not built already).
  • Start the Flask WebSocket server and Nginx frontend.

Access the Game

Once running, open a browser and go to:

http://localhost:8080

and play !

Stop the Project

Run:

./stop.sh

This will stop and remove all running containers.


Conclusion: Bringing It All Together 🎉

In this tutorial, we explored how WebSockets work and how Socket.IO simplifies real-time communication. We took a hands-on approach by building a real-time multiplayer Snake game, where players compete against a "not-so-smart AI" over a WebSocket connection.

We structured our project using:

  • Flask-SocketIO to handle real-time WebSocket communication.
  • Nginx to serve the frontend and act as a WebSocket proxy.
  • Docker & Docker Compose to streamline deployment.

While we focused on a simple example, the same approach can be applied to real-world applications like live chat systems, real-time dashboards, or multiplayer games.

Remember:

👉 Check out the full project on GitHub: github.com/doradame/mojalab-websocket
👉 Try it live: mojalab.com/websockets-in-action/

WebSockets open up a world of possibilities for real-time applications. Now that you’ve seen them in action, what will you build next?

Disclaimer: At MojaLab, we aim to provide accurate and useful content, but hey, we’re human (well, mostly)! If you spot an error, have questions, or think something could be improved, feel free to reach out—we’d love to hear from you. Use the tutorials and tips here with care, and always test in a safe environment. Happy learning!!!

No AI was mistreated in the making of this tutorial—every LLM was used with the respect it deserves.