Documentation that's up to date.

This commit is contained in:
James Coglan
2013-04-20 19:35:31 +01:00
parent 787697dc59
commit 9cfac60925
+182 -95
View File
@@ -1,127 +1,214 @@
= faye-websocket-parser {<img src="https://secure.travis-ci.org/faye/faye-websocket-parser-ruby.png" />}[http://travis-ci.org/faye/faye-websocket-ruby]
= websocket-protocol {<img src="https://secure.travis-ci.org/faye/websocket-protocol-ruby.png" />}[http://travis-ci.org/faye/websocket-protocol-ruby]
This library is a set of WebSocket parsers extracted from the
{Faye}[http://faye.jcoglan.com] project. It provides parsing and serialization
functionality for the {draft-75}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75],
{draft-76}[http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76],
and {hybi-07}[http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07]
and later versions of the protocol, and can select the right parser to use
based on the client handshake. It can be used with any Ruby TCP library to
create both servers and clients.
This module provides a complete implementation of the WebSocket protocols that
can be hooked up to any TCP library. It aims to simplify things by decoupling
the protocol details from the I/O layer, such that users only need to implement
code to stream data in and out of it without needing to know anything about how
the protocol actually works. Think of it as a complete WebSocket system with
pluggable I/O.
Due to this design, you get a lot of things for free. In particular, if you
hook this module up to some I/O object, it will do all of this for you:
* Select the correct server-side protocol handler to talk to the client
* Generate and sent both server- and client-side handshakes
* Recognize when the handshake phase completes and the WS protocol begins
* Negotiate subprotocol selection based on <tt>Sec-WebSocket-Protocol</tt>
* Buffer sent messages until the handshake process is finished
* Deal with proxies that defer delivery of the draft-76 handshake body
* Notify you when the socket is open and closed and when messages arrive
* Recombine fragmented messages
* Dispatch text, binary, ping and close frames
* Manage the socket-closing handhake process
* Automatically reply to ping frames with a matching pong
* Apply masking to messages sent by the client
This library was originally extracted from the {Faye}[http://faye.jcoglan.com]
project but now aims to provide simple WebSocket support for any Ruby server or
I/O system.
== Usage
For building server-side sockets, you need to provide a Rack-style +env+ hash
representing the client handshake for Faye to select the right parser for you.
For client-side sockets, only the <tt>Faye::WebSocket::HybiParser</tt> may be
used.
To build either a server-side or client-side socket, the only requirement is
that you supply a +socket+ object with these methods:
* <tt>socket.url</tt> - returns the full URL of the socket as a string.
* <tt>socket.write(string)</tt> - writes the given string to a TCP stream.
Server-side sockets require one additional method:
* <tt>socket.env</tt> - returns a Rack-style env hash that will contain some of
the following fields. Their values are strings containing the value of the
named header, unless stated otherwise.
* +HTTP_CONNECTION+
* +HTTP_HOST+
* +HTTP_ORIGIN+
* +HTTP_SEC_WEBSOCKET_KEY+
* +HTTP_SEC_WEBSOCKET_KEY1+
* +HTTP_SEC_WEBSOCKET_KEY2+
* +HTTP_SEC_WEBSOCKET_PROTOCOL+
* +HTTP_SEC_WEBSOCKET_VERSION+
* +HTTP_UPGRADE+
* <tt>rack.input</tt>, an +IO+ object representing the request body
* +REQUEST_METHOD+, the request's HTTP verb
=== Server-side
To process the server side of a WebSocket connection, you need to build a Rack
+env+ hash. This hash will contain a subset of following keys as strings, which
map to string header values unless states otherwise. The set of keys present
will depend on which version of the protocol the client is using.
To handle a server-side WebSocket connection, you need to check whether the
request is a WebSocket handshake, and if so create a protocol handler for it.
You must give the handler an object with the +env+, +url+ and +write+ methods.
A simple example might be:
* +HTTP_CONNECTION+
* +HTTP_HOST+
* +HTTP_ORIGIN+
* +HTTP_SEC_WEBSOCKET_KEY+
* +HTTP_SEC_WEBSOCKET_KEY1+
* +HTTP_SEC_WEBSOCKET_KEY2+
* +HTTP_SEC_WEBSOCKET_PROTOCOL+
* +HTTP_SEC_WEBSOCKET_VERSION+
* +HTTP_UPGRADE+
* +HTTP_X_FORWARDED_PROTO+, should have the value <tt>'https'</tt> if the
socket is using an encrypted connection
* <tt>rack.input</tt>, an +IO+ object representing the request body
* +REQUEST_METHOD+, the request's HTTP verb
* +REQUEST_URI+, the request's path and query string
require 'websocket/protocol'
require 'eventmachine'
Given an +env+ hash, the first thing to do is check whether it represents a
valid WebSocket request.
class WS
attr_reader :env, :url
is_websocket = Faye::WebSocket.websocket?(env)
def initialize(env)
@env = env
If it is a valid WebSocket, you can get a parser for it like so:
secure = Rack::Request.new(env).ssl?
scheme = secure ? 'ws:' : 'wss:'
@url = scheme + '//' + env['HTTP_HOST'] + env['REQUEST_URI']
parser = Faye::WebSocket.parser(env).new(handler, options)
@handler = WebSocket::Protocol.server(self)
See 'Parser API' below for the +parser+ interface and the API expected on the
+handler+ object. +options+ is an optional hash containing:
env['rack.hijack'].call
@io = env['rack.hijack_io']
* <tt>:masking</tt> - optional, boolean flag saying whether to mask outgoing
message frames. This is not required for server-side handlers.
* <tt>:protocols</tt> - optional, an array of strings that will be used to set
the <tt>Sec-WebSocket-Protocol</tt> header for subprotocol negotiation.
EM.attach(@io, Reader) { |conn| conn.handler = @handler }
To complete the connection, you need to send a response to the client's
handshake, which you do by writing <tt>parser.handshake_response</tt> to the
TCP socket:
@handler.start
end
socket.write(parser.handshake_response)
def write(string)
@io.write(string)
end
At this point the handshake might still not be complete. In some situations
part of the client's handshake is deferred until the server has sent this
response, and the server should not begin sending message frames until the
handshake is done. You should check the connection is ready to exchange
messages using:
module Reader
attr_writer :handler
can_send_messages = parser.open?
def receive_data(string)
@handler.parse(string)
end
end
end
After this setup is done, use the parser API detailed below to process data.
To explain what's going on here: the +WS+ class implements the +env+, +url+ and
+write(string)+ methods as required. When instantiated with a Rack environment,
it stores the environment and infers the complete URL from it. Having set up
the +env+ and +url+, it asks <tt>WebSocket::Protocol</tt> for a server-side
handler for the socket. Then it uses the Rack hijack API to gain access to the
TCP stream, and uses EventMachine to stream in incoming data from the client,
handing incoming data off to the handler for parsing. Finally, we tell the
handler to +start+, which will begin sending the handshake response. This will
invoke the <tt>WS#write</tt> method, which will send the response out over the
TCP socket.
Having defined this class we could use it like this when handling a request:
if WebSocket::Protocol.websocket?(env)
socket = WS.new(env)
end
The handler API is described in full below.
=== Client-side
If you're building a client, construct a parser like this:
Similarly, to implement a WebSocket client you just need an object with +url+
and +write+ methods. Once you have one such object, you ask for a handler for
it:
parser = Faye::WebSocket:HybiParser.new(handler, options)
handler = WebSocket::Protocol.client(socket)
The +parser+ interface and the required +handler+ API is documented below under
'Parser API'. +options+ is a hash containing the following keys:
* <tt>:masking</tt> - must have the value +true+.
* <tt>:protocols</tt> - optional, an array of strings that will be used to set
the <tt>Sec-WebSocket-Protocol</tt> header for subprotocol negotiation.
To make a connection, you must send a WebSocket handshake request over TCP to
the target server.
handshake = parser.create_handshake
socket.write(handshake.request_data)
<tt>handshake.request_data</tt> is a string containing the handshake format.
Send this over an open TCP socket, then parse any data you receive over the
socket. Here, +chunk+ is a string representing a chunk of input read from the
socket.
overflow_bytes = handshake.parse(chunk)
If the handshake is complete, any bytes received after the end of the handshake
are returned as an array of ints. After parsing each chunk of input, use the
following to check for handshake completion:
* <tt>handshake.complete?</tt> returns +true+ if a whole handshake response has
been received from the server
* <tt>handshake.valid?</tt> returns +true+ if the server's response is valid
and allows the client to connect
* <tt>handshake.protocol</tt> is either +nil+ or a string containing the first
selected subprotocol that matches both our <tt>:protocols</tt> setting and
the server's <tt>Sec-WebSocket-Protocol</tt> response
Once you know you have a valid connection, you should have the parser process
the overflow bytes from the handshake parser:
parser.parse(overflow_bytes)
After this, use the parser API detailed below to handle data tranfer on the
connection.
After this you use the handler API as described below to process incoming data
and send outgoing data.
=== Parser API
=== Handler API
Handlers are create using one of the following methods:
handler = WebSocket::Protocol.server(socket, options)
handler = WebSocket::Protocol.client(socket, options)
The +server+ method returns a handler chosen using the socket's +env+. The
+client+ method always returns a handler for the RFC version of the protocol
with masking enabled on outgoing frames.
The +options+ argument is optional, and is a hash. It may contain the following
keys:
* <tt>:protocols</tt> - an array of strings representing acceptable
subprotocols for use over the socket. The handler will negotiate one of these
to use via the <tt>Sec-WebSocket-Protocol</tt> header if supported by the
other peer.
All handlers respond to the following API methods, but some of them are no-ops
depending on whether the client supports the behaviour.
Note that all of these methods are commands: if they produce data that should
be sent over the socket, they will give this to you by calling
<tt>socket.write(string)</tt>.
==== <tt>handler.onopen { |event| }</tt>
Sets the handler block to execute when the socket becomes open.
==== <tt>handler.onmessage { |event| }</tt>
Sets the handler block to execute when a message is received. +event+ will have
a +data+ attribute containing either a string in the case of a text message or
an array of integers in the case of a binary message.
==== <tt>handler.onclose { |event| }</tt>
Sets the handler block to execute when the socket becomes closed. The +event+
object has +code+ and +reason+ attributes.
==== <tt>handler.start</tt>
Initiates the protocol by sending the handshake - either the response for a
server-side handler or the request for a client-side one. This should be the
first method you invoke. Returns +true+ iff a handshake was sent.
==== <tt>handler.parse(string)</tt>
Takes a string and parses it, potentially resulting in message events being
emitted (see +onmessage+ above) or in data being sent to <tt>socket.write</tt>.
You should send all data you receive via I/O to this method.
==== <tt>handler.text(string)</tt>
Sends a text message over the socket. If the socket handshake is not yet
complete, the message will be queued until it is. Returns +true+ if the message
was sent or queued, and +false+ if the socket can no longer send messages.
==== <tt>handler.binary(array)</tt>
Takes an array of byte-sized integers and sends them as a binary message. Will
queue and return +true+ or +false+ the same way as the +text+ method. It will
also return +false+ if the handler does not support binary messages.
==== <tt>handler.ping(string = '', &callback)</tt>
Sends a ping frame over the socket, queueing it if necessary. +string+ and the
+callback+ block are both optional. If a callback is given, it will be invoked
when the socket receives a pong frame whose content matches +string+. Returns
+false+ if frames can no longer be sent, or if the handler does not support
ping/pong.
==== <tt>handler.close</tt>
Initiates the closing handshake if the socket is still open. For handlers with
no closing handshake, this will result in the immediate execution of the
+onclose+ handler. For handlers with a closing handshake, this sends a closing
frame and +onclose+ will execute when a response is received or a protocol
error occurs.
== License