Rename channels and change message format docs

This commit is contained in:
Andrew Godwin 2015-09-09 21:21:43 -05:00
parent fc52e3c5a2
commit 70caf7d171
6 changed files with 115 additions and 90 deletions

View File

@ -11,7 +11,7 @@ from channels import Channel, channel_backends, DEFAULT_CHANNEL_BACKEND
class InterfaceProtocol(WebSocketServerProtocol):
"""
Protocol which supports WebSockets and forwards incoming messages to
the django.websocket channels.
the websocket channels.
"""
def onConnect(self, request):
@ -23,22 +23,22 @@ class InterfaceProtocol(WebSocketServerProtocol):
def onOpen(self):
# Make sending channel
self.reply_channel = Channel.new_name("!django.websocket.send")
self.reply_channel = Channel.new_name("!websocket.send")
self.request_info["reply_channel"] = self.reply_channel
self.last_keepalive = time.time()
self.factory.protocols[self.reply_channel] = self
# Send news that this channel is open
Channel("django.websocket.connect").send(self.request_info)
Channel("websocket.connect").send(self.request_info)
def onMessage(self, payload, isBinary):
if isBinary:
Channel("django.websocket.receive").send(dict(
Channel("websocket.receive").send(dict(
self.request_info,
content = payload,
binary = True,
))
else:
Channel("django.websocket.receive").send(dict(
Channel("websocket.receive").send(dict(
self.request_info,
content = payload.decode("utf8"),
binary = False,
@ -62,13 +62,13 @@ class InterfaceProtocol(WebSocketServerProtocol):
def onClose(self, wasClean, code, reason):
if hasattr(self, "reply_channel"):
del self.factory.protocols[self.reply_channel]
Channel("django.websocket.disconnect").send(self.request_info)
Channel("websocket.disconnect").send(self.request_info)
def sendKeepalive(self):
"""
Sends a keepalive packet on the keepalive channel.
"""
Channel("django.websocket.keepalive").send(self.request_info)
Channel("websocket.keepalive").send(self.request_info)
self.last_keepalive = time.time()

View File

@ -15,7 +15,7 @@ class WSGIInterface(WSGIHandler):
super(WSGIInterface, self).__init__(*args, **kwargs)
def get_response(self, request):
request.reply_channel = Channel.new_name("django.wsgi.response")
Channel("django.wsgi.request", channel_backend=self.channel_backend).send(request.channel_encode())
request.reply_channel = Channel.new_name("http.response")
Channel("http.request", channel_backend=self.channel_backend).send(request.channel_encode())
channel, message = self.channel_backend.receive_many_blocking([request.reply_channel])
return HttpResponse.channel_decode(message)

View File

@ -1,5 +1,4 @@
from django.http import HttpResponse
from django.http.cookie import SimpleCookie
from six import PY3
@ -12,7 +11,7 @@ def encode_response(response):
"content": response.content,
"status_code": response.status_code,
"headers": list(response._headers.values()),
"cookies": {k: v.output(header="") for k, v in response.cookies.items()}
"cookies": [v.output(header="") for _, v in response.cookies.items()]
}
if PY3:
value["content"] = value["content"].decode('utf8')
@ -29,9 +28,9 @@ def decode_response(value):
content_type = value['content_type'],
status = value['status_code'],
)
for cookie in value['cookies'].values():
for cookie in value['cookies']:
response.cookies.load(cookie)
response._headers = {k.lower: (k, v) for k, v in value['headers']}
response._headers = {k.lower(): (k, v) for k, v in value['headers']}
return response

View File

@ -53,7 +53,7 @@ and producers running in different processes or on different machines.
Inside a network, we identify channels uniquely by a name string - you can
send to any named channel from any machine connected to the same channel
backend. If two different machines both write to the ``django.wsgi.request``
backend. If two different machines both write to the ``http.request``
channel, they're writing into the same channel.
How do we use channels?
@ -102,12 +102,12 @@ slightly more complex abstraction than that presented by Django views.
A view takes a request and returns a response; a consumer takes a channel
message and can write out zero to many other channel messages.
Now, let's make a channel for requests (called ``django.wsgi.request``),
and a channel per client for responses (e.g. ``django.wsgi.response.o4F2h2Fd``),
Now, let's make a channel for requests (called ``http.request``),
and a channel per client for responses (e.g. ``http.response.o4F2h2Fd``),
with the response channel a property (``reply_channel``) of the request message.
Suddenly, a view is merely another example of a consumer::
# Listens on django.wsgi.request.
# Listens on http.request
def my_consumer(message):
# Decode the request from JSON-compat to a full object
django_request = Request.decode(message.content)
@ -154,7 +154,7 @@ to the channel server they're listening on.
For this reason, Channels treats these as two different *channel types*, and
denotes a *response channel* by having the first character of the channel name
be the character ``!`` - e.g. ``!django.wsgi.response.f5G3fE21f``. *Normal
be the character ``!`` - e.g. ``!http.response.f5G3fE21f``. *Normal
channels* have no special prefix, but along with the rest of the response
channel name, they must contain only the characters ``a-z A-Z 0-9 - _``,
and be less than 200 characters long.
@ -186,14 +186,14 @@ set of channels (here, using Redis) to send updates to::
content=instance.content,
)
# Connected to django.websocket.connect
# Connected to websocket.connect
def ws_connect(message):
# Add to reader set
redis_conn.sadd("readers", message.reply_channel.name)
While this will work, there's a small problem - we never remove people from
the ``readers`` set when they disconnect. We could add a consumer that
listens to ``django.websocket.disconnect`` to do that, but we'd also need to
listens to ``websocket.disconnect`` to do that, but we'd also need to
have some kind of expiry in case an interface server is forced to quit or
loses power before it can send disconnect signals - your code will never
see any disconnect notification but the response channel is completely
@ -222,7 +222,7 @@ we don't need to; Channels has it built in, as a feature called Groups::
content=instance.content,
)
# Connected to django.websocket.connect and django.websocket.keepalive
# Connected to websocket.connect and websocket.keepalive
def ws_connect(message):
# Add to reader group
Group("liveblog").add(message.reply_channel)

View File

@ -12,7 +12,7 @@ First Consumers
---------------
Now, by default, Django will run things through Channels but it will also
tie in the URL router and view subsystem to the default ``django.wsgi.request``
tie in the URL router and view subsystem to the default ``http.request``
channel if you don't provide another consumer that listens to it - remember,
only one consumer can listen to any given channel.
@ -48,7 +48,7 @@ custom consumer we wrote above. Here's what that looks like::
"default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": {
"django.wsgi.request": "myproject.myapp.consumers.http_consumer",
"http.request": "myproject.myapp.consumers.http_consumer",
},
},
}
@ -74,19 +74,19 @@ serve HTTP requests from now on - and make this WebSocket consumer instead::
def ws_add(message):
Group("chat").add(message.reply_channel)
Hook it up to the ``django.websocket.connect`` channel like this::
Hook it up to the ``websocket.connect`` channel like this::
CHANNEL_BACKENDS = {
"default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": {
"django.websocket.connect": "myproject.myapp.consumers.ws_add",
"websocket.connect": "myproject.myapp.consumers.ws_add",
},
},
}
Now, let's look at what this is doing. It's tied to the
``django.websocket.connect`` channel, which means that it'll get a message
``websocket.connect`` channel, which means that it'll get a message
whenever a new WebSocket connection is opened by a client.
When it gets that message, it takes the ``reply_channel`` attribute from it, which
@ -100,12 +100,12 @@ don't keep track of the open/close states of the potentially thousands of
connections you have open at any one time.
The solution to this is that the WebSocket interface servers will send
periodic "keepalive" messages on the ``django.websocket.keepalive`` channel,
periodic "keepalive" messages on the ``websocket.keepalive`` channel,
so we can hook that up to re-add the channel (it's safe to add the channel to
a group it's already in - similarly, it's safe to discard a channel from a
group it's not in)::
# Connected to django.websocket.keepalive
# Connected to websocket.keepalive
def ws_keepalive(message):
Group("chat").add(message.reply_channel)
@ -114,8 +114,8 @@ just route both channels to the same consumer::
...
"ROUTING": {
"django.websocket.connect": "myproject.myapp.consumers.ws_add",
"django.websocket.keepalive": "myproject.myapp.consumers.ws_add",
"websocket.connect": "myproject.myapp.consumers.ws_add",
"websocket.keepalive": "myproject.myapp.consumers.ws_add",
},
...
@ -123,7 +123,7 @@ And, even though channels will expire out, let's add an explicit ``disconnect``
handler to clean up as people disconnect (most channels will cleanly disconnect
and get this called)::
# Connected to django.websocket.disconnect
# Connected to websocket.disconnect
def ws_disconnect(message):
Group("chat").discard(message.reply_channel)
@ -134,15 +134,15 @@ any message sent in to all connected clients. Here's all the code::
from channels import Channel, Group
# Connected to django.websocket.connect and django.websocket.keepalive
# Connected to websocket.connect and websocket.keepalive
def ws_add(message):
Group("chat").add(message.reply_channel)
# Connected to django.websocket.receive
# Connected to websocket.receive
def ws_message(message):
Group("chat").send(message.content)
# Connected to django.websocket.disconnect
# Connected to websocket.disconnect
def ws_disconnect(message):
Group("chat").discard(message.reply_channel)
@ -152,10 +152,10 @@ And what our routing should look like in ``settings.py``::
"default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": {
"django.websocket.connect": "myproject.myapp.consumers.ws_add",
"django.websocket.keepalive": "myproject.myapp.consumers.ws_add",
"django.websocket.receive": "myproject.myapp.consumers.ws_message",
"django.websocket.disconnect": "myproject.myapp.consumers.ws_disconnect",
"websocket.connect": "myproject.myapp.consumers.ws_add",
"websocket.keepalive": "myproject.myapp.consumers.ws_add",
"websocket.receive": "myproject.myapp.consumers.ws_message",
"websocket.disconnect": "myproject.myapp.consumers.ws_disconnect",
},
},
}
@ -332,7 +332,7 @@ name in the path of your WebSocket request (we'll ignore auth for now)::
from channels import Channel
from channels.decorators import channel_session
# Connected to django.websocket.connect
# Connected to websocket.connect
@channel_session
def ws_connect(message):
# Work out room name from path (ignore slashes)
@ -341,17 +341,17 @@ name in the path of your WebSocket request (we'll ignore auth for now)::
message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel)
# Connected to django.websocket.keepalive
# Connected to websocket.keepalive
@channel_session
def ws_add(message):
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
# Connected to django.websocket.receive
# Connected to websocket.receive
@channel_session
def ws_message(message):
Group("chat-%s" % message.channel_session['room']).send(content)
# Connected to django.websocket.disconnect
# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)
@ -400,7 +400,7 @@ have a ChatMessage model with ``message`` and ``room`` fields::
"content": message.content['message'],
})
# Connected to django.websocket.connect
# Connected to websocket.connect
@channel_session
def ws_connect(message):
# Work out room name from path (ignore slashes)
@ -409,12 +409,12 @@ have a ChatMessage model with ``message`` and ``room`` fields::
message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel)
# Connected to django.websocket.keepalive
# Connected to websocket.keepalive
@channel_session
def ws_add(message):
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
# Connected to django.websocket.receive
# Connected to websocket.receive
@channel_session
def ws_message(message):
# Stick the message onto the processing queue
@ -423,7 +423,7 @@ have a ChatMessage model with ``message`` and ``room`` fields::
"message": content,
})
# Connected to django.websocket.disconnect
# Connected to websocket.disconnect
@channel_session
def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel)

