diff --git a/channels/interfaces/websocket_twisted.py b/channels/interfaces/websocket_twisted.py index 8aa0b31..478d03b 100644 --- a/channels/interfaces/websocket_twisted.py +++ b/channels/interfaces/websocket_twisted.py @@ -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() diff --git a/channels/interfaces/wsgi.py b/channels/interfaces/wsgi.py index 8df49c0..b856b93 100644 --- a/channels/interfaces/wsgi.py +++ b/channels/interfaces/wsgi.py @@ -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) diff --git a/channels/response.py b/channels/response.py index 2b8d761..fa4557f 100644 --- a/channels/response.py +++ b/channels/response.py @@ -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 diff --git a/docs/concepts.rst b/docs/concepts.rst index 1f52294..e91cc64 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -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) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 72bff85..77ce6b0 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -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) diff --git a/docs/message-standards.rst b/docs/message-standards.rst index c018c39..405b23c 100644 --- a/docs/message-standards.rst +++ b/docs/message-standards.rst @@ -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.