T O P
Image

Django

channels实现WebSocke

本文将实现一个WebSocket聊天室

  • By - C灵C

  • 2019年5月27日 12:18






本例项目结构图如下图所示:

图10.39  channels结构图.png


通过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模块下新建一个名为templatesDirectory用来存放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      ]


djangowebrouting.pyProtocolTypeRouter列表中插入一个键,将根路由配置指向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将消息传输到ChatConsumerChatConsumer将接收该消息并将其转发到与房间名称对应的组。然后,同一组中的每个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/实时聊天吧。