File: ruby_server.rb

package info (click to toggle)
passenger 6.1.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 50,788 kB
  • sloc: cpp: 589,292; ruby: 43,761; ansic: 27,742; javascript: 5,948; python: 1,158; sh: 1,012; makefile: 51
file content (244 lines) | stat: -rwxr-xr-x 6,131 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env ruby
# A simple pure-Ruby HTTP server, meant as a helper tool in benchmarks.
# It supports HTTP keep-alive and it supports forwarding the request to
# another server.

require 'thread'
require 'socket'
require 'optparse'

class TestServer
  REQUEST =
    "GET / HTTP/1.1\r\n" <<
    "Connection: Keep-Alive\r\n" <<
    "Host: 127.0.0.1:3001\r\n" <<
    "User-Agent: ApacheBench/2.3\r\n" <<
    "Accept: */*\r\n\r\n"

  RESPONSE =
    "HTTP/1.1 200 OK\r\n" <<
    "Status: 200 OK\r\n" <<
    "Content-Type: text/plain\r\n" <<
    "Content-Length: 3\r\n" <<
    "Connection: keep-alive\r\n" <<
    "\r\n" <<
    "ok\n"

  def initialize(options = {})
    @options = options
    @options[:transport] ||= :tcp
    @options[:protocol] ||= :http
    @options[:port] ||= 3000
    @options[:file] ||= './socket'
    @options[:threads] ||= 2
    @options[:processes] ||= 2
    @forward = @options[:forward]
    @forward_transport = @options[:forward_transport]
    @forward_file = @options[:forward_file]
    @forward_port = @options[:forward_port]
    @forward_keep_alive = @options[:forward_keep_alive]
  end

  def run
    case @options[:transport]
    when :tcp
      @server = TCPServer.new('127.0.0.1', @options[:port])
      @server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
      puts "Listening on http://127.0.0.1:#{@options[:port]}/"
    when :unix
      File.unlink(@options[:file]) rescue nil
      @server = UNIXServer.new(@options[:file])
      puts "Listening on Unix domain socket: #{@options[:file]}"
    else
      abort "Unknown transport #{@options[:transport]}"
    end
    @server.listen(100)

    case @options[:protocol]
    when :http
      puts "Using HTTP protocol"
      @protocol = :http
    when :session
      puts "Using session protocol"
      @protocol = :session
    else
      abort "Unknown protocol #{@options[:protocol]}"
    end

    if @forward
      case @forward_transport
      when :tcp
        puts "Forwarding to http://127.0.0.1:#{@forward_port}/"
      when :unix
        puts "Forwarding to Unix domain socket: #{@forward_file}"
      end
    end

    puts "Using #{@options[:processes]} processes"
    puts "Using #{@options[:threads]} threads per process"
    fork_children
    threads = []
    @options[:threads].times { threads << start_thread }
    begin
      threads.each { |t| t.join }
    rescue Interrupt
    end
  end

private
  def fork_children
    if @options[:processes] == 1
      return
    end

    children = []
    @options[:processes].times do
      pid = fork
      if pid
        # Parent
        puts "Spawned child process: #{pid}"
        children << pid
      else
        return
      end
    end
    if !children.empty?
      # Parent
      begin
        sleep 999999
      rescue Interrupt
        exit
      ensure
        children.each do |pid|
          puts "Reaping child process: #{pid}"
          Process.kill('INT', pid)
        end
        children.each do |pid|
          Process.waitpid(pid)
        end
      end
    end
  end

  def start_thread
    Thread.new do
      Thread.current.abort_on_exception = true
      if @forward && @forward_keep_alive
        forward_connection = connect_to_forwarding_target
      end
      while true
        handle_next_client(forward_connection)
      end
    end
  end

  def handle_next_client(forward_connection)
    client = @server.accept
    begin
      buffer = String.new(encoding: Encoding::BINARY)
      while true
        begin
          read_header(client, buffer)

          if @forward
            forward(forward_connection)
          end

          # Write response
          client.write(RESPONSE)
        rescue EOFError, Errno::ECONNRESET
          break
        end
      end
    ensure
      client.close
    end
  end

  def read_header(client, buffer)
    if @protocol == :http
      while client.readline != "\r\n"
        # Do nothing.
      end
    else
      temp = client.read(4, buffer)
      raise EOFError if temp.nil?
      size = temp.unpack('N')[0]
      temp = client.read(size, buffer)
      raise EOFError if temp.nil?
    end
  end

  def forward(target_connection)
    if target_connection
      io = target_connection
    else
      io = connect_to_forwarding_target
    end
    begin
      io.write(REQUEST)
      while io.readline != "ok\n"
        # Do nothing
      end
    ensure
      if !target_connection
        io.close
      end
    end
  end

  def connect_to_forwarding_target
    if @forward_transport == :unix
      UNIXSocket.new(@forward_file)
    else
      TCPSocket.new('127.0.0.1', @forward_port)
    end
  end
end

options = {}
parser = OptionParser.new do |opts|
  opts.banner = "Usage: ./ruby.rb [options]"
  opts.separator ""

  opts.separator "Options:"
  opts.on("--port PORT", Integer, "Listen on the given TCP port. Default: 3000") do |val|
    options[:transport] = :tcp
    options[:port] = val
  end
  opts.on("--file PATH", String, "Listen on the given Unix domain socket file") do |val|
    options[:transport] = :unix
    options[:file] = val
  end
  opts.on("--session-protocol", "Accept session protocol instead of HTTP") do
    options[:protocol] = :session
  end
  opts.on("--threads N", Integer, "Number of threads to use. Default: 2") do |val|
    options[:threads] = val
  end
  opts.on("--processes N", Integer, "Number of processes to use. Default: 2") do |val|
    options[:processes] = val
  end
  opts.on("--forward-tcp PORT", Integer, "Forward request to another TCP server") do |val|
    options[:forward] = true
    options[:forward_transport] = :tcp
    options[:forward_port] = val
  end
  opts.on("--forward-file PATH", String, "Forward request to another Unix domain socket server") do |val|
    options[:forward] = true
    options[:forward_transport] = :unix
    options[:forward_file] = val
  end
  opts.on("--forward-keep-alive", "Use keep-alive when forwarding") do
    options[:forward_keep_alive] = true
  end
end
begin
  parser.parse!
rescue OptionParser::ParseError => e
  puts e
  puts
  puts "Please see '--help' for valid options."
  exit 1
end
TestServer.new(options).run