Question About TCPServer & TCPSocket classes

S

Steve Lewis

Hello Rubyists,

I've been messing around with socket programming in Ruby lately, and I've
hit a snag with some of my experiments. My first, (successful),
experiment was to create a basic client/server script wherein the client
sends a string to the server, and the server responds by simply writing
the string back to the client...that code is as follows:

Server:
#####################################
require 'socket'

class PingServer < TCPServer

def start_server
loop do
Thread.start(self.accept) do |s|
p "Connection accepted from server #{s.inspect}"
request = s.readline.gsub(/\n$/, '')
p "Request was #{request}"
s.write "Your request was as follows: #{request}"
s.write Time.now if request == 'time'
s.close
p "Connection #{s} closed"
end
end
end

end

PingServer.new('localhost', 3000).start_server

###############################################



The client code is thus:
########################################
require 'socket'



def send_message(message)
TCPSocket.open('localhost', 3000) do |client|
client.write(message)
stuff = client.read(100)
client.close
p stuff
get_message
end
end

def get_message
message = gets
send_message(message) unless message.gsub(/\n/, '') == 'quit'
exit
end


get_message

##################################

As I said, the above code works as expected, I start the server, then run
the client program...I can send strings to the server all day long, and
the server writes them right back to me.

The problem occurs when I try to send the following instead of a string:

Marshal.dump(%w(foo bar baz))

Sending the resulting string over the wire causes the server to hang...it
doesn't return the string back to the client...I'm at a loss to figure
out why it seems to have trouble handling the string...

Ideas would be appreciated!!

Thanks

Steve
 
J

Joel VanderWerf

Steve said:
Hello Rubyists,

I've been messing around with socket programming in Ruby lately, and I've
hit a snag with some of my experiments. My first, (successful),
experiment was to create a basic client/server script wherein the client
sends a string to the server, and the server responds by simply writing
the string back to the client...that code is as follows:

Server:
#####################################
require 'socket'

class PingServer < TCPServer

def start_server
loop do
Thread.start(self.accept) do |s|
p "Connection accepted from server #{s.inspect}"
request = s.readline.gsub(/\n$/, '')
^^^^^^^^

This method reads until newline, and binary data (such as Marshal.dump
output) is not generally delimited that way. Use IO#read or Socket#recv
instead.

If you want to read a binary message, you can do one of several things:

1. pick a delimiter char and escape that wherever it occurs within the
binary data (yuck)

2. send a length field before sending the data, so the receiver knows
how much to wait for

3. send one message per socket connection

4. encode the binary data in some non-binary form: e.g. base64, using
pack("m"), and delimit using blank lines.
 
R

Robert Klemme

Hello Rubyists,

I've been messing around with socket programming in Ruby lately, and I've
hit a snag with some of my experiments. My first, (successful),
experiment was to create a basic client/server script wherein the client
sends a string to the server, and the server responds by simply writing
the string back to the client...that code is as follows:

Server:
#####################################
require 'socket'

class PingServer < TCPServer

def start_server
loop do
Thread.start(self.accept) do |s|
p "Connection accepted from server #{s.inspect}"
request = s.readline.gsub(/\n$/, '')
p "Request was #{request}"
s.write "Your request was as follows: #{request}"
s.write Time.now if request == 'time'
s.close
p "Connection #{s} closed"
end
end
end

end

PingServer.new('localhost', 3000).start_server

###############################################



The client code is thus:
########################################
require 'socket'



def send_message(message)
TCPSocket.open('localhost', 3000) do |client|
client.write(message)
stuff = client.read(100)
client.close
p stuff
get_message
end
end

def get_message
message = gets
send_message(message) unless message.gsub(/\n/, '') == 'quit'
exit
end


get_message

##################################

As I said, the above code works as expected, I start the server, then run
the client program...I can send strings to the server all day long, and
the server writes them right back to me.

The problem occurs when I try to send the following instead of a string:

Marshal.dump(%w(foo bar baz))

Sending the resulting string over the wire causes the server to hang...it
doesn't return the string back to the client...I'm at a loss to figure
out why it seems to have trouble handling the string...

Ideas would be appreciated!!

Steve, you are mixing #read, #write on one side and #readline on the
other side. Since Marshal.dump creates binary data your #readline on
the server won't properly work. You should either use a binary
protocoll *or* a textual (line based) protocol. Mixing both causes
problems.

Just an additional note: if you use Marshal, you can directly read to
and write from the socket without the intermediate String. This is
likely more efficient and also you avoid the issue of not knowing how
long the Marshal sequence is.

And yet another note: just in case you need a solution with
communicating processes where all of them are Ruby applications you can
use DRuby. But I guess at the moment you want to learn about Ruby
socket programming so this is probably just for keeping in the back of
your head.

Kind regards

robert
 
J

Joel VanderWerf

Robert said:
Just an additional note: if you use Marshal, you can directly read to
and write from the socket without the intermediate String. This is
likely more efficient and also you avoid the issue of not knowing how
long the Marshal sequence is.

Good point. I forgot that the output of Marshal records how long the
data is, and therefore #load can read the correct number of bytes
directly from an io.
And yet another note: just in case you need a solution with
communicating processes where all of them are Ruby applications you can
use DRuby. But I guess at the moment you want to learn about Ruby
socket programming so this is probably just for keeping in the back of
your head.

Another possibility, if you are communicating to C code or other languages:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/177704

It's a lib for both C and ruby that uses length fields to make tcp
sockets "message oriented" rather than stream oriented.
 
S

Steve Lewis

Steve, you are mixing #read, #write on one side and #readline on the
other side. Since Marshal.dump creates binary data your #readline on
the server won't properly work. You should either use a binary
protocoll *or* a textual (line based) protocol. Mixing both causes
problems.

Just an additional note: if you use Marshal, you can directly read to
and write from the socket without the intermediate String. This is
likely more efficient and also you avoid the issue of not knowing how
long the Marshal sequence is.

And yet another note: just in case you need a solution with
communicating processes where all of them are Ruby applications you can
use DRuby. But I guess at the moment you want to learn about Ruby
socket programming so this is probably just for keeping in the back of
your head.

Kind regards

robert

Robert,

Thanks for the help! You're correct, my curiosity here is mostly
academic, but DRuby looks like the practical solution to use when I
decide I'd like to try something more substantial.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,767
Messages
2,569,571
Members
45,045
Latest member
DRCM

Latest Threads

Top