From 9cfac60925679c8f6171d4aeb3afca4cb6e90f90 Mon Sep 17 00:00:00 2001 From: James Coglan Date: Sat, 20 Apr 2013 19:35:31 +0100 Subject: [PATCH] Documentation that's up to date. --- README.rdoc | 277 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 182 insertions(+), 95 deletions(-) diff --git a/README.rdoc b/README.rdoc index 780fbbd..bd31117 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,127 +1,214 @@ -= faye-websocket-parser {}[http://travis-ci.org/faye/faye-websocket-ruby] += websocket-protocol {}[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 Sec-WebSocket-Protocol +* 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 Faye::WebSocket::HybiParser 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: + +* socket.url - returns the full URL of the socket as a string. +* socket.write(string) - writes the given string to a TCP stream. + +Server-side sockets require one additional method: + +* socket.env - 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+ + * rack.input, 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 'https' if the - socket is using an encrypted connection -* rack.input, 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'] -* :masking - optional, boolean flag saying whether to mask outgoing - message frames. This is not required for server-side handlers. -* :protocols - optional, an array of strings that will be used to set - the Sec-WebSocket-Protocol 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 parser.handshake_response 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 WebSocket::Protocol 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 WS#write 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: - -* :masking - must have the value +true+. -* :protocols - optional, an array of strings that will be used to set - the Sec-WebSocket-Protocol 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) - -handshake.request_data 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: - -* handshake.complete? returns +true+ if a whole handshake response has - been received from the server -* handshake.valid? returns +true+ if the server's response is valid - and allows the client to connect -* handshake.protocol is either +nil+ or a string containing the first - selected subprotocol that matches both our :protocols setting and - the server's Sec-WebSocket-Protocol 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: + +* :protocols - an array of strings representing acceptable + subprotocols for use over the socket. The handler will negotiate one of these + to use via the Sec-WebSocket-Protocol 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 +socket.write(string). + +==== handler.onopen { |event| } + +Sets the handler block to execute when the socket becomes open. + +==== handler.onmessage { |event| } + +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. + +==== handler.onclose { |event| } + +Sets the handler block to execute when the socket becomes closed. The +event+ +object has +code+ and +reason+ attributes. + +==== handler.start + +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. + +==== handler.parse(string) + +Takes a string and parses it, potentially resulting in message events being +emitted (see +onmessage+ above) or in data being sent to socket.write. +You should send all data you receive via I/O to this method. + +==== handler.text(string) + +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. + +==== handler.binary(array) + +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. + +==== handler.ping(string = '', &callback) + +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. + +==== handler.close + +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