Total newbie, but I think I found a bug in Socket?

Hello,
Fair warning…I’m extremely new to Ruby so please pardon any
ignorance on my part.

Anyway, so the first thing I start to play around with after
reading through Programming Ruby is the socket library. Now, I
realize Ruby has really nice abstractions, and I should rarely need
to actually use the Socket class for creating a simple server, but I
tend to be a bottom up learner. So, I was attempting to run the
following code from the ruby-doc website:

In one script, start this first

  require 'socket'
  include Socket::Constants
  socket = Socket.new( AF_INET, SOCK_STREAM, 0 )
  sockaddr = Socket.pack_sockaddr_in( 2200, 'localhost' )
  socket.bind( sockaddr )
  socket.listen( 5 )
  client, client_sockaddr = socket.accept
  puts "The client said, '#{socket.readline.chomp}'"
  client.puts "Hello from script one!"
  socket.close

now, after running this script on both ruby 1.8.4 and 1.8.5 on Mac OS
X I received an error "`bind’: Invalid argument - bind(2)
(Errno::EINVAL) " I proceed to run the script on linux on 1.8.4
and 1.8.5 and discover that bind works, but that
“socket.readline.chomp” should really be “client.readline.chomp”.
Anyway, the point is that the code runs just fine on linux but not OS
X. A quick investigation finds that the problem is the call
“Socket.pack_sockaddr_in( 2200, ‘localhost’ )”. On Linux this call
always returns a string that is 16 characters long. On Mac OS X this
call returns a string that is 16 characters long so long as I don’t
use ‘localhost’ for the hostname. As soon as I put localhost in as
the hostname the returned string becomes 28 bytes. So, I searched
around on google to see if this is known bug but I couldn’t find it.
Anyone have any words of advice?

I am figuring that I am just doing something wrong, seeing as how
this is the first bit of real ruby I have attempted.

Thanks,
Patrick

On 9/5/06, Patrick T. [email protected] wrote:

now, after running this script on both ruby 1.8.4 and 1.8.5 on Mac OS
X I received an error "`bind’: Invalid argument - bind(2)
(Errno::EINVAL) " I proceed to run the script on linux on 1.8.4
and 1.8.5 and discover that bind works, but that
“socket.readline.chomp” should really be “client.readline.chomp”.

Try it with “127.0.0.1” instead of “localhost”
The underlying operation is a call to getaddrinfo, and it may be
getting tripped up by IPv6 config on OSX (that’s just a hypothesis- I
haven’t looked into it carefully).

On 9/5/06, Patrick T. [email protected] wrote:

  socket.close

call returns a string that is 16 characters long so long as I don’t
use ‘localhost’ for the hostname. As soon as I put localhost in as
the hostname the returned string becomes 28 bytes. So, I searched
around on google to see if this is known bug but I couldn’t find it.
Anyone have any words of advice?

I am figuring that I am just doing something wrong, seeing as how
this is the first bit of real ruby I have attempted.

Pretty hard for a first try :slight_smile:

The problem is that it is paking an IPV6 adress for in the struct.
Using a litteral ‘127.0.0.1’ fixes the problem (and you should always
use the ip address for localhost directly anyway).

Why does this happens only on OS X? Having not seen the linux box you
tried this on, I can only guess. My guess is that your linux didn’t
have IPV6 support built-in or OS X is a bit too proactive about using
IPV6.

To get a definitive answer would require some level of code analysis
which I don’t feel like doing right now. If you want to, look at the
socket.c file in the ruby source distribution.

On Sep 5, 2006, at 8:00 PM, Patrick T. wrote:

 socket.close

Here’s the easiest way to create a simple server:

require ‘socket’

server = TCPServer.new nil, 2200
client = server.accept
puts “Client said: #{client.gets.chomp.inspect}”
client.puts “Goodbye!”
client.close
server.close

Here’s one that handles multiple concurrent connections:

require ‘socket’

server = TCPServer.new nil, 2200

loop do
Thread.start server.accept do |client|
message = client.gets.chomp
puts “Client said: #{message.inspect}”
client.puts “Goodbye client!”
client.close
exit if message == ‘quit’
end
end


