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