在 Angular 2+ 客户端和 Django 后端之间打开 WebSocket 连接
Opening a WebSocket connection between Angular 2+ client and Django backend
我正在尝试使用 Django Channels 从我的 angular 2+ 应用程序与我的 Django 后端打开一个 websocket 连接。
我完成了本教程:https://www.youtube.com/watch?v=RVH05S1qab8
并设法让所有与 javascript 部分一起工作的东西都写在 Django html 模板中。但是当我将前端聊天表单迁移到单独的 angular 2 应用程序时,我只是打开一个 websocket 连接时遇到了问题。
字体端和后端都在本地托管。
前端 - 聊天表单模板
<div *ngFor="let message of thread.messages">
<div>{{message.message}}</div>
</div>
<form #formData [formGroup]="form">
<div class="form-group">
<input formControlName="messageInput" #msgInput type="text" class="form-control" id="newMessage" placeholder="Type a message">
</div>
<button (click)="onSubmit($event)" type="submit" class="btn btn-primary">Send</button>
</form>
</div>
前端 - 聊天表单组件
otherUser: User;
me: User;
threadId: number;
thread: Thread;
endpoint: string;
socket: WebSocket;
form = new FormGroup({
messageInput: new FormControl("")
});
get messageInput() {
return this.form.get('messageInput');
}
getThreadById(id: number) {
this._chatService.getThreadById(id).subscribe(thread => {
console.log("response: thread found", thread);
this.thread = thread;
},
error => {
console.log("error", error);
});
}
getEndpoint() {
let loc = window.location
let wsStart = "ws://"
if (loc.protocol == "https:") {
wsStart = 'wss://'
}
this.endpoint = wsStart + loc.host + loc.pathname;
return this.endpoint;
}
constructor(private _activatedRoute: ActivatedRoute) {
}
ngOnInit() {
this._activatedRoute.params.subscribe(params => { this.threadId = params['id']; });
if (this.threadId) {
this.getThreadById(this.threadId);
}
this.getEndpoint();
this.createSocket();
}
createSocket() {
this.socket = new WebSocket(this.endpoint);
console.log("endpoint ", this.endpoint);
this.socket.onopen = (e) => {
console.log("open", e);
}
this.socket.onmessage = (e) => {
console.log("message", e);
let chatDataMsg = JSON.parse(e.data);
this.thread.messages.push(chatDataMsg.message);
}
this.socket.onerror = (e) => {
console.log("error", e);
}
this.socket.onclose = (e) => {
console.log("close", e);
}
}
onSubmit($event) {
let msgText = this.messageInput.value;
let finalData = {
'message': msgText
};
let chatMessage: ChatMessage = new ChatMessage(this.thread, this.me, new Date(), msgText);
this.socket.send(JSON.stringify(finalData))
this.form.reset();
}
Django 后端项目设置
ASGI_APPLICATION = "joole.routing.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("localhost", 6379)],
#"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')]
},
},
}
ALLOWED_HOSTS = ['joole-api.herokuapp.com', '.herokuapp.com', '127.0.0.1']
Django Websocket 路由
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator, OriginValidator
from chat.consumers import ChatConsumer
application = ProtocolTypeRouter({
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
[
url(r"^messages/(?P<id>[\w.@+-]+)/$", ChatConsumer)
]
)
)
)
})
Django 项目网址
from django.urls import path, include
from django.conf import settings
urlpatterns = [
path('messages/', include('chat.urls')),
]
Django WebSocket 消费者
import asyncio
import json
from users.models import CustomUser, Employee
from channels.consumer import AsyncConsumer
from channels.db import database_sync_to_async
from .models import Thread, ChatMessage
class ChatConsumer(AsyncConsumer):
async def websocket_connect(self, event):
print("CONNECTED", event)
thread_id = self.scope['url_route']['kwargs']['id']
thread_obj = await self.get_thread(thread_id)
self.thread_obj = thread_obj
chat_room = f"thread_{thread_obj.id}"
self.chat_room = chat_room
await self.channel_layer.group_add(
chat_room,
self.channel_name
)
await self.send({
"type": 'websocket.accept'
})
async def websocket_receive(self, event):
print("receive", event)
front_text = event.get('text', None)
if front_text is not None:
loaded_dict_data = json.loads(front_text)
msg = loaded_dict_data.get('message')
user = self.scope['user']
username = 'default'
if user.is_authenticated:
username = user.email
myResponse = {
'message': msg,
'username': user.email
}
await self.create_chat_message(user, msg)
# broadcasts the message event to be sent
await self.channel_layer.group_send(
self.chat_room,
{
"type": "chat_message",
"text": json.dumps(myResponse)
}
)
async def chat_message(self, event):
# send the actual message event
print("message", event)
await self.send({
"type": "websocket.send",
"text": event["text"]
})
async def websocket_disconnect(self, event):
print("disconnected", event)
@database_sync_to_async
def get_thread(self, id):
return Thread.objects.get(id=id)
@database_sync_to_async
def create_chat_message(self, me, msg):
thread_obj = self.thread_obj
return ChatMessage.objects.create(thread=thread_obj, user=me, message=msg)
Django 聊天模型
class Thread(models.Model):
first = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_first')
second = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_second')
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
@property
def room_group_name(self):
return f'chat_{self.id}'
def broadcast(self, msg=None):
if msg is not None:
broadcast_msg_to_chat(msg, group_name=self.room_group_name, user='admin')
return True
return False
class ChatMessage(models.Model):
thread = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.SET_NULL, related_name="messages")
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='sender', on_delete=models.CASCADE)
message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
Django 聊天网址
from django.urls import include, path
from rest_framework import routers
from .views import ThreadViewSet, ChatMessageViewSet
router = routers.DefaultRouter()
router.register('thread', ThreadViewSet)
router.register('chatMessage', ChatMessageViewSet)
urlpatterns = [
path('', include(router.urls)),
]
预期输出
我期待 "CONNECTED" 消息,根据消费者 class 中的 websocket_connect 方法在控制台上打印事件信息。类似地,带有事件的 "open" 消息在我的浏览器控制台中从 Angular 2+ chat.component.ts.
实际输出
我立即在浏览器控制台上收到此警告消息:
与 'ws://localhost:4200/sockjs-node/344/ux0z32ma/websocket' 的 WebSocket 连接失败:WebSocket 在建立连接之前关闭。
等了大约2分钟后...
Attached image shows what is automatically output on the console.
我可能漏掉了什么,因为我看不到 Django 配置,但据我了解,您写道您 运行 分别是前端和后端服务器。您可以看到前端正在尝试建立与 localhost:4200
的连接。我相信那是没有意义的 angular 服务器,你应该将你的 WebSocket 指向 Django 应用程序,所以在我看来你应该修改下面的方法:
SERVER_URL = "localhost:8000"
getEndpoint() {
let wsStart = "ws://"
if (window.location.protocol == "https:") {
wsStart = 'wss://'
}
this.endpoint = wsStart + SERVER_URL + window.location.pathname;
return this.endpoint;
}
但是,如果您在 Django 上托管这两个应用程序并将其暴露在不适用的端口 4200 上,尽管我很确定这不是您的情况。
我正在尝试使用 Django Channels 从我的 angular 2+ 应用程序与我的 Django 后端打开一个 websocket 连接。 我完成了本教程:https://www.youtube.com/watch?v=RVH05S1qab8 并设法让所有与 javascript 部分一起工作的东西都写在 Django html 模板中。但是当我将前端聊天表单迁移到单独的 angular 2 应用程序时,我只是打开一个 websocket 连接时遇到了问题。 字体端和后端都在本地托管。
前端 - 聊天表单模板
<div *ngFor="let message of thread.messages">
<div>{{message.message}}</div>
</div>
<form #formData [formGroup]="form">
<div class="form-group">
<input formControlName="messageInput" #msgInput type="text" class="form-control" id="newMessage" placeholder="Type a message">
</div>
<button (click)="onSubmit($event)" type="submit" class="btn btn-primary">Send</button>
</form>
</div>
前端 - 聊天表单组件
otherUser: User;
me: User;
threadId: number;
thread: Thread;
endpoint: string;
socket: WebSocket;
form = new FormGroup({
messageInput: new FormControl("")
});
get messageInput() {
return this.form.get('messageInput');
}
getThreadById(id: number) {
this._chatService.getThreadById(id).subscribe(thread => {
console.log("response: thread found", thread);
this.thread = thread;
},
error => {
console.log("error", error);
});
}
getEndpoint() {
let loc = window.location
let wsStart = "ws://"
if (loc.protocol == "https:") {
wsStart = 'wss://'
}
this.endpoint = wsStart + loc.host + loc.pathname;
return this.endpoint;
}
constructor(private _activatedRoute: ActivatedRoute) {
}
ngOnInit() {
this._activatedRoute.params.subscribe(params => { this.threadId = params['id']; });
if (this.threadId) {
this.getThreadById(this.threadId);
}
this.getEndpoint();
this.createSocket();
}
createSocket() {
this.socket = new WebSocket(this.endpoint);
console.log("endpoint ", this.endpoint);
this.socket.onopen = (e) => {
console.log("open", e);
}
this.socket.onmessage = (e) => {
console.log("message", e);
let chatDataMsg = JSON.parse(e.data);
this.thread.messages.push(chatDataMsg.message);
}
this.socket.onerror = (e) => {
console.log("error", e);
}
this.socket.onclose = (e) => {
console.log("close", e);
}
}
onSubmit($event) {
let msgText = this.messageInput.value;
let finalData = {
'message': msgText
};
let chatMessage: ChatMessage = new ChatMessage(this.thread, this.me, new Date(), msgText);
this.socket.send(JSON.stringify(finalData))
this.form.reset();
}
Django 后端项目设置
ASGI_APPLICATION = "joole.routing.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("localhost", 6379)],
#"hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')]
},
},
}
ALLOWED_HOSTS = ['joole-api.herokuapp.com', '.herokuapp.com', '127.0.0.1']
Django Websocket 路由
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator, OriginValidator
from chat.consumers import ChatConsumer
application = ProtocolTypeRouter({
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
[
url(r"^messages/(?P<id>[\w.@+-]+)/$", ChatConsumer)
]
)
)
)
})
Django 项目网址
from django.urls import path, include
from django.conf import settings
urlpatterns = [
path('messages/', include('chat.urls')),
]
Django WebSocket 消费者
import asyncio
import json
from users.models import CustomUser, Employee
from channels.consumer import AsyncConsumer
from channels.db import database_sync_to_async
from .models import Thread, ChatMessage
class ChatConsumer(AsyncConsumer):
async def websocket_connect(self, event):
print("CONNECTED", event)
thread_id = self.scope['url_route']['kwargs']['id']
thread_obj = await self.get_thread(thread_id)
self.thread_obj = thread_obj
chat_room = f"thread_{thread_obj.id}"
self.chat_room = chat_room
await self.channel_layer.group_add(
chat_room,
self.channel_name
)
await self.send({
"type": 'websocket.accept'
})
async def websocket_receive(self, event):
print("receive", event)
front_text = event.get('text', None)
if front_text is not None:
loaded_dict_data = json.loads(front_text)
msg = loaded_dict_data.get('message')
user = self.scope['user']
username = 'default'
if user.is_authenticated:
username = user.email
myResponse = {
'message': msg,
'username': user.email
}
await self.create_chat_message(user, msg)
# broadcasts the message event to be sent
await self.channel_layer.group_send(
self.chat_room,
{
"type": "chat_message",
"text": json.dumps(myResponse)
}
)
async def chat_message(self, event):
# send the actual message event
print("message", event)
await self.send({
"type": "websocket.send",
"text": event["text"]
})
async def websocket_disconnect(self, event):
print("disconnected", event)
@database_sync_to_async
def get_thread(self, id):
return Thread.objects.get(id=id)
@database_sync_to_async
def create_chat_message(self, me, msg):
thread_obj = self.thread_obj
return ChatMessage.objects.create(thread=thread_obj, user=me, message=msg)
Django 聊天模型
class Thread(models.Model):
first = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_first')
second = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chat_thread_second')
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
@property
def room_group_name(self):
return f'chat_{self.id}'
def broadcast(self, msg=None):
if msg is not None:
broadcast_msg_to_chat(msg, group_name=self.room_group_name, user='admin')
return True
return False
class ChatMessage(models.Model):
thread = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.SET_NULL, related_name="messages")
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='sender', on_delete=models.CASCADE)
message = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
Django 聊天网址
from django.urls import include, path
from rest_framework import routers
from .views import ThreadViewSet, ChatMessageViewSet
router = routers.DefaultRouter()
router.register('thread', ThreadViewSet)
router.register('chatMessage', ChatMessageViewSet)
urlpatterns = [
path('', include(router.urls)),
]
预期输出 我期待 "CONNECTED" 消息,根据消费者 class 中的 websocket_connect 方法在控制台上打印事件信息。类似地,带有事件的 "open" 消息在我的浏览器控制台中从 Angular 2+ chat.component.ts.
实际输出
我立即在浏览器控制台上收到此警告消息:
与 'ws://localhost:4200/sockjs-node/344/ux0z32ma/websocket' 的 WebSocket 连接失败:WebSocket 在建立连接之前关闭。
等了大约2分钟后... Attached image shows what is automatically output on the console.
我可能漏掉了什么,因为我看不到 Django 配置,但据我了解,您写道您 运行 分别是前端和后端服务器。您可以看到前端正在尝试建立与 localhost:4200
的连接。我相信那是没有意义的 angular 服务器,你应该将你的 WebSocket 指向 Django 应用程序,所以在我看来你应该修改下面的方法:
SERVER_URL = "localhost:8000"
getEndpoint() {
let wsStart = "ws://"
if (window.location.protocol == "https:") {
wsStart = 'wss://'
}
this.endpoint = wsStart + SERVER_URL + window.location.pathname;
return this.endpoint;
}
但是,如果您在 Django 上托管这两个应用程序并将其暴露在不适用的端口 4200 上,尽管我很确定这不是您的情况。