serverside 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. data/CHANGELOG +97 -0
  2. data/COPYING +18 -0
  3. data/README +51 -0
  4. data/Rakefile +92 -0
  5. data/bin/serverside +74 -0
  6. data/doc/rdoc/classes/Daemon.html +253 -0
  7. data/doc/rdoc/classes/Daemon/Base.html +146 -0
  8. data/doc/rdoc/classes/Daemon/Cluster.html +308 -0
  9. data/doc/rdoc/classes/Daemon/Cluster/PidFile.html +228 -0
  10. data/doc/rdoc/classes/Daemon/PidFile.html +178 -0
  11. data/doc/rdoc/classes/ServerSide.html +160 -0
  12. data/doc/rdoc/classes/ServerSide/Application.html +147 -0
  13. data/doc/rdoc/classes/ServerSide/Application/Base.html +196 -0
  14. data/doc/rdoc/classes/ServerSide/Application/Static.html +154 -0
  15. data/doc/rdoc/classes/ServerSide/Connection.html +128 -0
  16. data/doc/rdoc/classes/ServerSide/Connection/Base.html +343 -0
  17. data/doc/rdoc/classes/ServerSide/Connection/Const.html +229 -0
  18. data/doc/rdoc/classes/ServerSide/Connection/Static.html +172 -0
  19. data/doc/rdoc/classes/ServerSide/Server.html +162 -0
  20. data/doc/rdoc/classes/ServerSide/StaticFiles.html +208 -0
  21. data/doc/rdoc/classes/ServerSide/StaticFiles/Const.html +179 -0
  22. data/doc/rdoc/classes/String.html +210 -0
  23. data/doc/rdoc/classes/Symbol.html +156 -0
  24. data/doc/rdoc/created.rid +1 -0
  25. data/doc/rdoc/files/CHANGELOG.html +260 -0
  26. data/doc/rdoc/files/COPYING.html +129 -0
  27. data/doc/rdoc/files/README.html +171 -0
  28. data/doc/rdoc/files/lib/serverside/application_rb.html +109 -0
  29. data/doc/rdoc/files/lib/serverside/cluster_rb.html +101 -0
  30. data/doc/rdoc/files/lib/serverside/connection_rb.html +101 -0
  31. data/doc/rdoc/files/lib/serverside/core_ext_rb.html +107 -0
  32. data/doc/rdoc/files/lib/serverside/daemon_rb.html +108 -0
  33. data/doc/rdoc/files/lib/serverside/server_rb.html +108 -0
  34. data/doc/rdoc/files/lib/serverside/static_rb.html +101 -0
  35. data/doc/rdoc/files/lib/serverside_rb.html +131 -0
  36. data/doc/rdoc/fr_class_index.html +44 -0
  37. data/doc/rdoc/fr_file_index.html +37 -0
  38. data/doc/rdoc/fr_method_index.html +60 -0
  39. data/doc/rdoc/index.html +24 -0
  40. data/doc/rdoc/rdoc-style.css +208 -0
  41. data/lib/serverside.rb +13 -0
  42. data/lib/serverside/application.rb +40 -0
  43. data/lib/serverside/cluster.rb +72 -0
  44. data/lib/serverside/connection.rb +115 -0
  45. data/lib/serverside/core_ext.rb +27 -0
  46. data/lib/serverside/daemon.rb +67 -0
  47. data/lib/serverside/server.rb +18 -0
  48. data/lib/serverside/static.rb +96 -0
  49. data/test/functional/primitive_static_server_test.rb +37 -0
  50. data/test/functional/static_profile.rb +17 -0
  51. data/test/functional/static_rfuzz.rb +67 -0
  52. data/test/functional/static_server_test.rb +25 -0
  53. data/test/test_helper.rb +2 -0
  54. data/test/unit/application_test.rb +16 -0
  55. data/test/unit/cluster_test.rb +129 -0
  56. data/test/unit/connection_test.rb +193 -0
  57. data/test/unit/core_ext_test.rb +32 -0
  58. data/test/unit/daemon_test.rb +75 -0
  59. data/test/unit/server_test.rb +26 -0
  60. data/test/unit/static_test.rb +143 -0
  61. metadata +140 -0