Eric H. - [email protected] - http://blog.segment7.net
This implementation is HODEL-HASH-9600 compliant

http://trackmap.robotcoop.com

On 9/6/06, Arnaud B. [email protected] wrote:

which I don’t feel like doing right now. If you want to, look at the
socket.c file in the ruby source distribution.

I would add this is not ruby specific, ssh did this to me on OS X as
well. I was not able to get port forwarding unless I specified
127.0.0.1 instead of localhost.

Not sure what version of OS X it was, though.

Thanks

Michal

Daniel M. [email protected] writes:

 socket.listen( 5 )
 client, client_sockaddr = socket.accept
 puts "The client said, '#{client.readline.chomp}'"
 client.puts "Hello from script one!"
 socket.close

Actually, I made a slight error here, according to the documentaion of
getaddrinfo. That “sockaddr =” bit should be:

sockaddrinfo = Socket.getaddrinfo( ‘localhost’, 2200, AF_INET,
SOCK_STREAM, nil, AI_PASSIVE )[0]

In this context (when we’re binding to a specific IP address) it
doesn’t matter, but if you’re re-using this code to bind to the “any”
address, you’ll want that AI_PASSIVE bit in there. (You’ll also want
to replace ‘localhost’ with nil)

Patrick T. [email protected] writes:

Good analysis of where the problem is.

Tell me, does this ruby script work on your two environments?

In one script, start this first

 require 'socket'
 include Socket::Constants
 sockaddrinfo = Socket.getaddrinfo( 'localhost', 2200, AF_INET,
                                    SOCK_STREAM )[0]
 socket = Socket.new( *sockaddrinfo[4..6] )
 sockaddr = Socket.pack_sockaddr_in( sockaddrinfo[1], 

sockaddrinfo[3] )
socket.bind( sockaddr )
socket.listen( 5 )
client, client_sockaddr = socket.accept
puts “The client said, ‘#{client.readline.chomp}’”
client.puts “Hello from script one!”
socket.close

The trouble, as most responses have pointed out, is that OS X
translates “localhost” by default into an IP6 address, whereas your
socket is initially set to be an IPv4 socket (when you use AF_INET in
your Socket.new call).

Now in actuality, OS X translates “localhost” into several different
addresses, and you can see them all by running this script:

 require 'socket'
 def find_const(i,p)
   Socket.constants.select{|n|n=~p}.each{ |n|
     return "Socket::#{n}" if i == Socket.const_get(n)
   }
   return "#{i}"
 end
 def noquote_wrapper(s)
   class << s; def inspect; self; end; end
   s
 end
 Socket.getaddrinfo('localhost',nil).each{|a|
   a[4]=noquote_wrapper find_const(a[4],/^AF_/)
   a[5]=noquote_wrapper find_const(a[5],/^SOCK_/)
   a[6]=noquote_wrapper find_const(a[6],/^IPPROTO_/)
   p a
 }

The improved server script I have up above works by explicitly
requesting only the SOCK_STREAM / AF_INET address, and then using the
returned values in the Socket.new call, rather than using potentially
mismatched values for the socket’s address family specified in the
“new” call and the address returned from pack_sockaddr_in. Note that
when calling pack_sockaddr_in it also passes that call the IP address
rather than the hostname.

On Sep 6, 2006, at 8:15 AM, Daniel M. wrote:

 sockaddr = Socket.pack_sockaddr_in( sockaddrinfo[1],  

getaddrinfo. That “sockaddr =” bit should be:

sockaddrinfo = Socket.getaddrinfo( ‘localhost’, 2200, AF_INET,
SOCK_STREAM, nil, AI_PASSIVE )[0]

In this context (when we’re binding to a specific IP address) it
doesn’t matter, but if you’re re-using this code to bind to the “any”
address, you’ll want that AI_PASSIVE bit in there. (You’ll also want
to replace ‘localhost’ with nil)

This script ran perfectly. Thanks for all your help. When I get
some time I’ll sit down to actually process what your above script is
actually doing :slight_smile:

Thanks,
Patrick