From a42b01892a53ba31a10e80322c39e88810c1638c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 13 Dec 2009 14:05:26 -0800 Subject: [PATCH] more documentation updates --- GNUmakefile | 3 +++ README | 48 ++++++++++++++++++++++++++++++++++++++---------- lib/sunshowers/io.rb | 45 +++++++++++++++++++++++++++++++-------------- sunshowers.gemspec | 7 +++++-- 4 files changed, 77 insertions(+), 26 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index cf9a258..bf61c20 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -158,4 +158,7 @@ test-unit: $(TEST_UNIT) test-integration: $(MAKE) -C t +get-draft: + wget http://tools.ietf.org/id/draft-hixie-thewebsocketprotocol + .PHONY: .FORCE-GIT-VERSION-FILE doc manifest man test $(TEST_UNIT) diff --git a/README b/README index 257e084..efb7b62 100644 --- a/README +++ b/README @@ -1,14 +1,21 @@ = Sunshowers: Web Sockets for Rack+Rainbows! -Sunshowers is a Rainbows! (and Zbatery) Rack library for Web Sockets. -It uses unofficial extensions for Rack which are subject to -change, but it appears to work well enough for the echo_client.py -example shipped with pywebsocket. +Sunshowers is a Ruby library for Web Sockets. It exposes an easy-to-use +API that may be used in both clients and servers. On the server side, +it is designed to work with Rack::Request and Rainbows! concurrency +models that expose a synchronous application flow. -It has not been tested against normal web browsers, though there's -no reason it shouldn't work. +It appears works well with the echo_client.py example shipped with +pywebsocket. It has not been tested against normal web browsers, though +there's no reason it shouldn't work. -It currently depends on the git version of Rainbows. +== Features + +* supports reads and writes of both UTF-8 and binary Web Socket frames + +* compatible with Revactor, Rainbows::Fiber::IO and core Ruby IO objects + +* pure Ruby implementation, should be highly portable, tested under 1.9 == License @@ -40,10 +47,31 @@ for Rainbows!: * ThreadSpawn * ThreadPool + # A simple echo server example require "sunshowers" - -In your application, use Sunshowers::Request.new(env) instead of -Rack::Request.new(env). See examples/echo.ru for a full example. + use Rack::ContentLength + use Rack::ContentType + run lambda { |env| + req = Sunshowers::Request.new(env) + if req.ws? + req.ws_handshake! + ws_io = req.ws_io + ws_io.each do |record| + ws_io.write(record) + break if record == "Goodbye" + end + req.ws_quit! # Rainbows! should handle this quietly + end + [404, {}, []] + } + +Already using a Rack::Request-derived class? Sunshowers::WebSocket may +also be included in any Rack::Request-derived class, so you can just +open it up and include it: + + class Sinatra::Request < Rack::Request + include Sunshowers::WebSocket + end == Disclaimer diff --git a/lib/sunshowers/io.rb b/lib/sunshowers/io.rb index ea45c4d..5a95ef5 100644 --- a/lib/sunshowers/io.rb +++ b/lib/sunshowers/io.rb @@ -2,29 +2,41 @@ module Sunshowers + # Wraps IO objects and provides transparent framing of #gets and + # #write methods. + # + # It is compatible with core Ruby IO objects, Revactor and + # Rainbows::Fiber::IO objects. class IO < Struct.new(:to_io, :buf) - # most Web Socket messages from clients are expected to be small text + + # most Web Socket messages from clients are expected to be small text, + # so we only read 128 bytes off the socket at a time RD_SIZE = 128 - # maximum size of a UTF-8 buffer we'll allow in memory + # maximum size of a UTF-8 buffer we'll allow in memory (16K) # this is a soft limit and may be offset by the value of RD_SIZE MAX_UTF8_SIZE = 1024 * 16 - # maximum size of a binary buffer we'll allow in memory + # maximum size of a binary buffer we'll allow in memory (112K) # this is a soft limit and may be offset by the value of RD_SIZE MAX_BINARY_SIZE = 1024 * 112 # Web Sockets usually uses UTF-8 when interfacing with the client - # Ruby Sockets always return strings of Encoding::Binary + # Ruby Sockets always return strings of Encoding::Binary under 1.9 ENC = defined?(Encoding::UTF_8) ? Encoding::UTF_8 : nil - Z = "" + Z = "" # :nodoc: - def initialize(io, buf = Z.dup) + # Wraps the given +io+ with Web Sockets-compatible framing. + # + # TCPSocket.new('example.com', 80) + # io = Sunshowers::IO.new(socket) + def initialize(io, buffer = Z.dup) super end - # iterates through each message until a client the connection is closed + # iterates through each message until a client closes the connection + # or block the issues a break/return def each(&block) while str = gets yield str @@ -32,7 +44,9 @@ module Sunshowers self end - # retrieves the next record, returns nil if client closes connection + # Retrieves the next record, returns nil if client close connection. + # The record may be either a UTF-8 or binary String, under + # Ruby 1.9, the String encoding will be set appropriately. def gets begin unless buf.empty? @@ -47,17 +61,19 @@ module Sunshowers end while true end - def syswrite(buf) + def syswrite(buf) # :nodoc: to_io.write(buf) rescue EOFError,Errno::ECONNRESET,Errno::EPIPE, Errno::EINVAL,Errno::EBADF => e raise ClientShutdown, e.message, [] end + # writes a UTF-8 frame containing +buf+ def write_utf8(buf) syswrite("\0#{binary(buf)}\xff") end + # writes a binary frame containing +buf+ def write_binary(buf) buf = binary(buf) n = buf.size @@ -71,24 +87,23 @@ module Sunshowers end if ENC.nil? - def valid_utf8?(buf) + def valid_utf8?(buf) # :nodoc: buf.unpack("U*") true rescue ArgumentError false end - def write(buf) + def write(buf) # :nodoc: valid_utf8?(buf) ? write_utf8(buf) : write_binary(buf) end else - def write(buf) + def write(buf) # :nodoc: buf.encoding == ENC ? write_utf8(buf) : write_binary(buf) end end - # :stopdoc: - def read(size = nil) + def read(size = nil) # :nodoc: i = to_io # read with no args for Revactor compat i.respond_to?(:readpartial) ? @@ -96,6 +111,7 @@ module Sunshowers i.read(size.nil? ? nil : size) end + # :stopdoc: if ENC.nil? def utf8!(buf) valid_utf8?(buf) or raise ProtocolError, "not UTF-8: #{buf.inspect}" @@ -145,5 +161,6 @@ module Sunshowers rv end + # :startdoc: end end diff --git a/sunshowers.gemspec b/sunshowers.gemspec index 646dcca..ea8531b 100644 --- a/sunshowers.gemspec +++ b/sunshowers.gemspec @@ -6,7 +6,7 @@ manifest = File.readlines('.manifest').map! { |x| x.chomp! } # don't bother with tests that fork, not worth our time to get working # with `gem check -t` ... (of course we care for them when testing with # GNU make when they can run in parallel) -test_files = manifest.grep(%r{\Atest/unit/test_.*\.rb\z}).map do |f| +test_files = manifest.grep(%r{\Atest/test_.*\.rb\z}).map do |f| File.readlines(f).grep(/\bfork\b/).empty? ? f : nil end.compact @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.name = %q{sunshowers} s.version = ENV["VERSION"] - s.authors = ["sunshowers hackers"] + s.authors = ["Sunshowers hackers"] s.date = Time.now.utc.strftime('%Y-%m-%d') s.description = File.read("README").split(/\n\n/)[1] s.email = %q{sunshowers@librelist.com} @@ -45,10 +45,13 @@ Gem::Specification.new do |s| # unicorn + rack # optional: # revactor + rev + iobuffer + # # the following do not work directly with Sunshowers, yet # rev + iobuffer # eventmachine # espace-neverblock + eventmachine # async_sinatra + sinatra + eventmachine + # recommended: + # Ruby 1.9 + FiberSpawn concurrency model with Rainbows! s.add_dependency(%q, ["~> 0.9.0"]) # s.licenses = %w(GPLv2 Ruby) # accessor not compatible with older RubyGems -- 2.11.4.GIT