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

View File

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

View File

@ -1,5 +1,4 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.http.cookie import SimpleCookie
from six import PY3 from six import PY3
@ -12,7 +11,7 @@ def encode_response(response):
"content": response.content, "content": response.content,
"status_code": response.status_code, "status_code": response.status_code,
"headers": list(response._headers.values()), "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: if PY3:
value["content"] = value["content"].decode('utf8') value["content"] = value["content"].decode('utf8')
@ -29,9 +28,9 @@ def decode_response(value):
content_type = value['content_type'], content_type = value['content_type'],
status = value['status_code'], status = value['status_code'],
) )
for cookie in value['cookies'].values(): for cookie in value['cookies']:
response.cookies.load(cookie) 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 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 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 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. channel, they're writing into the same channel.
How do we use channels? 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 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. message and can write out zero to many other channel messages.
Now, let's make a channel for requests (called ``django.wsgi.request``), Now, let's make a channel for requests (called ``http.request``),
and a channel per client for responses (e.g. ``django.wsgi.response.o4F2h2Fd``), and a channel per client for responses (e.g. ``http.response.o4F2h2Fd``),
with the response channel a property (``reply_channel``) of the request message. with the response channel a property (``reply_channel``) of the request message.
Suddenly, a view is merely another example of a consumer:: Suddenly, a view is merely another example of a consumer::
# Listens on django.wsgi.request. # Listens on http.request
def my_consumer(message): def my_consumer(message):
# Decode the request from JSON-compat to a full object # Decode the request from JSON-compat to a full object
django_request = Request.decode(message.content) 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 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 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 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 - _``, channel name, they must contain only the characters ``a-z A-Z 0-9 - _``,
and be less than 200 characters long. and be less than 200 characters long.
@ -186,14 +186,14 @@ set of channels (here, using Redis) to send updates to::
content=instance.content, content=instance.content,
) )
# Connected to django.websocket.connect # Connected to websocket.connect
def ws_connect(message): def ws_connect(message):
# Add to reader set # Add to reader set
redis_conn.sadd("readers", message.reply_channel.name) redis_conn.sadd("readers", message.reply_channel.name)
While this will work, there's a small problem - we never remove people from 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 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 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 loses power before it can send disconnect signals - your code will never
see any disconnect notification but the response channel is completely 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, content=instance.content,
) )
# Connected to django.websocket.connect and django.websocket.keepalive # Connected to websocket.connect and websocket.keepalive
def ws_connect(message): def ws_connect(message):
# Add to reader group # Add to reader group
Group("liveblog").add(message.reply_channel) 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 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, channel if you don't provide another consumer that listens to it - remember,
only one consumer can listen to any given channel. 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": { "default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend", "BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": { "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): def ws_add(message):
Group("chat").add(message.reply_channel) 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 = { CHANNEL_BACKENDS = {
"default": { "default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend", "BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": { "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 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. whenever a new WebSocket connection is opened by a client.
When it gets that message, it takes the ``reply_channel`` attribute from it, which 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. connections you have open at any one time.
The solution to this is that the WebSocket interface servers will send 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 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 a group it's already in - similarly, it's safe to discard a channel from a
group it's not in):: group it's not in)::
# Connected to django.websocket.keepalive # Connected to websocket.keepalive
def ws_keepalive(message): def ws_keepalive(message):
Group("chat").add(message.reply_channel) Group("chat").add(message.reply_channel)
@ -114,8 +114,8 @@ just route both channels to the same consumer::
... ...
"ROUTING": { "ROUTING": {
"django.websocket.connect": "myproject.myapp.consumers.ws_add", "websocket.connect": "myproject.myapp.consumers.ws_add",
"django.websocket.keepalive": "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 handler to clean up as people disconnect (most channels will cleanly disconnect
and get this called):: and get this called)::
# Connected to django.websocket.disconnect # Connected to websocket.disconnect
def ws_disconnect(message): def ws_disconnect(message):
Group("chat").discard(message.reply_channel) 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 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): def ws_add(message):
Group("chat").add(message.reply_channel) Group("chat").add(message.reply_channel)
# Connected to django.websocket.receive # Connected to websocket.receive
def ws_message(message): def ws_message(message):
Group("chat").send(message.content) Group("chat").send(message.content)
# Connected to django.websocket.disconnect # Connected to websocket.disconnect
def ws_disconnect(message): def ws_disconnect(message):
Group("chat").discard(message.reply_channel) Group("chat").discard(message.reply_channel)
@ -152,10 +152,10 @@ And what our routing should look like in ``settings.py``::
"default": { "default": {
"BACKEND": "channels.backends.database.DatabaseChannelBackend", "BACKEND": "channels.backends.database.DatabaseChannelBackend",
"ROUTING": { "ROUTING": {
"django.websocket.connect": "myproject.myapp.consumers.ws_add", "websocket.connect": "myproject.myapp.consumers.ws_add",
"django.websocket.keepalive": "myproject.myapp.consumers.ws_add", "websocket.keepalive": "myproject.myapp.consumers.ws_add",
"django.websocket.receive": "myproject.myapp.consumers.ws_message", "websocket.receive": "myproject.myapp.consumers.ws_message",
"django.websocket.disconnect": "myproject.myapp.consumers.ws_disconnect", "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 import Channel
from channels.decorators import channel_session from channels.decorators import channel_session
# Connected to django.websocket.connect # Connected to websocket.connect
@channel_session @channel_session
def ws_connect(message): def ws_connect(message):
# Work out room name from path (ignore slashes) # 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 message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel) Group("chat-%s" % room).add(message.reply_channel)
# Connected to django.websocket.keepalive # Connected to websocket.keepalive
@channel_session @channel_session
def ws_add(message): def ws_add(message):
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel) Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
# Connected to django.websocket.receive # Connected to websocket.receive
@channel_session @channel_session
def ws_message(message): def ws_message(message):
Group("chat-%s" % message.channel_session['room']).send(content) Group("chat-%s" % message.channel_session['room']).send(content)
# Connected to django.websocket.disconnect # Connected to websocket.disconnect
@channel_session @channel_session
def ws_disconnect(message): def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel) 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'], "content": message.content['message'],
}) })
# Connected to django.websocket.connect # Connected to websocket.connect
@channel_session @channel_session
def ws_connect(message): def ws_connect(message):
# Work out room name from path (ignore slashes) # 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 message.channel_session['room'] = room
Group("chat-%s" % room).add(message.reply_channel) Group("chat-%s" % room).add(message.reply_channel)
# Connected to django.websocket.keepalive # Connected to websocket.keepalive
@channel_session @channel_session
def ws_add(message): def ws_add(message):
Group("chat-%s" % message.channel_session['room']).add(message.reply_channel) Group("chat-%s" % message.channel_session['room']).add(message.reply_channel)
# Connected to django.websocket.receive # Connected to websocket.receive
@channel_session @channel_session
def ws_message(message): def ws_message(message):
# Stick the message onto the processing queue # Stick the message onto the processing queue
@ -423,7 +423,7 @@ have a ChatMessage model with ``message`` and ``room`` fields::
"message": content, "message": content,
}) })
# Connected to django.websocket.disconnect # Connected to websocket.disconnect
@channel_session @channel_session
def ws_disconnect(message): def ws_disconnect(message):
Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel) 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 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 In addition to the standards outlined below, each message may contain a
"channel", so there is no need for separate type information to let ``reply_channel``, which details where to send responses. Protocols with
multi-channel consumers distinguish. 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 HTTP Request
@ -16,29 +30,45 @@ HTTP Request
Represents a full-fledged, single HTTP request coming in from a client. Represents a full-fledged, single HTTP request coming in from a client.
Standard channel name is ``http.request``.
Contains the following keys: Contains the following keys:
* GET: List of (key, value) tuples of GET variables * GET: List of (key, value) tuples of GET variables (keys and values are strings)
* POST: List of (key, value) tuples of POST variables * POST: List of (key, value) tuples of POST variables (keys and values are strings)
* COOKIES: Same as ``request.COOKIES`` * COOKIES: Dict of cookies as {cookie_name: cookie_value} (names and values are strings)
* META: Same as ``request.META`` * META: Dict of HTTP headers and info as defined in the Django Request docs (names and values are strings)
* path: Same as ``request.path`` * path: String, full path to the requested page, without query string or domain
* path_info: Same as ``request.path_info`` * path_info: String, like ``path`` but without any script prefix. Often just ``path``.
* method: Upper-cased HTTP method * method: String, upper-cased HTTP method
* response_channel: Channel name to write response to
Should come with an associated ``reply_channel`` which accepts HTTP Responses.
HTTP Response 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: String of content to send
* content_type: Mimetype of content * more_content: Boolean, signals the interface server should wait for another response chunk to stream.
* status_code: Numerical HTTP status code
* headers: Dictionary of headers (key is header name, value is value)
HTTP Disconnect HTTP Disconnect
@ -47,7 +77,9 @@ HTTP Disconnect
Send when a client disconnects early, before the response has been sent. Send when a client disconnects early, before the response has been sent.
Only sent by long-polling-capable HTTP interface servers. 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 WebSocket Connection
@ -55,14 +87,9 @@ WebSocket Connection
Sent when a new WebSocket is connected. 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 Contains the same keys as HTTP Request, without the ``POST`` or ``method`` keys.
* 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
WebSocket Receive WebSocket Receive
@ -70,10 +97,12 @@ WebSocket Receive
Sent when a datagram is received on the WebSocket. 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 Contains the following keys:
* binary: If the content is to be interpreted as text or binary
* 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 WebSocket Client Close
@ -81,25 +110,22 @@ WebSocket Client Close
Sent when the WebSocket is closed by either the client or the server. Sent when the WebSocket is closed by either the client or the server.
Contains the same keys as WebSocket Connection, including reply_channel, Standard channel name is ``websocket.disconnect``.
though nothing should be sent on it.
Contains no keys.
WebSocket Send WebSocket Send/Close
-------------- --------------------
Sent by a Django consumer to send a message back over the WebSocket to 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: Contains the keys:
* content: String content of the datagram * content: String content of the datagram.
* binary: If the content is to be interpreted as text or binary * binary: If the content is to be interpreted as text or binary.
* close: Boolean. If set to True, will close the client connection.
WebSocket Server Close
----------------------
Sent by a Django consumer to close the client's WebSocket.
Contains no keys.