Skip to content

Instantly share code, notes, and snippets.

@TeWu
Last active February 2, 2022 20:23
Show Gist options
  • Save TeWu/1234573 to your computer and use it in GitHub Desktop.
Save TeWu/1234573 to your computer and use it in GitHub Desktop.
TCP client and multithreaded server in 14 lines of Ruby code

TCP client and multithreaded server in 14 lines of Ruby code

Server:

require "socket"
server = TCPServer.open(2626)
loop do
	Thread.fork(server.accept) do |client| 
		client.puts("Hello, I'm Ruby TCP server", "I'm disconnecting, bye :*")
		client.close
	end
end

Client:

require "socket"
s = TCPSocket.open("localhost", 2626)
while line = s.gets
	puts "received : #{line.chop}"
end
s.close

.. but can be minified to 7 lines without using instruction terminator ";"

.. just 4 fun ;)

Server:

require "socket"
server = TCPServer.open(2626)
loop { Thread.fork(server.accept) { |client| client.puts("Hello, I'm Ruby TCP server", "I'm disconnecting, bye :*") or client.close }}

Client:

require "socket"
s = TCPSocket.open("localhost", 2626)
while line = s.gets do puts "received : #{line.chop}" end
s.close
@zipizap
Copy link

zipizap commented Jun 20, 2013

Version using Kernel::fork (new process) instead of Thread::fork (new thread) - MRI-Ruby only has green-threads, it can be better to use child-processes than child-threads in some situations)

require "socket"
server = TCPServer.open(2626)
while client=server.accept
    fork do 
        client.puts("Hello, I'm Ruby TCP server", "I'm disconnecting, bye :*")
        client.close # child's connection to client is closed
    end
    client.close  # parent's connection to client is closed
end

@cleesmith
Copy link

How's about the evented option? ... not that many more lines of code really

TCP server:

require 'eventmachine'

PORT = 4545
puts "Listening on #{PORT}...\n"

class Counter
  attr_accessor :total_data

  def initialize
    @total_data = 0
  end

  def total_data=(num)
    @total_data += num
  end
end

class EchoServer < EM::Connection
  def initialize(counter)
    @total = 0
    @total_data = 0
    @counter = counter
  end

  def receive_data(data)
    @total += 1
    @total_data += data.length
    @counter.total_data = data.length
    # puts "\ntotal=#{@total} \t #{data.length}\n"
    # send_data(data)
    # do some work, monkey with the data, kill some time:
    lines = data.split("\n")
    # puts "lines=#{lines.length}"
    lines.each do |line|
      fields = line.split("\t")
      # puts "\tfields=#{fields.length}"
      fields.each do |field|
        # puts "\t\tfield=#{field.length}"
      end
    end
  end
end

EventMachine.run do
  counter = Counter.new
  Signal.trap("INT")  { EventMachine.stop }
  Signal.trap("TERM") { EventMachine.stop }
  EventMachine.start_server("127.0.0.1", PORT, EchoServer, counter)
  EM.add_periodic_timer(30) { puts "counter=#{counter.inspect}" }
end

TCP client:

require 'socket'
start = Time.now
begin
  if ARGV.empty?
    port = 4545
  else
    port = ARGV[0].to_i
  end
  client = TCPSocket.new('127.0.0.1', port)
  # puts "Socket::TCP_NODELAY=#{Socket::TCP_NODELAY}"
  client.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) # Nagle off
  # 420_000_000.times do |x| <<--- took about 40 minutes to process 128,100,000,000 of data (OS X)
  1_000_000.times do |x|
    # send 305 bytes:
    msg  = "Lorem \t ipsum \t dolor sit amet, consectetur adipiscing elit. Donec luctus enim mollis eros interdum egestas. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae est pellentesque, suscipit nisl et, rutrum nulla. Nullam sed justo nisl. Donec rutrum velit odio, non sollicitudin lorem amet.\n"
    # msg += "Lorem \t ipsum \t dolor sit amet, consectetur adipiscing elit. Donec luctus enim mollis eros interdum egestas. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae est pellentesque, suscipit nisl et, rutrum nulla. Nullam sed justo nisl. Donec rutrum velit odio, non sollicitudin lorem amet.\n"
    # msg += "Lorem \t ipsum \t dolor sit amet, consectetur adipiscing elit. Donec luctus enim mollis eros interdum egestas. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae est pellentesque, suscipit nisl et, rutrum nulla. Nullam sed justo nisl. Donec rutrum velit odio, non sollicitudin lorem amet.\n"
    # msg += "Lorem \t ipsum \t dolor sit amet, consectetur adipiscing elit. Donec luctus enim mollis eros interdum egestas. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vitae est pellentesque, suscipit nisl et, rutrum nulla. Nullam sed justo nisl. Donec rutrum velit odio, non sollicitudin lorem amet.\n"
    # puts "#{msg.length}\n"
    client.puts(msg)
    # ignore response:
    # resp = client.gets
    # puts "resp=#{resp}"
  end
ensure
  client.close if client
  puts "elapsed: #{(Time.now - start)}"
end

Why ?
Well, it's all the rage (or was), it's very fast, it doesn't use a lot of memory (hovered around 30K) compared to
what it's processing, and there's no worries about threading and forking and all those entail.

Sure, you are not going to traipse off to the bonneville salt flats with this server and break any speed records,
but after trying all of the other options this seems to work the best for pushing data into a server for various
purposes. It's amazing what 50 lines of ruby code, not counting eventmachine itself, and *nix can do. Also,
all of my testing was done with ruby MRI.


fin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment