官方文档链接:https://channels.readthedocs.io/en/latest/
聊天服务器包括两个网页:提供索引的index页,用来选择要加入的聊天室的名称;进行聊天的room页,用来查看特定聊天室中发布的信息
通过以下查看当前Django的安装版本
python3 -m django --version
通过以下命令查看安装的channels版本
python3 -c 'import channels;print(channels.__version__)'
Django2.0适配python3.5+和Django1.11+
创建一个mysite的项目并添加一个chat的app
django-admin startproject mysite
python3 manage.py startapp chat
将除了view之外的文件删除,通过tree命令可以得到如下树状图
mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py
chat/
__init__.py
views.py
在setting文件中添加chat和channels模块
# mysite/settings.py
INSTALLED_APPS = [
'chat',
'channels',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
在template文件夹中创建一个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>
修改chat模块的view,添加响应的视图函数
# chat/views.py
from django.shortcuts import render
def index(request):
return render(request, 'chat/index.html', {})
在chat模块中新建一个urls文件并添加URL路径
# chat/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.index, name='index'),
]
修改根路径下的urls文件,添加url配置信息
# mysite/urls.py
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^chat/', include('chat.urls')),
url(r'^admin/', admin.site.urls),
]
如果你使用的是Django2.0+的版本而不是1.x的版本需要手动从django.conf.url中导入url和include,因为从Django2.0开始模块使用的是path而不是url
配置完成之后执行运行命令
python3 manage.py runserver
可以在命令行界面看到以下内容
Django version 1.11.10, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
至此基本的项目构建完毕,接下来完成channels的构建
在channels中有两个不同于常规Django项目的文件,分别是routing和consumers,routing文件用来识别websocket连接,consumers用来处理websocket,简单来说前者相当于Django中的url,而后者相当于Django中的view
首先需要在setting文件中添加channels的根routing配置位置
# mysite/settings.py
# Channels
ASGI_APPLICATION = 'mysite.routing.application'
接着创建在根路径下创建一个routing文件,添加以下内容
# mysite/routing.py
from channels.routing import ProtocolTypeRouter
application = ProtocolTypeRouter({
# (http->django views is added by default)
})
现在再次重启服务,可以命令行信息改动
Django version 1.11.10, using settings 'mysite.settings'
Starting ASGI/Channels development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
2018-02-18 22:16:23,729 - INFO - server - HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2018-02-18 22:16:23,730 - INFO - server - Configuring endpoint tcp:port=8000:interface=127.0.0.1
2018-02-18 22:16:23,731 - INFO - server - Listening on TCP address 127.0.0.1:8000
现在的启动方式由默认启动方式变成了ASGI/channels启动,同时还有对应的监听信息
接着创建聊天的room页,并添加以下内容
<!-- 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"/>
</body>
<script>
var roomName = {{ room_name_json }};
var chatSocket = new WebSocket(
'ws://' + window.location.host +
'/ws/chat/' + roomName + '/');
chatSocket.onmessage = function(e) {
var data = JSON.parse(e.data);
var message = data['message'];
document.querySelector('#chat-log').value += (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) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</html>
并在chat的url中添加配置信息
url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),
重启服务发现可以通知先前的index页面可以进入当前页面,使用F12打开审查页面,选择console页,会发现提示websocket连接异常,这是因为虽然在setting中添加了routing配置信息但是实际文件中尚未完善,接下来进行consumers和routing的完善
在chat模块下创建consumers文件,并添加以下内容
# chat/consumers.py
from channels.generic.websocket import WebsocketConsumer
import json
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
}))
现在就已经完成一个简单的websocket连接配置,一般来说,默认使用的是同步websocket连接,这个连接会接收所有的连接,从客户端接收信息,同时将信息同步发送回客户端
需要注意的是,对于channels,也是支持异步功能的,通过异步也可以获得更高的性能,但是相应的,异步的操作就可能出现阻塞操作,比如异步访问Django的model的时候
完成了consumers的编写,最后进行routing
的配置,在chat模块下创建一个routing文件,添加以下内容
# chat/routing.py
from django.conf.urls import url
from . import consumers
websocket_urlpatterns = [
url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
]
以及修改原本尚未添加配置信息的根路径的routing文件
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
将上述内容添加到application=ProtocolTypeRouter()的括号中
这样配置之后,每次发起连接,ProtocolTypeRouter都会进行检查,如果有websocket连接(ws://或者wss://),就回自动调用AuthMiddlewareStack,而AuthMiddlwareStack则会连接到对应的URLRouter上,此处为跳转进入chat.routing.websocket_urlpatterns,也就是我们在chat模块中配置的routing中
重启服务,开启两个页面,我们在其中一个页面中发送hello,可以在另外一个页面的对话框中看到之前的页面发送的信息
下面介绍channel中通道层(channel layer),channel layer 属于通信系统的一种,他允许多实例(也是多个consumers)进行交谈,或者与Django的其他部分进行交互
channel layer提供以下两个概念
- channel:位于单个channel中的所有用户发送的信息会广播给当前channel下的所有用户
- group:多个channel构成的组,拥有组名称的用户可以添加/删除组,同时发送的信息广播给当前组的所有频道的所有用户,需要注意的是组内只能广播无法指定某一channel进行单播
每一个consumers都有一个唯一的channel,并且可以通过这个channel进行与channel layer的通讯,在这个聊天服务器上希望能够多个实例之间互相通讯,所以我们将其添加到基于房间名的组中,通过组进行多个实例互相交互
在修改consumers文件之间我们先完成Redis的配置,一般来说channel使用Redis作为存储层
在安装完毕Redis并成功开启服务之后向setting文件添加以下内容
# mysite/settings.py
# Channels
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
配置完成之后修改chat模块下的consumers文件,添加以下内容:
# chat/consumers.py
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json
class ChatConsumer(WebsocketConsumer):
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
}))
这样,当用户发送消息时,js通过websocket将消息传输到Chatconsumer,chatconsumer将消息转发到对应的组,组内的每一个chatconsumer接收消息并通过websocket转发回客户端,同步添加到聊天日志中
//部分代码解释
self.scope['url']['kwargs']['room_name']:通过解包url获取传入的关键词的信息,对于每一个consumer实例而言,都拥有一个scope,在这个scope中包含了此次连接的信息,包含url的位置以及关键词
self.room_group_name = 'chat_%s'%self.room_name:使用用户的房间名称直接命名组名称,不进行转义
async_to_sync(self.channel_layer.group_add)(...):1、加入一个组中;2、async_to_sync 这个装饰器是必须的因为chatconsumer是一个同步websocket连接,但是对于channel layer需要调用异步方法,因为所有的channel layer都需要异步实现;3、组名字仅限于ASCII字母数字和字符句点
self.accept():接受websocket连接,如果该方法并非在connect()中被调用,则会发生拒绝并关闭连接
async_to_sync(self.channel_layer.group_discard)(...):关闭组
async_to_sync(self.channel_layer.group_send):1、发送一个event到组;2、在这个event中一般会定义的第一个key:value组为type,value锁指向的就是调用的方法
接下来将consumer由同步改写成异步,提高性能
之前在chatconsumer中所使用的是同步方式,这个方式可以调用常规的IO函数,但是对于异步而言,可以具有更高的性能,并且不需要创建新的线程;对于在这个例子中的chatconsumer而言,并不访问同步Django model,仅仅使用了 async-native 库,不会因为重写为异步出现复杂的情况,但是,就算是访问了Django model或者其他同步代码,也可以重写为异步,然后调用sync_to_async(与async_to_sync位于同一个模块下)以同步状态调用异步代码,但是性能提升没有这个例子的高
重写consumer,添加以下内容
# chat/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class ChatConsumer(AsyncWebsocketConsumer):
async 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
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
对比之前的原始代码可以发现
- 现在chatconsumer继承自AsyncWebsocketConsumer
而不是WebsocketConsumer - 所有的方法都是async def而不是def了
- await用来调用异步的IO函数
- 在channel layer上调用方法时不再需要async_to_sync了