@@ -0,0 +1,115 @@
1
+ module ServerSide
2
+ # The Connection module takes care of HTTP connection. While most HTTP servers
3
+ # (at least the OO ones) will define separate classes for request and
4
+ # response, I chose to use the concept of a connection both for better
5
+ # performance, and also because a single connection might handle multiple
6
+ # requests, if using HTTP 1.1 persistent connection.
7
+ module Connection
8
+ # A bunch of frozen constants to make the parsing of requests and rendering
9
+ # of responses faster than otherwise.
10
+ module Const
11
+ LineBreak = "\r\n".freeze
12
+ # Here's a nice one - parses the first line of a request.
13
+ RequestRegexp = /([A-Za-z0-9]+)\s(\/[^\/\?]*(\/[^\/\?]+)*)\/?(\?(.*))?\sHTTP\/(.+)\r/.freeze
14
+ # Regexp for parsing headers.
15
+ HeaderRegexp = /([^:]+):\s?(.*)\r\n/.freeze
16
+ ContentLength = 'Content-Length'.freeze
17
+ Version_1_1 = '1.1'.freeze
18
+ Connection = 'Connection'.freeze
19
+ Close = 'close'.freeze
20
+ Ampersand = '&'.freeze
21
+ # Regexp for parsing URI parameters.
22
+ ParameterRegexp = /(.+)=(.*)/.freeze
23
+ EqualSign = '='.freeze
24
+ StatusClose = "HTTP/1.1 %d\r\nConnection: close\r\nContent-Type: %s\r\n%sContent-Length: %d\r\n\r\n".freeze
25
+ StatusStream = "HTTP/1.1 %d\r\nConnection: close\r\nContent-Type: %s\r\n%s\r\n".freeze
26
+ StatusPersist = "HTTP/1.1 %d\r\nContent-Type: %s\r\n%sContent-Length: %d\r\n\r\n".freeze
27
+ Header = "%s: %s\r\n".freeze
28
+ Empty = ''.freeze
29
+ Slash = '/'.freeze
30
+ end
31
+
32
+ # This is the base request class. When a new request is created, it starts
33
+ # a thread in which it is parsed and processed.
34
+ #
35
+ # Connection::Base is overriden by applications to create
36
+ # application-specific behavior.
37
+ class Base
38
+ # Initializes the request instance. A new thread is created for
39
+ # processing requests.
40
+ def initialize(conn)
41
+ @conn = conn
42
+ @thread = Thread.new {process}
43
+ end
44
+
45
+ # Processes incoming requests by parsing them and then responding. If
46
+ # any error occurs, or the connection is not persistent, the connection is
47
+ # closed.
48
+ def process
49
+ while true
50
+ break unless parse_request
51
+ respond
52
+ break unless @persistent
53
+ end
54
+ rescue
55
+ # We don't care. Just close the connection.
56
+ ensure
57
+ @conn.close
58
+ end
59
+
60
+ # Parses an HTTP request. If the request is not valid, nil is returned.
61
+ # Otherwise, the HTTP headers are returned. Also determines whether the
62
+ # connection is persistent (by checking the HTTP version and the
63
+ # 'Connection' header).
64
+ def parse_request
65
+ return nil unless @conn.gets =~ Const::RequestRegexp
66
+ @method, @path, @query, @version = $1.downcase.to_sym, $2, $5, $6
67
+ @parameters = @query ? parse_parameters(@query) : {}
68
+ @headers = {}
69
+ while (line = @conn.gets)
70
+ break if line.nil? || (line == Const::LineBreak)
71
+ if line =~ Const::HeaderRegexp
72
+ @headers[$1.freeze] = $2.freeze
73
+ end
74
+ end
75
+ @persistent = (@version == Const::Version_1_1) &&
76
+ (@headers[Const::Connection] != Const::Close)
77
+ @headers
78
+ end
79
+
80
+ # Parses query parameters by splitting the query string and unescaping
81
+ # parameter values.
82
+ def parse_parameters(query)
83
+ query.split(Const::Ampersand).inject({}) do |m, i|
84
+ if i =~ Const::ParameterRegexp
85
+ m[$1.to_sym] = $2.uri_unescape
86
+ end
87
+ m
88
+ end
89
+ end
90
+
91
+ # Sends an HTTP response.
92
+ def send_response(status, content_type, body = nil, content_length = nil,
93
+ headers = nil)
94
+ h = headers ?
95
+ headers.inject('') {|m, kv| m << (Const::Header % kv)} : Const::Empty
96
+
97
+ content_length = body.length if content_length.nil? && body
98
+ @persistent = false if content_length.nil?
99
+
100
+ # Select the right format to use according to circumstances.
101
+ @conn << ((@persistent ? Const::StatusPersist :
102
+ (body ? Const::StatusClose : Const::StatusStream)) %
103
+ [status, content_type, h, content_length])
104
+ @conn << body if body
105
+ rescue
106
+ @persistent = false
107
+ end
108
+
109
+ # Streams additional data to the client.
110
+ def stream(body)
111
+ (@conn << body if body) rescue (@persistent = false)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,27 @@
1
+ # String extension methods.
2
+ class String
3
+ # Encodes a normal string to a URI string.
4
+ def uri_escape
5
+ gsub(/([^ a-zA-Z0-9_.-]+)/n) {'%'+$1.unpack('H2'*$1.size).
6
+ join('%').upcase}.tr(' ', '+')
7
+ end
8
+
9
+ # Decodes a URI string to a normal string.
10
+ def uri_unescape
11
+ tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')}
12
+ end
13
+
14
+ # Concatenates a path (do we really need this sugar?)
15
+ def /(o)
16
+ File.join(self, o.to_s)
17
+ end
18
+ end
19
+
20
+ # Symbol extensions and overrides.
21
+ class Symbol
22
+ # A faster to_s method. This is called a lot, and memoization gives us
23
+ # performance between 10%-35% better.
24
+ def to_s
25
+ @_to_s || (@_to_s = id2name)
26
+ end
27
+ end
@@ -0,0 +1,67 @@
1
+ require 'fileutils'
2
+
3
+ # The Daemon module takes care of starting and stopping daemons.
4
+ module Daemon
5
+ WorkingDirectory = FileUtils.pwd
6
+
7
+ class Base
8
+ def self.pid_fn
9
+ File.join(WorkingDirectory, "#{name.gsub('::', '.').downcase}.pid")
10
+ end
11
+ end
12
+
13
+ # Stores and recalls the daemon pid.
14
+ module PidFile
15
+ # Stores the daemon pid.
16
+ def self.store(daemon, pid)
17
+ File.open(daemon.pid_fn, 'w') {|f| f << pid}
18
+ end
19
+
20
+ # Recalls the daemon pid. If the pid can not be recalled, an error is
21
+ # raised.
22
+ def self.recall(daemon)
23
+ IO.read(daemon.pid_fn).to_i
24
+ rescue
25
+ raise 'Pid not found. Is the daemon started?'
26
+ end
27
+ end
28
+
29
+ # Controls a daemon according to the supplied command or command-line
30
+ # parameter. If an invalid command is specified, an error is raised.
31
+ def self.control(daemon, cmd = nil)
32
+ case (cmd || (!ARGV.empty? && ARGV[0]) || :nil).to_sym
33
+ when :start
34
+ start(daemon)
35
+ when :stop
36
+ stop(daemon)
37
+ when :restart
38
+ stop(daemon)
39
+ start(daemon)
40
+ else
41
+ raise 'Invalid command. Please specify start, stop or restart.'
42
+ end
43
+ end
44
+
45
+ # Starts the daemon by forking and bcoming session leader.
46
+ def self.start(daemon)
47
+ fork do
48
+ Process.setsid
49
+ exit if fork
50
+ PidFile.store(daemon, Process.pid)
51
+ Dir.chdir WorkingDirectory
52
+ File.umask 0000
53
+ STDIN.reopen "/dev/null"
54
+ STDOUT.reopen "/dev/null", "a"
55
+ STDERR.reopen STDOUT
56
+ trap("TERM") {daemon.stop; exit}
57
+ daemon.start
58
+ end
59
+ end
60
+
61
+ # Stops the daemon by sending it a TERM signal.
62
+ def self.stop(daemon)
63
+ pid = PidFile.recall(daemon)
64
+ FileUtils.rm(daemon.pid_fn)
65
+ pid && Process.kill("TERM", pid)
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ require 'socket'
2
+
3
+ module ServerSide
4
+ # The ServerSide HTTP server is designed to be fast and simple. It is also
5
+ # designed to support both HTTP 1.1 persistent connections, and HTTP streaming
6
+ # for applications which use Comet techniques.
7
+ class Server
8
+ # Creates a new server by opening a listening socket and starting an accept
9
+ # loop. When a new connection is accepted, a new instance of the the
10
+ # supplied connection class is instantiated and passed the connection for
11
+ # processing.
12
+ def initialize(host, port, connection_class)
13
+ @connection_class = connection_class
14
+ @server = TCPServer.new(host, port)
15
+ loop {@connection_class.new(@server.accept)}
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,96 @@
1
+ module ServerSide
2
+ # This module provides functionality for serving files and directory listings
3
+ # over HTTP. It is mainly used by ServerSide::Connection::Static.
4
+ module StaticFiles
5
+ # Frozen constants to be used by the module.
6
+ module Const
7
+ ETag = 'ETag'.freeze
8
+ ETagFormat = '%x:%x:%x'.inspect.freeze
9
+ CacheControl = 'Cache-Control'.freeze
10
+ MaxAge = "max-age=#{86400 * 30}".freeze
11
+ IfNoneMatch = 'If-None-Match'.freeze
12
+ NotModifiedClose = "HTTP/1.1 304 Not Modified\r\nConnection: close\r\nContent-Length: 0\r\nETag: %s\r\nCache-Control: #{MaxAge}\r\n\r\n".freeze
13
+ NotModifiedPersist = "HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\nETag: %s\r\nCache-Control: #{MaxAge}\r\n\r\n".freeze
14
+ TextPlain = 'text/plain'.freeze
15
+ MaxCacheFileSize = 100000.freeze # 100KB for the moment
16
+
17
+ DirListingStart = '<html><head><title>Directory Listing for %s</title></head><body><h2>Directory listing for %s:</h2>'.freeze
18
+ DirListing = '<a href="%s">%s</a><br/>'.freeze
19
+ DirListingStop = '</body></html>'.freeze
20
+ end
21
+
22
+ @@mime_types = Hash.new {|h, k| ServerSide::StaticFiles::Const::TextPlain}
23
+ @@mime_types.merge!({
24
+ '.html'.freeze => 'text/html'.freeze,
25
+ '.css'.freeze => 'text/css'.freeze,
26
+ '.js'.freeze => 'text/javascript'.freeze,
27
+
28
+ '.gif'.freeze => 'image/gif'.freeze,
29
+ '.jpg'.freeze => 'image/jpeg'.freeze,
30
+ '.jpeg'.freeze => 'image/jpeg'.freeze,
31
+ '.png'.freeze => 'image/png'.freeze
32
+ })
33
+
34
+ @@static_files = {}
35
+
36
+ # Serves a file over HTTP. The file is cached in memory for later retrieval.
37
+ # If the If-None-Match header is included with an ETag, it is checked
38
+ # against the file's current ETag. If there's a match, a 304 response is
39
+ # rendered.
40
+ def serve_file(fn)
41
+ stat = File.stat(fn)
42
+ etag = (Const::ETagFormat % [stat.mtime.to_i, stat.size, stat.ino]).freeze
43
+ unless etag == @headers[Const::IfNoneMatch]
44
+ if @@static_files[fn] && (@@static_files[fn][0] == etag)
45
+ content = @@static_files[fn][1]
46
+ else
47
+ content = IO.read(fn).freeze
48
+ @@static_files[fn] = [etag.freeze, content]
49
+ end
50
+
51
+ send_response(200, @@mime_types[File.extname(fn)], content, stat.size,
52
+ {Const::ETag => etag, Const::CacheControl => Const::MaxAge})
53
+ else
54
+ @conn << ((@persistent ? Const::NotModifiedPersist :
55
+ Const::NotModifiedClose) % etag)
56
+ end
57
+ rescue => e
58
+ send_response(404, Const::TextPlain, 'Error reading file.')
59
+ end
60
+
61
+ # Serves a directory listing over HTTP in the form of an HTML page.
62
+ def serve_dir(dir)
63
+ html = (Const::DirListingStart % [@path, @path]) +
64
+ Dir.entries(dir).inject('') {|m, fn|
65
+ (fn == '.') ? m : m << Const::DirListing % [@path/fn, fn]
66
+ } + Const::DirListingStop
67
+ send_response(200, 'text/html', html)
68
+ end
69
+ end
70
+
71
+ module Connection
72
+ module Const
73
+ WD = '.'.freeze
74
+ FileNotFound = "Couldn't open file %s.".freeze
75
+ end
76
+
77
+ # A connection type for serving static files.
78
+ class Static < Base
79
+ include StaticFiles
80
+
81
+ # Responds with a file's content or a directory listing. If the path
82
+ # does not exist, a 404 response is rendered.
83
+ def respond
84
+ fn = './%s' % @path
85
+ if File.file?(fn)
86
+ serve_file(fn)
87
+ elsif File.directory?(fn)
88
+ serve_dir(fn)
89
+ else
90
+ send_response(404, 'text', Const::FileNotFound % @path)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'net/http'
3
+
4
+ class StaticServerTest < Test::Unit::TestCase
5
+
6
+ class StaticConnection < ServerSide::Connection::Base
7
+ def respond
8
+ status = 200
9
+ body = IO.read('.'/@path)
10
+ rescue => e
11
+ status = 404
12
+ body = "Couldn't open file #{@path}."
13
+ ensure
14
+ send_response(status, 'text', body)
15
+ end
16
+ end
17
+
18
+ def test_basic
19
+ t = Thread.new {ServerSide::Server.new('0.0.0.0', 17654, StaticConnection)}
20
+ sleep 0.1
21
+
22
+ h = Net::HTTP.new('localhost', 17654)
23
+ resp, data = h.get('/qqq', nil)
24
+ assert_equal 404, resp.code.to_i
25
+ assert_equal "Couldn't open file /qqq.", data
26
+
27
+ h = Net::HTTP.new('localhost', 17654)
28
+ resp, data = h.get("/#{__FILE__}", nil)
29
+ assert_equal 200, resp.code.to_i
30
+ assert_equal IO.read(__FILE__), data
31
+ assert_equal 'text', resp['Content-Type']
32
+ # Net::HTTP includes this header in the request, so our server returns
33
+ # likewise.
34
+ assert_equal 'close', resp['Connection']
35
+ t.exit
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'serverside'
3
+ require 'fileutils'
4
+
5
+ FileUtils.cd(File.dirname(__FILE__))
6
+
7
+ pid = fork do
8
+ trap('TERM') {exit}
9
+ require 'profile'
10
+ ServerSide::Server.new('0.0.0.0', 8000, ServerSide::Connection::Static)
11
+ end
12
+
13
+ puts "Please wait..."
14
+ `ab -n 1000 http://localhost:8000/#{File.basename(__FILE__)}`
15
+ puts
16
+ Process.kill('TERM', pid)
17
+ puts
@@ -0,0 +1,67 @@
1
+ # Simple script that hits a host port and URI with a bunch of connections
2
+ # and measures the timings.
3
+
4
+ require 'rubygems'
5
+ require 'rfuzz/client'
6
+ require 'rfuzz/stats'
7
+ include RFuzz
8
+
9
+ class StatsTracker
10
+
11
+ def initialize
12
+ @stats = {}
13
+ @begins = {}
14
+ @error_count = 0
15
+ end
16
+
17
+ def mark(event)
18
+ @begins[event] = Time.now
19
+ end
20
+
21
+ def sample(event)
22
+ @stats[event] ||= Stats.new(event.to_s)
23
+ @stats[event].sample(Time.now - @begins[event])
24
+ end
25
+
26
+ def method_missing(event, *args)
27
+ case args[0]
28
+ when :begins
29
+ mark(:request) if event == :connect
30
+ mark(event)
31
+ when :ends
32
+ sample(:request) if event == :close
33
+ sample(event)
34
+ when :error
35
+ @error_count += 1
36
+ end
37
+ end
38
+
39
+ def to_s
40
+ request = @stats[:request]
41
+ @stats.delete :request
42
+ "#{request}\n----\n#{@stats.values.join("\n")}\nErrors: #@error_count"
43
+ end
44
+ end
45
+
46
+
47
+ if ARGV.length != 4
48
+ STDERR.puts "usage: ruby perftest.rb host port uri count"
49
+ exit 1
50
+ end
51
+
52
+ host, port, uri, count = ARGV[0], ARGV[1], ARGV[2], ARGV[3].to_i
53
+
54
+ codes = {}
55
+ cl = HttpClient.new(host, port, :notifier => StatsTracker.new)
56
+ count.times do
57
+ begin
58
+ resp = cl.get(uri)
59
+ code = resp.http_status.to_i
60
+ codes[code] ||= 0
61
+ codes[code] += 1
62
+ rescue Object
63
+ end
64
+ end
65
+
66
+ puts cl.notifier.to_s
67
+ puts "Status Codes: #{codes.inspect}"