https://mp.weixin.qq.com/s/hqaPrPS7w3D-9SeegQAB2Q
本人的demo路径
https://github.com/aawuliliaa/python-test/tree/master/WebSocket
全双工:
通信允许数据在两个方向上同时传输。
全双工可以同时进行信号的双向传输,即A-->B的同时B-->A,是瞬间同步的。
日常生活中的电话聊天就是全双工。
半双工:
可分时进行信号的双向传输。
指A-->B时,不能B-->A。
数据可以双向传输,但双向传输不是同时进行的。
例如对讲机,一方讲话的同时,另一方不能讲话。
单工:
单向的数据传输。
只允许A-->B传送消息,不能B-->A传送。
例如日常生活中的广播。
WebSocket是在单个TCP连接上进行的全双工通信的协议。
在WebSocket协议中,没有Request和Response的概念,连接一旦建立,就建立了持久连接,双方可以随时向对方发送数据。
HTTP协议是浏览器客户端发出请求,服务端进行响应,服务端不能主动发送信息到客户端。
WebSocket协议区别于HTTP协议的最大特点,即WebSocket可以由服务端主动发送消息到浏览器端。
最普遍的应用就是tail -f查看服务器端的日志,可实现动态刷新最新日志。
WebSocket的另外一个应用场景就是聊天室,一个浏览器端发送的消息,其他浏览器端可同时接受。这在HTTP协议下很难实现的。
Django本身不支持WebSocket协议,但可以通过Channels框架来实现WebSocket。
下面示例代码的基本环境是
python==3.6.3
django==2.2.3
channels==2.2.0
pip3 install channels==2.2.0
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'testchannel',
'django_celery_beat',
'django_celery_results',
'channels',
]
# 指定ASGI的路由地址
ASGI_APPLICATION = 'untitled.routing.application'
channels运行与ASGI协议上,ASGI的全名是Asynchronous Server Gateway Iterface(异步服务网关接口)。
ASGI是区别于Django使用的WSGI协议的一种异步网关接口协议,正是因为它才实现了websocket。
ASGI_APPLICATION指定路由的位置是project name下的routing.py文件中的application。
routing.py类似于Django中的url.py,指明websocket协议的路由。
from channels.routing import ProtocolTypeRouter
application = ProtocolTypeRouter({
# 暂时为空,下文填充
})
ssh://[email protected]:22/usr/bin/python3 -u /project/untitled/manage.py runserver 10.0.0.61:8000
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
July 31, 2019 - 12:49:23
Django version 2.2.3, using settings 'untitled.settings'
Starting ASGI/Channels version 2.2.0 development server at http://10.0.0.61:8000/
Quit the server with CONTROL-C.
Django启动中的Starting development server 变成了Starting ASGI/Channels version 2.2.0 development server at
说明项目已经由django使用的WSGI协议变为了Channels使用的ASGI协议。
到这里Django已经集成了Channels框架。
上面只是集成了channels,但还没有使用它。
下面我们构建一个聊天的例子
from django.contrib import admin
from django.urls import path
from testchannel.views import *
urlpatterns = [
path('admin/', admin.site.urls),
path('chat/', chat, name="chat-url"),
]
from django.shortcuts import render
def chat(request):
return render(request, 'index.html')
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.routing import ProtocolTypeRouter,URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path,re_path
from untitled.consumers import *
websocket_urlpatterns = [
path(r"ws/chat/", ChatConsumer),
]
application = ProtocolTypeRouter({
'websocket':AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
)
})
ProtocolTypeRouter:
ASGI支持多种不同的协议,在这里可以指定特定协议的路由信息,由于我们只使用了websocket协议,这里只配置了websocket。
AuthMiddlewareStack:
django的channels封装了django的auth模块,使用这个配置就可以在consumer中通过下面的方式获取用户的信息。
self.scope类似于django的request,包含了请求的type,path,header,cookie,session,user等有用的信息。
def connect(self):
self.user = self.scope["user"]
URLRouter:
指定路由文件的路径,或者也可以将路由信息写在这里。
routing.py和django的url.py功能类似,语法也一样,"ws/chat/"是访问路径,ChatConsumer是该路径对应的类。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import WebsocketConsumer
import json
class ChatConsumer(WebsocketConsumer):
def connect(self):
# 连接时触发
self.accept()
def disconnect(self, code):
# 关闭连接时触发
pass
def receive(self, text_data=None, bytes_data=None):
# 收到消息后触发
# 前端页面使用send()发送数据给websocket,由该函数处理
# 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
text_data_json = json.loads(text_data)
message = "聊天:"+text_data_json["message"]
self.send(text_data=json.dumps({"message":message}))
Title
{# 加载bootstrap的js#}
WebSocket对象支持四个消息:onopen,onmessage,onclose和onerror。
onopen:当浏览器和websocket服务端连接成功后触发onopen消息。
onerror:如果连接失败,或发送、接收数据失败,或者数据处理出错,都会触发onerror消息。
onmessage:当浏览器收到websocket服务器发送过来的数据时,就会触发onmessage,参数e.data是服务端发送过来的数据。
onclose:当浏览器接收到websocket服务器发送过来的关闭连接请求时,会触发onclose。
上面的例子只是实现了消息的发送和接收,如果另外打开一个浏览器,是看不到消息的。
但如果是聊天室的场景,需要一个人发送消息,多个人的浏览器端都能接收到消息。
Channel引入了一个layer的概念,channel layer是一种通信系统,允许多个consumer实例之间互相通信,以及与外部Django程序实现互通。
Channel layer主要实现了两种概念抽象:
channel name:
channel实际上是一个发送消息的通道,每个channel都有一个名称,每一个拥有这个名称的人都可以往channel里面发送消息
group:
多个channel可以组成一个group,每个group都有一个名称。
每个拥有这个名称的人都可以往这个group里添加/删除channel,也可以往group里发送消息。
group内的所有channel都可以收到,但是不能给group内的具体某个channel发送消息。
下面实现一个浏览器端发送消息,多个浏览器端都能接受到消息。
pip3 install channels_redis==2.4.0
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('10.0.0.61', 6379)],
},
},
}
通过下面的命令检查通道是否正常工作
python3 manage.py shell
>>> import channels.layers
>>> channel_layer=channels.layers.get_channel_layer()
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test',{'site':"www"})
>>> async_to_sync(channel_layer.receive)('test')
{'site': 'www'}
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import WebsocketConsumer
import json
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
def connect(self):
# 连接时触发
self.group_name = "chat_group"
async_to_sync(self.channel_layer.group_add)(
self.group_name, self.channel_name
)
self.accept()
def disconnect(self, code):
# 关闭连接时触发
async_to_sync(self.channel_layer.group_discard)(
self.group_name, self.channel_name
)
pass
def receive(self, text_data=None, bytes_data=None):
# 收到消息后触发
# 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
text_data_json = json.loads(text_data)
message = text_data_json['message']
print("wwwwwwwwwwwww", message) # yyyyyyyyyyyyy
async_to_sync(self.channel_layer.group_send)(
self.group_name,
{
'type': 'chat_message',
'message':message
}
)
def chat_message(self,event):
print("enventooooooooooooooooo", event) # {'type': 'chat_message', 'message': 'yyyyyyyyyyyyy'}
message = "聊天:" + event["message"]
self.send(text_data=json.dumps({'message':message}))
这里我们设置了一个固定了group name,所有的消息都会发送到这个group中。
也可以通过设置参数的方式设置group name,从而创建多个group,这样可以实现仅同group内的消息互通。
注意:
我在设置的时候,曾把[email protected]设置为组名,经debug调试发现报错,报错信息没有保留,就不展示了。
在设置组名的时候,@符号是不可以的了。
当我们启用了channel layer之后,所有与consumer之间的通信都会变成异步的,所以必须使用async_to_sync
一个连接(channel)创建时,通过group_add将channel添加到Group中。
连接关闭时,通过group_discard将channel从Group中删除。
收到消息时,调用group_send发送消息到Group中,group中的所有channel都可以收到。
group_send中的type指定了消息处理的函数,这里会把消息交给chat_message函数处理。
经过场面的修改,我们可以看到,一个浏览器发送的数据,多个浏览器端都能收到信息。
前面的consumer是同步的,为了更好的性能,官方支持异步的写法,只需要修改consumer.py即可
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import AsyncWebsocketConsumer,WebsocketConsumer
import json
from asgiref.sync import async_to_sync
from testchannel.tasks import *
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
# 连接时触发
self.group_name = "chat_group"
await self.channel_layer.group_add(
self.group_name, self.channel_name
)
await self.accept()
async def disconnect(self, code):
# 关闭连接时触发
await self.channel_layer.group_discard(
self.group_name, self.channel_name
)
pass
async def receive(self, text_data=None, bytes_data=None):
# 收到消息后触发
# 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
text_data_json = json.loads(text_data)
message = text_data_json['message']
print("wwwwwwwwwwwww", message)# yyyyyyyyyyyyy
await self.channel_layer.group_send(
self.group_name,
{
'type': 'chat_message',
'message':message
}
)
async def chat_message(self,event):
print("enventooooooooooooooooo",event) # {'type': 'chat_message', 'message': 'yyyyyyyyyyyyy'}
message = "聊天:" + event["message"]
await self.send(text_data=json.dumps({'message':message}))
class TailfConsumer(WebsocketConsumer):
def connect(self):
self.file_id = self.scope["url_route"]["kwargs"]["id"]
self.result = tailf.delay(self.file_id,self.channel_name)
# self.result = add.delay(1,8)
self.accept()
def disconnect(self, code):
self.result.revoke(terminate=True)
def send_message(self,event):
self.send(text_data=json.dumps({"message":event["message"]}))
异步的代码跟之前的差别不大,只有几个小区别:
ChatConsumer由WebsocketConsumer修改为了AsyncWebsocketConsumer
所有的方法都修改为了异步defasync def
用await来实现异步I/O的调用
channel layer也不再需要使用async_to_sync了
期望的效果是前端的Log中的内容,能动态的刷新
这里只是例子,就写在settings.py中了,在实际应用中,是动态获取的
LOG_PATH = {
1: '/var/log/messages',
2: '/var/log/secure',
}
from django.contrib import admin
from django.urls import path
from testchannel.views import *
urlpatterns = [
path('admin/', admin.site.urls),
path('chat/', chat, name="chat-url"),
path('login/', login, name="login"),
path('tail/', tail, name="tail"),
]
由于channel也是支持Django的auth认证模块,只有登录成功的用户才能操作,所以需要写个Login模块。
from django.shortcuts import render,redirect,reverse
from django.contrib import auth
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
# Create your views here.
from untitled import settings
def chat(request):
return render(request, 'index.html')
def login(request):
# User.objects.create_user(username="vita", password="123")
if request.method=="POST":
user = request.POST.get("username")
password = request.POST.get("password")
user = auth.authenticate(username=user, password=password)
print("mmmmmmmmmm",user)
if user:
auth.login(request, user)
return redirect(reverse('tail'))
return render(request,'login.html')
@login_required(login_url='/login/')
# 登录后才能操作
def tail(request):
log_paths = settings.LOG_PATH
return render(request, "tail.html", locals())
Title
Title
{# 加载bootstrap的js#}
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.routing import ProtocolTypeRouter,URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path,re_path
from untitled.consumers import *
#换种写法
application = ProtocolTypeRouter({
'websocket':AuthMiddlewareStack(
URLRouter([
path('ws/chat/', ChatConsumer),
re_path(r'^ws/tailf/(?P\d+)/$', TailfConsumer),
])
)
})
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import AsyncWebsocketConsumer,WebsocketConsumer
import json
from asgiref.sync import async_to_sync
from testchannel.tasks import *
class TailfConsumer(WebsocketConsumer):
def connect(self):
self.file_id = self.scope["url_route"]["kwargs"]["id"]
# 开始celery的异步任务
self.result = tailf.delay(self.file_id,self.channel_name)
# self.result = add.delay(1,8)
self.accept()
def disconnect(self, code):
# 终止celery的异步任务的执行
self.result.revoke(terminate=True)
def send_message(self,event):
self.send(text_data=json.dumps({"message":event["message"]}))
connect:
我们知道,self.scope类似于Django中的request,记录了请求信息。
通过self.scope["url_route"]["kwargs"]["id"]取出routing中正则匹配的日志ID。
然后将id和channel_name传递给celery的任务函数tailf,tailf根据id取到日志文件的路径,然后循环文件,将新内容根据channel_name写入对应channel
disconnect:
当websocket连接断开时,需要终止celery的task执行,以清除celery的资源占用。
终止celery任务使用revoke指令。
self.result.revoke(terminate=True)。
self.result是一个result对象,不是id。
canshu terminate=True意思是是否立刻终止task。
为True时无论Task是否正在执行都立即终止,为False(默认)时需要等待Task运行结束之后才会终止,我们使用了While循环不设置为True就永远不会终止了。
终止celery的另外一个方法:
from webapp.celery import app
app.control.revoke(result.id, terminate=True)
send_message
方便我们通过Django的view或者Celery的task调用给channel发送消息,官方也比较推荐这种方式
https://blog.51cto.com/10983441/2421459
这里有celery的说明
settings.py
INSTALLED_APPS = [
'django_celery_beat',
'django_celery_results',
]
CELERY_RESULT_BACKEND = 'django-db'
CELERY_BROKER_URL = 'redis://10.0.0.61:6379/1'
CELERY_TIMEZONE = 'UTC'
CELERY_ENABLE_UTC = True
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# 这是使用了django-celery默认的数据库调度模型,任务执行周期都被存在你指定的orm数据库中
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'
untitled/init.py
from __future__ import absolute_import, unicode_literals
from untitled.celery import app as celery_app
__all__ = ['celery_app']
import pymysql
pymysql.install_as_MySQLdb()
celery.py
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery,platforms
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'untitled.settings')
app = Celery('untitled')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task_manage modules from all registered Django testchannel configs.
app.autodiscover_tasks("")
# platforms.C_FORCE_ROOT = True
tasks.py
from celery import shared_task
import time
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.conf import settings
from celery import shared_task
@shared_task(name="tailf")
def tailf(id, channel_name):
channel_layer = get_channel_layer()
filename = settings.LOG_PATH[int(id)]
print("llllllllllllllllllll")
try:
with open(filename) as f:
# f.seek(0, 2)
# 一开始任务总是不执行,才print调试
# print("kkkkkkkkkk")
while True:
# print("dddd")
line = f.readline()
# print("ssssssssssss",line)
if line:
print(".............",line)
print(channel_name, line)
async_to_sync(channel_layer.send)(
channel_name,
{
"type": "send.message",
"message": str(line)
}
)
else:
time.sleep(0.5)
except Exception as e:
print(e)
@shared_task(name="add")
def add(x,y):
print("add")
return x+y
这里是从channels的外部发送消息给channel的。
async_to_sync(channel_layer.send)(
channel_name,
{
"type": "send.message",
"message": str(line)
}
)
channel_name 对应于传递给这个任务的channel_name,发送消息给这个名字的channel
type 对应于我们Channels的TailfConsumer类中的send_message方法,将方法中的_换成.即可
message 就是要发送给这个channel的具体信息
上边是发送给单Channel的情况,如果是需要发送到Group的话需要使用如下代码
async_to_sync(channel_layer.group_send)(
group_name,
{
'type': 'chat.message',
'message': 'messagehahahahah'
}
)
只需要将发送单channel的send改为group_send,channel_name改为group_name即可
需要特别注意的是:使用了channel layer之后一定要通过async_to_sync来异步执行
开启worker
celery -A untitled worker --loglevel=debug