Django Channels facilitates support of WebSockets in Django in a manner similar to traditional HTTP views. It wraps Django’s native asynchronous view support, allowing Django projects to handle not only HTTP, but also protocols that require long-running connections, such as WebSocket.
In this tutorial, we’ll show you how to create a real-time app with Django Channels. To demonstrate with a live example, we’ll create a two-player tic-tac-toe game, as illustrated below.
Follow the steps outlined below to configure your Django project.
First, install Django and channels. You must also install
channels_redis
so that channels know how to interface with Redis.
Run the following command:
pip install django==3.1 pip install channels==3.0 pip install channels_redis==3.2
You should use pip3 for Linux/mac instead of pip and python3 in place of python. I used ” django==3.1″ and “channels==3.0″,” channels_redis==3.2.0″ for this guide.
Start the Django project:
django-admin startproject tic_tac_toe
Next, create an app with the name game:
python manage.py startapp game
Add “channels” and “game” in the “INSTALLED_APPS” inside your “settings.py”:
## settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', 'game' ]
Run “migrate” to apply unapplied migrations:
python manage.py migrate
Also, add “STATICFILES_DIRS” inside your “settings.py”:
## settings.py import os STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ]
Now it’s time to create the necessary files for our Django project. Throughout the guide, you may refer to the following directory structure:
├── db.sqlite3 ├── game │ ├── consumers.py │ ├── routing.py │ ├── templates │ │ ├── base.html │ │ ├── game.html │ │ └── index.html │ └── views.py ├── manage.py ├── requirements.txt ├── static │ ├── css │ │ └── main.css │ └── js │ └── game.js └── tic_tac_toe ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py
Now let’s integrate Channels into the Django project.
Django >2 doesn’t have built-in ASGI support, so you need to use Channel’s fallback alternative.
Update the “asgi.py” as shown below:
## tic_tac_toe/asgi.py import os import django from channels.http import AsgiHandler from channels.routing import ProtocolTypeRouter os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tic_tac_toe.settings') django.setup() application = ProtocolTypeRouter({ "http": AsgiHandler(), ## IMPORTANT::Just HTTP for now. (We can add other protocols later.) })
Update “settings.py” and change the Django application from WSGI to ASGI by making the following changes. This will point the channels at the root routing configuration.
## settings.py # WSGI_APPLICATION = 'tic_tac_toe.wsgi.application' # Channels ASGI_APPLICATION = "tic_tac_toe.asgi.application"
Next, enable the channel layer, which allows multiple consumer instances to talk with each other.
Note that you could the Redis as the backing store. To enable Redis, you could use Method 1 if you want Redis Cloud or Method 2 for local Redis. In this guide, I used Method 3 — “In-memory channel layer” — which is helpful for testing and for local development purposes.
To enable the channel layer, add the following “CHANNEL_LAYERS” in “settings.py”:
## settings.py CHANNEL_LAYERS = { 'default': { ### Method 1: Via redis lab # 'BACKEND': 'channels_redis.core.RedisChannelLayer', # 'CONFIG': { # "hosts": [ # 'redis://h:<password>;@<redis Endpoint>:<port>' # ], # }, ### Method 2: Via local Redis # 'BACKEND': 'channels_redis.core.RedisChannelLayer', # 'CONFIG': { # "hosts": [('127.0.0.1', 6379)], # }, ### Method 3: Via In-memory channel layer ## Using this method. "BACKEND": "channels.layers.InMemoryChannelLayer" }, }
Make sure that the channels development server is working correctly. Run the following command:
python manage.py runserver
Let’s start by building the index page, where the user is asked for room code and character choice (X or O).
Create the function-based view in “game/views.py”:
# game/views.py from django.shortcuts import render, redirect def index(request): if request.method == "POST": room_code = request.POST.get("room_code") char_choice = request.POST.get("character_choice") return redirect( '/play/%s?&choice=%s' %(room_code, char_choice) ) return render(request, "index.html", {})
Next, create the route for the index view in “tic_tac_toe/urls.py”:
## urls.py from django.urls import path from game.views import index urlpatterns = [ ## ... Other URLS path('', index), ]
Now, create the base template in “game/templates/base.html” (ignore if you have already created it). This template is going to be inherited to other template views.
{% comment %} base.html {% endcomment %} {% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tic Tac Toe</title> <link rel='stylesheet' href='{% static "/css/main.css" %}'> </head> <body> {% block content %} {% endblock content %} <script src = "{% static 'js/game.js' %}"></script> {% block javascript %} {% endblock javascript %} </body> </html>
Create the view template for the index view in ” game/templates/index.html “:
{% comment %} index.html {% endcomment %} {% extends 'base.html' %} {% block content %} <div class="wrapper"> <h1>Welcome to Tic Tac Toe Game</h1> <form method="POST"> {% csrf_token %} <div class='form-control'> <label for="room">Room id</label> <input id="room" type="text" name="room_code" required /> </div> <div class='form-control'> <label for="character_choice">Your character</label> <select for="character_choice" name = "character_choice"> <option value="X">X</option> <option value="O">O</option> </select> </div> <input type="submit" class="button" value="Start Game" /> </div> </form> {% endblock content %}
Start the Django development server and navigate to http://127.0.0.1:8000 to check whether the index page is working:
Now that the index page is done, let’s build the game page
Start by creating “game/views.py”:
## game/views.py from django.shortcuts import render, redirect from django.http import Http404 def game(request, room_code): choice = request.GET.get("choice") if choice not in ['X', 'O']: raise Http404("Choice does not exists") context = { "char_choice": choice, "room_code": room_code } return render(request, "game.html", context)
Add the URL route of the above view:
## urls.py from django.urls import path from game.views import game urlpatterns = [ ## other url routes path('play/<room_code>', game), ]
Now that the backend is done, let’s create the frontend of the game board. Below is the “game/templates/game.html” Django template:
{% extends 'base.html' %} {% comment %} game.html {% endcomment %} {% load static %} {% block content %} <div class="wrapper"> <div class="head"> <h1>TIC TAC TOE</h1> <h3>Welcome to room_{{room_code}}</h3> </div> <div id = "game_board" room_code = {{room_code}} char_choice = {{char_choice}}> <div class="square" data-index = '0'></div> <div class="square" data-index = '1'></div> <div class="square" data-index = '2'></div> <div class="square" data-index = '3'></div> <div class="square" data-index = '4'></div> <div class="square" data-index = '5'></div> <div class="square" data-index = '6'></div> <div class="square" data-index = '7'></div> <div class="square" data-index = '8'></div> </div> <div id = "alert_move">Your turn. Place your move <strong>{{char_choice}}</strong></div> </div> {% endblock content %}
To make the grid and index page look good, add the CSS, as shown below:
/* static/css/main.css */ body { /* width: 100%; */ height: 90vh; background: #f1f1f1; display: flex; justify-content: center; align-items: center; } #game_board { display: grid; grid-gap: 0.5em; grid-template-columns: repeat(3, 1fr); width: 16em; height: auto; margin: 0.5em 0; } .square{ background: #2f76c7; width: 5em; height: 5em; display: flex; justify-content: center; align-items: center; border-radius: 0.5em; font-weight: 500; color: white; box-shadow: 0.025em 0.125em 0.25em rgba(0, 0, 0, 0.25); } .head{ width: 16em; text-align: center; } .wrapper h1, h3 { color: #0a2c1a; } label { font-size: 20px; color: #0a2c1a; } input, select{ margin-bottom: 10px; width: 100%; padding: 15px; border: 1px solid #125a33; font-size: 14px; background-color: #71d19e; color: white; } .button{ color: white; white-space: nowrap; background-color: #31d47d; padding: 10px 20px; border: 0; border-radius: 2px; transition: all 150ms ease-out; }
When you run the development server, you’ll see the game board, as shown below:
Now that the pages are created, let’s add the WebSockets to it.
Enter the following code in “game/consumers.py”:
## game/consumers.py import json from channels.generic.websocket import AsyncJsonWebsocketConsumer class TicTacToeConsumer(AsyncJsonWebsocketConsumer): async def connect(self): self.room_name = self.scope['url_route'\]['kwargs']['room_code'] self.room_group_name = 'room_%s' % self.room_name # Join room group await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): print("Disconnected") # Leave room group await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) async def receive(self, text_data): """ Receive message from WebSocket. Get the event and send the appropriate event """ response = json.loads(text_data) event = response.get("event", None) message = response.get("message", None) if event == 'MOVE': # Send message to room group await self.channel_layer.group_send(self.room_group_name, { 'type': 'send_message', 'message': message, "event": "MOVE" }) if event == 'START': # Send message to room group await self.channel_layer.group_send(self.room_group_name, { 'type': 'send_message', 'message': message, 'event': "START" }) if event == 'END': # Send message to room group await self.channel_layer.group_send(self.room_group_name, { 'type': 'send_message', 'message': message, 'event': "END" }) async def send_message(self, res): """ Receive message from room group """ # Send message to WebSocket await self.send(text_data=json.dumps({ "payload": res, }))
Create a routing configuration for the game app that has a route to the consumer. Create a new file “game/routing.py” and paste the following code:
## game/routing.py from django.conf.urls import url from game.consumers import TicTacToeConsumer websocket_urlpatterns = [ url(r'^ws/play/(?P<room_code>\w+)/$', TicTacToeConsumer.as_asgi()), ]
The next step is to point the root routing configuration at the “game.routing” module. Update “tic_tac_toe/asgi.py” as follows:
## tic_tac_toe/asgi.py import os from django.core.asgi import get_asgi_application from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import game.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tic_tac_toe.settings') # application = get_asgi_application() application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": AuthMiddlewareStack( URLRouter( game.routing.websocket_urlpatterns ) ), })
Let’s build the final part of the code by creating the JavaScript, which is the client side that talks to the server asynchronously. Put the following code in “static/js/game.js”:
// static/js/game.js var roomCode = document.getElementById("game_board").getAttribute("room_code"); var char_choice = document.getElementById("game_board").getAttribute("char_choice"); var connectionString = 'ws://' + window.location.host + '/ws/play/' + roomCode + '/'; var gameSocket = new WebSocket(connectionString); // Game board for maintaing the state of the game var gameBoard = [ -1, -1, -1, -1, -1, -1, -1, -1, -1, ]; // Winning indexes. winIndices = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] let moveCount = 0; //Number of moves done let myturn = true; // Boolean variable to get the turn of the player. // Add the click event listener on every block. let elementArray = document.getElementsByClassName('square'); for (var i = 0; i < elementArray.length; i++){ elementArray[i].addEventListener("click", event=>{ const index = event.path[0].getAttribute('data-index'); if(gameBoard[index] == -1){ if(!myturn){ alert("Wait for other to place the move") } else{ myturn = false; document.getElementById("alert_move").style.display = 'none'; // Hide make_move(index, char_choice); } } }) } // Make a move function make_move(index, player){ index = parseInt(index); let data = { "event": "MOVE", "message": { "index": index, "player": player } } if(gameBoard[index] == -1){ // if the valid move, update the gameboard // state and send the move to the server. moveCount++; if(player == 'X') gameBoard[index] = 1; else if(player == 'O') gameBoard[index] = 0; else{ alert("Invalid character choice"); return false; } gameSocket.send(JSON.stringify(data)) } // place the move in the game box. elementArray[index].innerHTML = player; // check for the winner const win = checkWinner(); if(myturn){ // if player winner, send the END event. if(win){ data = { "event": "END", "message": `${player} is a winner. Play again?` } gameSocket.send(JSON.stringify(data)) } else if(!win && moveCount == 9){ data = { "event": "END", "message": "It's a draw. Play again?" } gameSocket.send(JSON.stringify(data)) } } } // function to reset the game. function reset(){ gameBoard = [ -1, -1, -1, -1, -1, -1, -1, -1, -1, ]; moveCount = 0; myturn = true; document.getElementById("alert_move").style.display = 'inline'; for (var i = 0; i < elementArray.length; i++){ elementArray[i].innerHTML = ""; } } // check if their is winning move const check = (winIndex) => { if ( gameBoard[winIndex[0]] !== -1 && gameBoard[winIndex[0]] === gameBoard[winIndex[1]] && gameBoard[winIndex[0]] === gameBoard[winIndex[2]] ) return true; return false; }; // function to check if player is winner. function checkWinner(){ let win = false; if (moveCount >= 5) { winIndices.forEach((w) => { if (check(w)) { win = true; windex = w; } }); } return win; } // Main function which handles the connection // of websocket. function connect() { gameSocket.onopen = function open() { console.log('WebSockets connection created.'); // on websocket open, send the START event. gameSocket.send(JSON.stringify({ "event": "START", "message": "" })); }; gameSocket.onclose = function (e) { console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason); setTimeout(function () { connect(); }, 1000); }; // Sending the info about the room gameSocket.onmessage = function (e) { // On getting the message from the server // Do the appropriate steps on each event. let data = JSON.parse(e.data); data = data["payload"]; let message = data['message']; let event = data["event"]; switch (event) { case "START": reset(); break; case "END": alert(message); reset(); break; case "MOVE": if(message["player"] != char_choice){ make_move(message["index"], message["player"]) myturn = true; document.getElementById("alert_move").style.display = 'inline'; } break; default: console.log("No event") } }; if (gameSocket.readyState == WebSocket.OPEN) { gameSocket.onopen(); } } //call the connect function at the start. connect();
Now we’re finally finished coding and ready to play our tic-tac-toe game!
We covered a lot of topics in this tutorial: Django Channels, WebSockets, and some frontend. Our game so far has only minimal, basic functionality. The demo for building Tic Tac Toe with Django Channels showcases the potential of real-time web applications. By leveraging the power of Django Channels, developers can create interactive and engaging multiplayer games like Tic Tac Toe. This project highlights the seamless integration of Django Channels with WebSocket technology, enabling real-time communication between players and providing an immersive gaming experience.
With this knowledge, developers can explore endless possibilities for building dynamic and collaborative applications using Django Channels. You’re welcome to use your new foundational knowledge to play around and add more functionality to it.
How to Effectively Hire and Manage a Remote Team of Developers.
Download nowThe Mindbowser team's professionalism consistently impressed me. Their commitment to quality shone through in every aspect of the project. They truly went the extra mile, ensuring they understood our needs perfectly and were always willing to invest the time to...
CTO, New Day Therapeutics
I collaborated with Mindbowser for several years on a complex SaaS platform project. They took over a partially completed project and successfully transformed it into a fully functional and robust platform. Throughout the entire process, the quality of their work...
President, E.B. Carlson
Mindbowser and team are professional, talented and very responsive. They got us through a challenging situation with our IOT product successfully. They will be our go to dev team going forward.
Founder, Cascada
Amazing team to work with. Very responsive and very skilled in both front and backend engineering. Looking forward to our next project together.
Co-Founder, Emerge
The team is great to work with. Very professional, on task, and efficient.
Founder, PeriopMD
I can not express enough how pleased we are with the whole team. From the first call and meeting, they took our vision and ran with it. Communication was easy and everyone was flexible to our schedule. I’m excited to...
Founder, Seeke
Mindbowser has truly been foundational in my journey from concept to design and onto that final launch phase.
CEO, KickSnap
We had very close go live timeline and Mindbowser team got us live a month before.
CEO, BuyNow WorldWide
If you want a team of great developers, I recommend them for the next project.
Founder, Teach Reach
Mindbowser built both iOS and Android apps for Mindworks, that have stood the test of time. 5 years later they still function quite beautifully. Their team always met their objectives and I'm very happy with the end result. Thank you!
Founder, Mindworks
Mindbowser has delivered a much better quality product than our previous tech vendors. Our product is stable and passed Well Architected Framework Review from AWS.
CEO, PurpleAnt
I am happy to share that we got USD 10k in cloud credits courtesy of our friends at Mindbowser. Thank you Pravin and Ayush, this means a lot to us.
CTO, Shortlist
Mindbowser is one of the reasons that our app is successful. These guys have been a great team.
Founder & CEO, MangoMirror
Kudos for all your hard work and diligence on the Telehealth platform project. You made it possible.
CEO, ThriveHealth
Mindbowser helped us build an awesome iOS app to bring balance to people’s lives.
CEO, SMILINGMIND
They were a very responsive team! Extremely easy to communicate and work with!
Founder & CEO, TotTech
We’ve had very little-to-no hiccups at all—it’s been a really pleasurable experience.
Co-Founder, TEAM8s
Mindbowser was very helpful with explaining the development process and started quickly on the project.
Executive Director of Product Development, Innovation Lab
The greatest benefit we got from Mindbowser is the expertise. Their team has developed apps in all different industries with all types of social proofs.
Co-Founder, Vesica
Mindbowser is professional, efficient and thorough.
Consultant, XPRIZE
Very committed, they create beautiful apps and are very benevolent. They have brilliant Ideas.
Founder, S.T.A.R.S of Wellness
Mindbowser was great; they listened to us a lot and helped us hone in on the actual idea of the app. They had put together fantastic wireframes for us.
Co-Founder, Flat Earth
Ayush was responsive and paired me with the best team member possible, to complete my complex vision and project. Could not be happier.
Founder, Child Life On Call
The team from Mindbowser stayed on task, asked the right questions, and completed the required tasks in a timely fashion! Strong work team!
CEO, SDOH2Health LLC
Mindbowser was easy to work with and hit the ground running, immediately feeling like part of our team.
CEO, Stealth Startup
Mindbowser was an excellent partner in developing my fitness app. They were patient, attentive, & understood my business needs. The end product exceeded my expectations. Thrilled to share it globally.
Owner, Phalanx
Mindbowser's expertise in tech, process & mobile development made them our choice for our app. The team was dedicated to the process & delivered high-quality features on time. They also gave valuable industry advice. Highly recommend them for app development...
Co-Founder, Fox&Fork