本文将实现一个WebSocket聊天室
By - C灵C
本例项目结构图如下图所示:
通过pip install –U channels来安装最新版的channels,这里要注意Channels2.0仅支持Python3.5+ 和 Django1.11+。接下来通过pip install channels_redis安装channels_redis。随后在apps下创建chat模块,在setting.py文件中注册chat,具体代码如下。
01 INSTALLED_APPS = [
02 'django.contrib.admin',
03 'django.contrib.auth',
04 'django.contrib.contenttypes',
05 'django.contrib.sessions',
06 'django.contrib.messages',
07 'django.contrib.staticfiles',
10 'apps.chat', # 实时聊天模块
11 ]
在chat模块下新建一个名为templates的Directory用来存放HTML文件,在刚建好的templates文件夹中新建一个index.html的索引视图模板文件。具体代码如下:
12 <!DOCTYPE html>
13 <html>
14 <head>
15 <meta charset="utf-8"/>
16 <title>Chat Rooms</title>
17 </head>
18 <body>
19 What chat room would you like to enter?<br/>
20 <input id="room-name-input" type="text" size="100"/><br/>
21 <input id="room-name-submit" type="button" value="Enter"/>
22
23 <script>
24 document.querySelector('#room-name-input').focus();
25 document.querySelector('#room-name-input').onkeyup = function(e) {
26 if (e.keyCode === 13) { // enter, return
27 document.querySelector('#room-name-submit').click();
28 }
29 };
30
31 document.querySelector('#room-name-submit').onclick = function(e) {
32 var roomName = document.querySelector('#room-name-input').value;
33 window.location.pathname = '/chat/' + roomName + '/';
34 };
35 </script>
36 </body>
37 </html>
38
为房间视图添加业务代码,在chat/views.py添加如下代码:
39 from django.shortcuts import render
40 def index(request):
41 return render(request, 'index.html', {})
为调用这个视图函数,我们还需为其配置路由信息。在chat下创建urls.py文件,具体代码如下:
42 from django.urls import re_path
43 from . import views
44 urlpatterns = [
45 re_path(r'^$', views.index, name='index'),
46 ]
接下来将URLconf指向chat.urls模块。在djangoweb下的urls.py文件中添加一个导入,具体代码如下:
47 from django.contrib import admin
48 from django.urls import path, include
49 urlpatterns = [
50 path('admin/', admin.site.urls),
51 path('chat/', include(('apps.chat.urls', 'apps.chat'), namespace='chat')), # 实时聊天模块
52 ]
配置完成后,你可以启动项目,查看索引视图是否有效。在浏览器中输入http://127.0.0.1:8000/chat/,会看到“What chat room would you like to enter?”,接下来我们要做,输入房间名按enter键即可进入对应的聊天室。
配置Channels的空路由,在djangoweb下创建一个routing.py文件,具体代码如下:
53 # djangoweb/routing.py
54 from channels.routing import ProtocolTypeRouter
55
56 application = ProtocolTypeRouter({
57 # (http->django views is added by default)
58 })
在settings.py中注册channels并且将根路由配置指向channels,具体代码如下:
59 INSTALLED_APPS = [
60 'django.contrib.admin',
61 'django.contrib.auth',
62 'django.contrib.contenttypes',
63 'django.contrib.sessions',
64 'django.contrib.messages',
65 'django.contrib.staticfiles',
66 'apps.mail', # 邮件验证模块
67 'apps.log', # 日志配置模块
68 'apps.chat', # 实时聊天模块
69 'channels',
70 ]
71 # 注意替换自己项目的名称
72 WSGI_APPLICATION = 'djangoweb.wsgi.application'
在chat模块下的templates文件夹中创建room.html房间视图模板文件,具体代码如下所示:
73 <!DOCTYPE html>
74 <html>
75 <head>
76 <meta charset="utf-8"/>
77 <title>Chat Room</title>
78 </head>
79 <body>
80 <textarea id="chat-log" cols="100" rows="20"></textarea><br/>
81 <input id="chat-message-input" type="text" size="100"/><br/>
82 <input id="chat-message-submit" type="button" value="Send"/>
83 </body>
84 <script>
85 var roomName = {{ room_name_json }};
86
87 var chatSocket = new WebSocket(
88 'ws://' + window.location.host +
89 '/ws/chat/' + roomName + '/');
90
91 chatSocket.onmessage = function(e) {
92 var data = JSON.parse(e.data);
93 var message = data['message'];
94 document.querySelector('#chat-log').value += (message + '\n');
95 };
96
97 chatSocket.onclose = function(e) {
98 console.error('Chat socket closed unexpectedly');
99 };
100
101 document.querySelector('#chat-message-input').focus();
102 document.querySelector('#chat-message-input').onkeyup = function(e) {
103 if (e.keyCode === 13) { // enter, return
104 document.querySelector('#chat-message-submit').click();
105 }
106 };
107
108 document.querySelector('#chat-message-submit').onclick = function(e) {
109 var messageInputDom = document.querySelector('#chat-message-input');
110 var message = messageInputDom.value;
111 chatSocket.send(JSON.stringify({
112 'message': message
113 }));
114
115 messageInputDom.value = '';
116 };
117 </script>
118 </html>
在chat/viewx.py中添加一个处理视图的功能,具体代码如下所示:
119 from django.shortcuts import render
120 from django.utils.safestring import mark_safe
121 import json
122 def index(request):
123 return render(request, 'index.html', {})
124 def room(request, room_name):
125 return render(request, 'room.html', {
126 'room_name_json': mark_safe(json.dumps(room_name))
127 })
在chat/urls.py中添加一个视图路径,具体代码如下:
128 from django.urls import re_path
129 from . import views
130 urlpatterns = [
131 re_path(r'^$', views.index, name='index'),
132 re_path(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),
133 ]
接下来配置接收路径上的WebSocket连接的消费者。在chat模块下创建一个新文件consumers.py,具体代码如下:
134 # chat/consumers.py
135 from channels.generic.websocket import AsyncWebsocketConsumer
136 import json
137
138 class ChatConsumer(AsyncWebsocketConsumer):
139 def connect(self):
140 self.accept()
141
142 def disconnect(self, close_code):
143 Pass
144
145 def receive(self, text_data):
146 text_data_json = json.loads(text_data)
147 message = text_data_json['message']
148 # 发送信息到WebSocket
149 await self.send(text_data=json.dumps({
150 'message': message
151 }))
此时,这是一个同步的WebSocket,他并不会向同一个房间内的其他客户端传递消息,所以我们需要编写异步的使用者来提高性能。在chat模块下创建routing.py文件,具体代码如下:
152 # chat/routing.py
153 from django.urls import re_path
154 from . import consumers
155 websocket_urlpatterns = [
156 re_path(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
157 ]
在djangoweb下routing.py中ProtocolTypeRouter列表中插入一个键,将根路由配置指向chat.routing模块,具体代码如下:
158 # djangoweb/routing.py
159 from channels.auth import AuthMiddlewareStack
160 from channels.routing import ProtocolTypeRouter, URLRouter
161 import apps.chat.routing
162
163 application = ProtocolTypeRouter({
164 # (http->django views is added by default)
165 'websocket': AuthMiddlewareStack(
166 URLRouter(
167 apps.chat.routing.websocket_urlpatterns
168 )
169 ),
170 })
接下来需要让多个相同的ChatConsumer能够相互通信,请确保已安装好Redis并启动,编辑settings.py文件,具体代码如下:
171 # djangoweb/settings.py
172 ASGI_APPLICATION = 'djangoweb.routing.application'
173
174 CHANNEL_LAYERS = {
175 'default': {
176 'BACKEND': 'channels_redis.core.RedisChannelLayer',
177 'CONFIG': {
178 "hosts": [('127.0.0.1', 6379)],
179 },
180 },
181 }
配置完成后,在chat/consumers.py中替换成如下代码:
182 # chat/consumers.py
183 from channels.generic.websocket import WebsocketConsumer
184 from asgiref.sync import async_to_sync
185 import json
186
187 class ChatConsumer(AsyncWebsocketConsumer):
188 def connect(self):
189 # 'room_name'从URL路由中获取参数chat/routing.py ,打开与消费者的WebSocket连接。
190 # 每个使用者都有一个范围,其中包含有关其连接的信息,
191 # 特别是包括URL路由中的任何位置或关键字参数以及当前经过身份验证的用户
192 self.room_name = self.scope['url_route']['kwargs']['room_name']
193 # 直接从用户指定的房间名称构造Channels组名称,不进行任何引用或转义。
194 # 组名只能包含字母,数字,连字符和句点。
195 # 因此,此示例代码将在具有其他字符的房间名称上失败。
196 self.room_group_name = 'chat_%s' % self.room_name
197
198 # 进入房间
199 async_to_sync (self.channel_layer.group_add)(
200 self.room_group_name,
201 self.channel_name
202 )
203 # 接受WebSocket连接。
204 # 如果不在connect()方法中调用accept(),则拒绝并关闭连接。
205 # 如果您选择接受连接,建议将accept()作为connect()中的最后一个操作调用。
206 self.accept()
207
208 def disconnect(self, close_code):
209 # 离开房间
210 async_to_sync (self.channel_layer.group_discard)(
211 self.room_group_name,
212 self.channel_name
213 )
214
215 # 从WebSocket接收信息
216 def receive(self, text_data):
217 text_data_json = json.loads(text_data)
218 message = text_data_json['message']
219
220 # 发送信息到房间
221 async_to_sync (self.channel_layer.group_send)(
222 self.room_group_name,
223 {
224 'type': 'chat_message',
225 'message': message
226 }
227 )
228
229 # 从房间接收信息
230 def chat_message(self, event):
231 message = event['message']
232
233 # 发送信息到WebSocket
234 self.send(text_data=json.dumps({
235 'message': message
236 }))
当用户发布消息时,JavaScript函数将通过WebSocket将消息传输到ChatConsumer。ChatConsumer将接收该消息并将其转发到与房间名称对应的组。然后,同一组中的每个ChatConsumer(因此在同一个房间中)将接收来自该组的消息,并通过WebSocket将其转发回JavaScript,并将其附加到聊天日志中。
到目前为止,ChatConsumer为同步消费者,为了提供更高级别的性能,我们需要将其重写为异步,将下列代码替换到chat/consumers.py中:
237 # chat/consumers.py
238 from channels.generic.websocket import AsyncWebsocketConsumer
239 import json
240
241 class ChatConsumer(AsyncWebsocketConsumer):
242 async def connect(self):
243 # 'room_name'从URL路由中获取参数chat/routing.py ,打开与消费者的WebSocket连接。
244 # 每个使用者都有一个范围,其中包含有关其连接的信息,
245 # 特别是包括URL路由中的任何位置或关键字参数以及当前经过身份验证的用户
246 self.room_name = self.scope['url_route']['kwargs']['room_name']
247 # 直接从用户指定的房间名称构造Channels组名称,不进行任何引用或转义。
248 # 组名只能包含字母,数字,连字符和句点。
249 # 因此,此示例代码将在具有其他字符的房间名称上失败。
250 self.room_group_name = 'chat_%s' % self.room_name
251
252 # 进入房间
253 await self.channel_layer.group_add(
254 self.room_group_name,
255 self.channel_name
256 )
257 # 接受WebSocket连接。
258 # 如果不在connect()方法中调用accept(),则拒绝并关闭连接。
259 # 如果您选择接受连接,建议将accept()作为connect()中的最后一个操作调用。
260 await self.accept()
261
262 async def disconnect(self, close_code):
263 # 离开房间
264 await self.channel_layer.group_discard(
265 self.room_group_name,
266 self.channel_name
267 )
268
269 # 从WebSocket接收信息
270 async def receive(self, text_data):
271 text_data_json = json.loads(text_data)
272 message = text_data_json['message']
273
274 # 发送信息到房间
275 await self.channel_layer.group_send(
276 self.room_group_name,
277 {
278 'type': 'chat_message',
279 'message': message
280 }
281 )
282
283 # 从房间接收信息
284 async def chat_message(self, event):
285 message = event['message']
286
287 # 发送信息到WebSocket
288 await self.send(text_data=json.dumps({
289 'message': message
290 }))
至此,您的聊天服务器是完全异步的了,开始启动项目,浏览器打开http://127.0.0.1:8000/chat/c0c/实时聊天吧。
文章评论