tags:

views:

1391

answers:

5

I'm trying to create a use-once HTTP server to handle a single callback and need help with finding a free TCP port in Ruby.

This is the skeleton of what I'm doing:

require 'socket'
t = STDIN.read
port = 8081
while s = TCPServer.new('127.0.0.1', port).accept
  puts s.gets
  s.print "HTTP/1.1 200/OK\rContent-type: text/plain\r\n\r\n" + t
  s.close
  exit
end

(It echoes standard input to the first connection and then dies.)

How can I automatically find a free port to listen on?

This seems to be the only way to start a job on a remote server which then calls back with a unique job ID. This job ID can then be queried for status info. Why the original designers couldn't just return the job ID when scheduling the job I'll never know. A single port cannot be used because conflicts with multiple callbacks may occur; in this way the ports are only used for +- 5 seconds.

A: 

I guess you could try all ports > 5000 (for example) in sequence. But how will you communicate to the client program what port you are listening to? It seems simpler to decide on a port, and then make it easily configurable, if you need to move your script between different enviroments.

For HTTP, the standard port is 80. Alternative ports i've seen used are 8080, 880 and 8000.

gnud
See (new) last paragraph of question.
Marius Marais
A: 

Don't communicate on random ports. Pick a default one and make it configurable. Random ports are incompatible with firewalls. FTP does this and firewall support for it is a nightmare - it has to deeply inspect packets.

Tometzky
+3  A: 

It is actually quite easy when you don't try to do everything in one line :-/

require 'socket'
t = STDIN.read

port = 8080 # preferred port
begin
  server = TCPServer.new('127.0.0.1', port)    
rescue Errno::EADDRINUSE
  port = rand(65000 - 1024) + 1024
  retry
end

# Start remote process with the value of port

socket = server.accept
puts socket.gets
socket.print "HTTP/1.1 200/OK\rContent-type: text/plain\r\n\r\n" + t
socket.close

This accomplishes (strong word) the same as the snippet in the question.

Marius Marais
+5  A: 

Pass 0 in for the port number. This will cause the system to pick a port for you out of the ephemeral port range. Once you create the server, you can ask it for its addr, which will contain the port that the server is bound to.

server = TCPServer.new('127.0.0.1', 0)
port = server.addr[1]
Aaron Hinni
I'm using a basic Socket (not TCPSocket), which doesn't have the `addr` method. Would you happen to know how I can get the port in this case?
troelskn
Answering my own question, use `socket.getsockname.unpack("snA*")[1]`
troelskn
A: 

I was using the "catch the error and retry" solution for a long time, but it harbors a race condition. I think the symptom was that two TCPServerSockets had the same port number in addr[1], but I didn't spend a lot of time trying to understand the race condition once I learned that you can pass in zero for the port number and have everything work perfectly.

PickAxe 1.8 forgets to tell you that you can pass in zero to have the OS pick an unused port. That is the preferred solution not just because it's simpler, but because it is free from race conditions.

Wayne Conrad