serverside 0.1.59

Sign up to get free protection for your applications and to get access to all the features.
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}"