View File

@ -2,13 +2,27 @@ Message Standards
=================
Some standardised message formats are used for common message types - they
are detailed below.
are detailed below. Message formats are meant to be generic and offload as
much protocol-specific processing to the interface server as is reasonable;
thus, they should generally represent things at as high a level as makes sense.
Note: All consumers also receive the channel name as the keyword argument
"channel", so there is no need for separate type information to let
multi-channel consumers distinguish.
In addition to the standards outlined below, each message may contain a
``reply_channel``, which details where to send responses. Protocols with
separate connection and data receiving messages (like WebSockets) will only
contain the connection and detailed client information in the first message;
use the ``@channel_session`` decorator to persist this data to consumers of
the received data (the decorator will take care of handling persistence and
ordering guarantees on messages).
The length limit on channel names will be 200 characters.
All messages must be able to be encoded as JSON; channel backends don't
necessarily have to use JSON, but we consider it the lowest common denominator
for serialisation format compatability.
The size limit on messages is 1MB (while channel backends may support larger
sizes, all message formats should stay under this limit, which might include
multi-part messages where large content must be transferred).
The length limit on channel names is 200 printable ASCII characters.
HTTP Request
@ -16,29 +30,45 @@ HTTP Request
Represents a full-fledged, single HTTP request coming in from a client.
Standard channel name is ``http.request``.
Contains the following keys:
* GET: List of (key, value) tuples of GET variables
* POST: List of (key, value) tuples of POST variables
* COOKIES: Same as ``request.COOKIES``
* META: Same as ``request.META``
* path: Same as ``request.path``
* path_info: Same as ``request.path_info``
* method: Upper-cased HTTP method
* response_channel: Channel name to write response to
* GET: List of (key, value) tuples of GET variables (keys and values are strings)
* POST: List of (key, value) tuples of POST variables (keys and values are strings)
* COOKIES: Dict of cookies as {cookie_name: cookie_value} (names and values are strings)
* META: Dict of HTTP headers and info as defined in the Django Request docs (names and values are strings)
* path: String, full path to the requested page, without query string or domain
* path_info: String, like ``path`` but without any script prefix. Often just ``path``.
* method: String, upper-cased HTTP method
Should come with an associated ``reply_channel`` which accepts HTTP Responses.
HTTP Response
-------------
Sends a whole response to a client.
Sends either a part of a response or a whole response to a HTTP client - to do
streaming responses, several response messages are sent with ``more_content: True``
and the final one has the key omitted. Normal, single-shot responses do not
need the key at all.
Contains the following keys:
Due to the 1MB size limit on messages, some larger responses will have to be
sent multi-part to stay within the limit.
Only sent on reply channels.
Keys that must only be in the first message of a set:
* content_type: String, mimetype of content
* status_code: Integer, numerical HTTP status code
* cookies: List of cookies to set (as encoded cookie strings suitable for headers)
* headers: Dictionary of headers (key is header name, value is value, both strings)
All messages in a set can the following keys:
* content: String of content to send
* content_type: Mimetype of content
* status_code: Numerical HTTP status code
* headers: Dictionary of headers (key is header name, value is value)
* more_content: Boolean, signals the interface server should wait for another response chunk to stream.
HTTP Disconnect
@ -47,7 +77,9 @@ HTTP Disconnect
Send when a client disconnects early, before the response has been sent.
Only sent by long-polling-capable HTTP interface servers.
Contains the same keys as HTTP Request.
Standard channel name is ``http.disconnect``.
Contains no keys.
WebSocket Connection
@ -55,14 +87,9 @@ WebSocket Connection
Sent when a new WebSocket is connected.
Contains the following keys:
Standard channel name is ``websocket.connect``.
* GET: List of (key, value) tuples of GET variables
* COOKIES: Same as ``request.COOKIES``
* META: Same as ``request.META``
* path: Same as ``request.path``
* path_info: Same as ``request.path_info``
* reply_channel: Channel name to send responses on
Contains the same keys as HTTP Request, without the ``POST`` or ``method`` keys.
WebSocket Receive
@ -70,10 +97,12 @@ WebSocket Receive
Sent when a datagram is received on the WebSocket.
Contains the same keys as WebSocket Connection, plus:
Standard channel name is ``websocket.receive``.
* content: String content of the datagram
* binary: If the content is to be interpreted as text or binary
Contains the following keys:
* content: String content of the datagram.
* binary: Boolean, saying if the content is binary. If not present or false, content is a UTF8 string.
WebSocket Client Close
@ -81,25 +110,22 @@ WebSocket Client Close
Sent when the WebSocket is closed by either the client or the server.
Contains the same keys as WebSocket Connection, including reply_channel,
though nothing should be sent on it.
Standard channel name is ``websocket.disconnect``.
Contains no keys.
WebSocket Send
--------------
WebSocket Send/Close
--------------------
Sent by a Django consumer to send a message back over the WebSocket to
the client.
the client or close the client connection. The content is optional if close
is set, and close will happen after any content is sent, if some is present.
Only sent on reply channels.
Contains the keys:
* content: String content of the datagram
* binary: If the content is to be interpreted as text or binary
WebSocket Server Close
----------------------
Sent by a Django consumer to close the client's WebSocket.
Contains no keys.
* content: String content of the datagram.
* binary: If the content is to be interpreted as text or binary.
* close: Boolean. If set to True, will close the client connection.