Hi Shannon,
I just found an example in talk archive about using thread with tcp server.
If anyone have a solution that does not use thread, please tell me,
otherwise, please accept my apology that I should check the talk-archive
first…
I prefer to use select() than threads, for a couple reasons, in Ruby.
My sense is that, since the whole process is blocked (all your threads)
if one of your IO calls stalls (in any thread), then you have to find
a way to do non-blocking IO whether you’re using threads or not. And I
personally find select() to be sufficient to the task without bothering
with threads. (It may just be personal opinion, or preference.
If it helps, here’s a snippet from a TCP-based chat-like server I
wrote recently. I’ve tried to delete the parts of the code that are
relevant only to my specific application. Thus the code below is
untested, but is drawn from working code (for whatever that’s worth
.)
def accept_and_process_clients(timeout)
begin
# This select call is the key to the whole operation. In my program,
# I only care which clients are ready to be read from (or whether the
# server socket is ready to accept a new connection from a client.)
# You’ll notice I pass in ‘nil’ for asking which sockets are ready
# to be written to. In my case, when I have something to write to
# a socket, I can get away with just doing a blocking write, because
# for my purposes it’s fast enough. You may or may not actually want
# to check which sockets are “ready” to be written to, to avoid any
# blocking at all in your writes.
ios = select([@tcp_server, *@tcp_clients], nil, @tcp_clients, timeout)
if ios
# disconnect any clients with errors
ios[2].each {|sock| ios[0].delete(sock); disconnect_client(sock, false) }
# accept new clients or process existing client input
ios[0].each do |sock|
if sock == @tcp_server
accept_client(sock)
else
process_client_input(sock) # whatever this means in your app.
end
end
end
rescue IOError, SystemCallError
end
end
def accept_client(sock)
client = sock.accept
if client
begin
# Initialize client however makes sense in your application.
# In my case I extend the client socket with some extra
# features from a login-session/terminal-server module.
# I also get the client’s IP and Port numbers from peeraddr,
# which can throw exceptions if there’s a problem, thus the
# rescue clause below…
rescue IOError, SystemCallError
client.close
else
@tcp_clients << client
end
end
end
def disconnect_client(client)
@tcp_clients.delete client
client.close
end
def run
@log_file = File.open(@log_filename, File::WRONLY | File::CREAT | File::APPEND)
@tcp_server = TCPServer.new(@local_port)
begin
loop {
# I do other periodic tasks in the loop besides accept and
# respond to clients - so I have a slightly more complicated
# way of keeping track of the timeout/next-update interval
# which I’ve deleted in this example…
timeout = WhateverYouNeed
accept_and_process_clients(timeout)
}
rescue Interrupt, IRB::Abort, NoMemoryError, SystemExit => ex
puts “Caught exception: #{ex.inspect} at #{ex.backtrace[0]} - saving and shutting down…”
rescue Exception => ex
puts “Caught exception: #{ex.inspect} at #{ex.backtrace[0]} - ignoring and continuing…”
sleep 1
retry
ensure
disconnect_all_clients
@tcp_server.close; @tcp_server = nil
@log_file.close; @log_file = nil
end
end
For completeness, here’s routines where I actually end up doing send &
recv on the client sockets. They’re part of a buffered terminal IO
module which I’ve extended onto each client socket returned from the
@tcp_server’s accept. (Thus the “self.recv”, etc.)
def terminal_buffered_read
begin
if Kernel.select([self], nil, nil, 0)
dat = self.recv(65536)
if !dat || dat.empty?
@term_eof = true
else
@term_read_buf << dat
end
end
rescue IOError, SystemCallError
@term_eof = true
end
end
def send_string(str)
str = str.gsub(/\n/, “\r\n”)
begin
while str.length > 0
sent = self.send(str, 0)
str = str[sent…-1]
end
rescue IOError, SystemCallError
@term_eof = true
end
end
If the recv returns EOF, or if something goes wrong with the
send or recv, I just set a @term_eof flag, because that’s sufficient
for my application… You might want to handle errors differently.
(Note, again, that my send is a blocking-send. But you can make yours
non-blocking using select() if you need to.)
Finally, I should mention that in theory, since Windows doesn’t
support true non-blocking IO on its sockets (as far as I know)
that there is a possibility that the accept() call could block.
[Note, the above code, in the context of my application, has been
used without problem on Windows and Linux.] The possibility for
accept() blocking happens when a client tries to connect, and
select() says “read me!”, but the client hurriedly disconnects,
so that by the time we actually get to the accept(), the client
is gone, and thus the accept() call blocks until a new client
connects. This has not yet happened in my application, but it
should be possible. To avoid this in Unix, I would put the
@tcp_server socket into true non-blocking mode, where in the
above condition, it would return the error EWOULDBLOCK instead
of blocking.
Again, however, in Ruby, using threads would not be a solution to
the above. Because if accept() blocks, your whole process (all
your threads) are blocked. So I’d suggest using threads only if
that model is more convenient to how your application processes
its data. For mine, threads seemed like extra work.
Hope this helps,
Bill