我们知道python有socket包可以直接实现socket通信。
但在使用django时,不太适用于socket的方式与前端交互,对此django有channels来很好的支持socket通信。参考了网上很多资料之后发现写的不是很详细,最详细的是官方的资料。
项目地址:
GitHub - Wellbulizy/django-socket-channels: django-channels实现socket通信的简单例子
参考资料
Channels官方资料:
Introduction — Channels 3.0.4 documentation
Pypi-channels-redis:
版本
Python:anaconda3/py3.7.2
Linux Centos8 localhost.localdomain 4.18.0-240.el8.x86_64 x86_64 x86_64 x86_64 GNU/Linux
Django: 2.2.16
项目结构
后续说明根据项目结构目录来讲解,请记住这个项目结构的文件。
页面代码编写
在传统的socket编写时,我们只需要一个端口,你发我接 + 我发你接,互相通信即完事了,然后突然转到django 的channels很难理解。其实原理是一样的,需要有路由中间件来区分web socket通信。
Ps:教程还是官网最详细,这里粗略带过,真的建议去看channels的官方
先不考虑socket通信,我们简单的把聊天室的界面做出来
dsocket/urls.py
from django.conf.urls import include
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('chat/', include('mysite.urls')),
path('admin/', admin.site.urls),
]
#指向app的urls路径↓
mysite/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<str:room_name>/', views.room, name='room'),
]
#因为模板这些设置是默认的,不需要更改settings可以直接创建目录并添加html文件
mysite/templates/index.html
<!-- chat/templates/chat/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Rooms</title>
</head>
<body>
What chat room would you like to enter?<br>
<input id="room-name-input" type="text" size="100"><br>
<input id="room-name-submit" type="button" value="Enter">
<script>
document.querySelector('#room-name-input').focus();
document.querySelector('#room-name-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#room-name-submit').click();
}
};
document.querySelector('#room-name-submit').onclick = function(e) {
var roomName = document.querySelector('#room-name-input').value;
window.location.pathname = '/chat/' + roomName + '/';
};
</script>
</body>
</html>
mysite/templates/room.html
#代码太长,稍微压缩了一些行,所以看上去可能不太美观…
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><title>Chat Room</title></head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const chatSocket = new WebSocket('ws://'+window.location.host+'/ws/chat/'+roomName+ '/');
chatSocket.onmessage = function(e) {const data = JSON.parse(e.data);document.querySelector('#chat-log').value += (data.message + '\n');};
chatSocket.onclose = function(e) {console.error('Chat socket closed unexpectedly');};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</body>
</html>
前置验证
启动项目:
Python manage.py runserver 192.168.70.132:8008
通过以上步骤,聊天室socket的界面暂时是OK了,访问如下
输入内容按回车,会跳转到聊天室:
当然这里只是聊天室,核心的通信功能都没有,F12大概能看到通信报错
socket代码编写
现在到了核心代码编写部分,我们需要加入channels的中间件,拦截请求识别请求类型,需要更改settings.py,由于官方和网上教程都是免密的redis配置,所以我到另一个channels-redis的官网找到了带密码的配置内容。
vim /dsocket/settings.py
#INSTALLED_APPS添加channels插件,如果已经有自定义的中间件,自己看情况添加
INSTALLED_APPS = [
'mysite', #app的名字
'channels',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
#添加下面的内容,单会话通信不需要
ASGI_APPLICATION = 'dsocket.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": ["redis://:123456@127.0.0.1:6379/6"],
#免密:"hosts": [('127.0.0.1', 6379)],
},
},
}
vim /dsocket/asgi.py
该文件的作用是请求拦截中间件,如下有识别http与websocket请求(前缀ws://或wss://),根据不同的请求做不同的处理。
import os
import django
from channels.auth import AuthMiddlewareStack
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter,URLRouter
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dsocket.settings')
django.setup()
from django.urls import re_path
from mysite import consumers
websocket_urlpatterns = [re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),]
application = ProtocolTypeRouter({
"http": AsgiHandler(), #django3这里有区别,这里只是django2的写法
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
})
单会话vim /mysite/consumers.py
#这是一个单会话通信的处理代码
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.accept()
def disconnect(self, close_code):
pass
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
self.send(text_data=json.dumps({
'message': message
}))
什么是单会话通信?如果你同时打开多个聊天室,比如http://192.168.70.132:8008/chat/rowb/开两个界面,两个界面互相输入的信息都只有自己能看到。如果需要在同一个聊天室的都能看到,那需要启动通道层。这就是上面settings.py里配置redis的作用。下面即启用通道层后的consumers.py代码。
多会话 /mysite/consumers.py
对比上面的单会话模式,逻辑上加入了同一个room的对象会加入到组中,即变成多对一的模式,在处理回信时发送给一个组。而不是单独回信与发起者。
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
#新的socket函数,支持广播
def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
def chat_message(self, event):
message = event['message']
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': message
}))
常见错误
ModuleNotFoundError: No module named 'django.core.asgi'
官方asgi.py的坑,其实这个导入没有必要,删掉不影响
aioredis.errors.AuthError: NOAUTH Authentication required.
Redis密码错误,或者redis需要密码,但是你没有配置
初始化运行
因为启用了redis的会话认证,django的会话框架需要数据库,所以要migrate初始化迁移应用数据库更改
python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK
#启动django后台
python manage.py runserver 192.168.70.132:8008
socket监听验证
运行项目后,会发现启动的信息变了,比如下面有starting asgi/channels即表示监听了socket的请求。
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
October 1, 2020 - 21:31:22
Django version 2.2.16, using settings dsocket.settings'
Starting ASGI/Channels version 3.0.0 development server at http://192.168.70.132:8008/
Quit the server with CONTROL-C.