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

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 Toomey <ptoomey3@mac.com> 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".

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.

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 9/5/06, Patrick Toomey <ptoomey3@mac.com> wrote:

Thanks,
Patrick

--
"What is your function in life?" - Killer

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

···

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

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

--
Eric Hodel - drbrain@segment7.net - http://blog.segment7.net
This implementation is HODEL-HASH-9600 compliant

http://trackmap.robotcoop.com

Patrick Toomey <ptoomey3@mac.com> writes:

<snip>

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.

···

--
s=%q( Daniel Martin -- martin@snowplow.org
       puts "s=%q(#{s})",s.map{|i|i}[1] )
       puts "s=%q(#{s})",s.map{|i|i}[1]

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

···

On 9/6/06, Arnaud Bergeron <abergeron@gmail.com> wrote:

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.

Daniel Martin <martin@snowplow.org> writes:

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

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)

···

--
s=%q( Daniel Martin -- martin@snowplow.org
       puts "s=%q(#{s})",s.map{|i|i}[1] )
       puts "s=%q(#{s})",s.map{|i|i}[1]

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

···

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

Daniel Martin <martin@snowplow.org> writes:

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

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)