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.
- data/CHANGELOG +97 -0
- data/COPYING +18 -0
- data/README +51 -0
- data/Rakefile +92 -0
- data/bin/serverside +74 -0
- data/doc/rdoc/classes/Daemon.html +253 -0
- data/doc/rdoc/classes/Daemon/Base.html +146 -0
- data/doc/rdoc/classes/Daemon/Cluster.html +308 -0
- data/doc/rdoc/classes/Daemon/Cluster/PidFile.html +228 -0
- data/doc/rdoc/classes/Daemon/PidFile.html +178 -0
- data/doc/rdoc/classes/ServerSide.html +160 -0
- data/doc/rdoc/classes/ServerSide/Application.html +147 -0
- data/doc/rdoc/classes/ServerSide/Application/Base.html +196 -0
- data/doc/rdoc/classes/ServerSide/Application/Static.html +154 -0
- data/doc/rdoc/classes/ServerSide/Connection.html +128 -0
- data/doc/rdoc/classes/ServerSide/Connection/Base.html +343 -0
- data/doc/rdoc/classes/ServerSide/Connection/Const.html +229 -0
- data/doc/rdoc/classes/ServerSide/Connection/Static.html +172 -0
- data/doc/rdoc/classes/ServerSide/Server.html +162 -0
- data/doc/rdoc/classes/ServerSide/StaticFiles.html +208 -0
- data/doc/rdoc/classes/ServerSide/StaticFiles/Const.html +179 -0
- data/doc/rdoc/classes/String.html +210 -0
- data/doc/rdoc/classes/Symbol.html +156 -0
- data/doc/rdoc/created.rid +1 -0
- data/doc/rdoc/files/CHANGELOG.html +260 -0
- data/doc/rdoc/files/COPYING.html +129 -0
- data/doc/rdoc/files/README.html +171 -0
- data/doc/rdoc/files/lib/serverside/application_rb.html +109 -0
- data/doc/rdoc/files/lib/serverside/cluster_rb.html +101 -0
- data/doc/rdoc/files/lib/serverside/connection_rb.html +101 -0
- data/doc/rdoc/files/lib/serverside/core_ext_rb.html +107 -0
- data/doc/rdoc/files/lib/serverside/daemon_rb.html +108 -0
- data/doc/rdoc/files/lib/serverside/server_rb.html +108 -0
- data/doc/rdoc/files/lib/serverside/static_rb.html +101 -0
- data/doc/rdoc/files/lib/serverside_rb.html +131 -0
- data/doc/rdoc/fr_class_index.html +44 -0
- data/doc/rdoc/fr_file_index.html +37 -0
- data/doc/rdoc/fr_method_index.html +60 -0
- data/doc/rdoc/index.html +24 -0
- data/doc/rdoc/rdoc-style.css +208 -0
- data/lib/serverside.rb +13 -0
- data/lib/serverside/application.rb +40 -0
- data/lib/serverside/cluster.rb +72 -0
- data/lib/serverside/connection.rb +115 -0
- data/lib/serverside/core_ext.rb +27 -0
- data/lib/serverside/daemon.rb +67 -0
- data/lib/serverside/server.rb +18 -0
- data/lib/serverside/static.rb +96 -0
- data/test/functional/primitive_static_server_test.rb +37 -0
- data/test/functional/static_profile.rb +17 -0
- data/test/functional/static_rfuzz.rb +67 -0
- data/test/functional/static_server_test.rb +25 -0
- data/test/test_helper.rb +2 -0
- data/test/unit/application_test.rb +16 -0
- data/test/unit/cluster_test.rb +129 -0
- data/test/unit/connection_test.rb +193 -0
- data/test/unit/core_ext_test.rb +32 -0
- data/test/unit/daemon_test.rb +75 -0
- data/test/unit/server_test.rb +26 -0
- data/test/unit/static_test.rb +143 -0
- 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}"
|