TCP sockets

&

Ruby

Create QR Code


             http://slid.es/dominikwronski/tcp-sockets


            dominik.wronski@gmail.com

            @DominikWro


Agenda

                                          Socket
                                          Server lifecycle
                                          Client lifecycle
                                          Client / Server app

                                          Architectural solutions:

        • Serial
        • Process per connection
        • Thread per connection
        • Preforking
        • Thread Pool
        • Evented (Reactor)

A socket

 A socket address is the combination of an IP address and

    

 Based on this address, internet sockets deliver incoming 

 data packets to the appropriate application process or thread.

                                                                                                     
                                                                                                                                                                       

Create Socket

require 'socket'

socket=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM)

require 'socket'
 
socket = Socket.new(:INET6, :STREAM)


STREAM vs DGRAM
TCP vs UDP


Client vs Server


When you create a socket it must assume one of two roles:

                             1) initiator or

                             2) listener



                   A socket that listens is a server

         A socket that initiates a connection is a client

Server

Server socket listens for connections rather than initiating them.

The typical life-cycle looks something like this:

       1. CREATE
2. BIND

    3. LISTEN
      4. ACCEPT
   5. CLOSE


CREATE


require 'socket'
socket = Socket.new(:INET, :STREAM)


Bind

require 'socket'
socket = Socket.new(:INET, :STREAM)

# Create a C struct to hold the address for listening.
 
addr = Socket.pack_sockaddr_in(4481,'0.0.0.0') socket.bind(addr)

LISTEN

require 'socket'

server = Socket.new(:INET, :STREAM)
addr =Socket.pack_sockaddr_in(4481,'0.0.0.0')

server.bind(addr)

server.listen(Socket::SOMAXCONN)




ACCEPT

require 'socket'

server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481,'0.0.0.0')
server.bind(addr)
server.listen(Socket::SOMAXCONN)

connection, _ = server.accept 



CLOSE



read / write / shutdown

require 'socket'

server = Socket.new(:INET, :STREAM)
addr= Socket.pack_sockaddr_in(4481,'0.0.0.0')
server.bind(addr)
server.listen(Socket::SOMAXCONN)
connection, _ = server.accept

# Create a copy of the connection. copy = connection.dup
# This shuts down communication on all  
# copies of the connection.

connection.shutdown
# This closes the original connection.
# The copy will be closed when the GC
# collects it.
connection.close



RUBY WRAPPERS



require 'socket'

Socket.tcp_server_loop(4481) do |connection| # handle connection connection.close end


Client


The client lifecycle:

       1. CREATE

2. BIND

          3. CONNECT

    4. CLOSE


CREATE


require 'socket'
socket = Socket.new(:INET, :STREAM)

BIND


require 'socket'socket = Socket.new(:INET, :STREAM)

remote_addr= Socket.pack_sockaddr_in(80,'google.com')

CONNECT


require 'socket'

socket = Socket.new(:INET, :STREAM) remote_addr= Socket.pack_sockaddr_in(80,'google.com') socket.connect(remote_addr)



CLOSE



nothing new here ;)

Client / Server app





SERVER

require 'socket'

module
CloudHash
class Server def initialize(port) @server = TCPServer.new(port) puts "Listening on port
#{@server
.local_address.ip_port}" @storage = {} end
def start
Socket.accept_loop(@server) do |connection| handle(connection) connection.close end end

  def handle(connection)
    request = connection.read
    connection.write process(request)
  end

  def process(request)
    command, key, value = request.split
case command.upcase when 'GET' @storage[key] when 'SET' @storage[key] = value end end end end

server = CloudHash::Server.new(4481) server.start



CLIENT

require 'socket'
  module CloudHash
    class Client
      class << self
        attr_accessor :host, :port
      end

  def self.get(key) request "GET #{key}" end

 def
self.set(key, value) request "SET #{key} #{value}"
end

      def self.request(string)
        @client = TCPSocket.new(host, port)
        @client.write(string)
        # Send EOF after writing the request.
        @client.close_write
        @client.read
      end
    end
  end

CloudHash
::Client.host = 'localhost' CloudHash::Client.port = 4481

puts CloudHash::Client.set 'Charlie','Unicorn' puts CloudHash::Client.get 'Charlie'



Architectural Solutions

                                            Serial

                                        Process per connection


                                        Thread per connection


                                        Preforking


                                        Thread Pool


                                        Evented (Reactor)

module FTP
  module Common 
CRLF = "\r\n"
def initialize(port = 21) @control_socket = TCPServer.new(port)
trap(:INT) { exit }
end

def
respond(response)
@client.write(response)
@client.write(CRLF)
end
    class CommandHandler 
attr_reader :connection

def initialize(connection)
@connection = connection
end
 
def
pwd
@pwd || Dir.pwd
end



      def handle(data) 
cmd = data[0..3].strip.upcase
options = data[4..-1].strip

case cmd
when 'USER'
# Accept any username anonymously
"230 Logged in anonymously"
      when 'SYST'
      # what's your name?
      "215 UNIX Working With FTP"
      when 'CWD'
        if File.directory?(options)
          @pwd = options
          "250 directory changed to #{pwd}"
        else
          "550 directory not found"
        end
      when 'PWD'
        "257 \"#{pwd}\" is the current directory"


    when 'PORT'
      parts = options.split(',')
      ip_address = parts[0..3].join('.')
      port = Integer(parts[4]) * 256 + Integer(parts[5])
      @data_socket = TCPSocket.new(ip_address, port)
      "200 Active connection established (#{port})"
    when 'RETR'
      file = File.open(File.join(pwd, options), 'r')
      connection.respond "125 Data transfer starting #{file.size} bytes"
      bytes = IO.copy_stream(file, @data_socket)
      @data_socket.close
      "226 Closing data connection, sent #{bytes} bytes"
   when 'LIST'      connection.respond "125 Opening data connection for file list"
     result = Dir.entries(pwd).join(CRLF)
     @data_socket.write(result)
     @data_socket.close
     "226 Closing data connection, sent #{result.size} bytes"
   when 'QUIT'
     "221 Ciao"
   else
     "502 Don't know how to respond to #{cmd}"
   end
  end
end 

Serial



     1. Client connects.

    2. Client/server exchange requests and responses.
               
    3. Client disconnects.
               
    4. Back to step #1.


require 'socket'
require_relative '../common'
module FTP
  class Serial
    include Common
    def run
      loop do
        @client = @control_socket.accept
        respond "220 OHAI"
        handler = CommandHandler.new(self) 
        loop do
          request = @client.gets(CRLF)
          if request
            respond handler.handle(request)
          else
            @client.close                   break
          end

Process per connection



  1. A connection comes in to the server.
     
  2. The main server process accepts the connection.
     
  3. It forks a new child process which is an exact copy of
      the server process.
 
  4. The child process continues to handle its connection in       parallel while the server process goes back to step #1.

class ProcessPerConnection
  include Common
  def run
    loop do
      @client = @control_socket.accept
        pid = fork do
        respond "220 OHAI"
        handler = CommandHandler.new(self)
        loop do
          request = @client.gets(CRLF)
          if request
            respond handler.handle(request)
          else
            @client.close
            break 
          end
        end
      end
        Process.detach(pid)
    end

Thread per connection




class ThreadPerConnection
  include Common
  def run
    Thread.abort_on_exception = true
    loop do
      @client = @control_socket.accept
      Thread.new do
      respond "220 OHAI"
      handler = CommandHandler.new(self)
      loop do
        request = @client.gets(CRLF) 
             if request
          respond handler.handle(request)
        else
          @client.close
          break
        end
        end
    end
  end

PREFORKING


   1. Main server process creates a listening socket.
        
   2. Main server process forks a horde of child processes
        
   3. Each child process accepts connections on the shared
       socket and handles them independently.

      

   4. Main server process keeps an eye on the child processes.

class Preforking
include Common
CONCURRENCY = 4
def run
child_pids = []
CONCURRENCY.times do
child_pids << spawn_child
end


     trap(:INT) {
      child_pids.each do |cpid|
        begin
          Process.kill(:INT, cpid)
          rescue Errno::ESRCH
          end
        end
    exit
   }
   loop do
     pid = Process.wait
     $stderr.puts "Process #{pid} quit unexpectedly"
     child_pids.delete(pid)
     child_pids << spawn_child

  def spawn_child
    fork do
      loop do
        @client = @control_socket.accept
        respond "220 OHAI"
        handler = CommandHandler.new(self)
 
        loop do
          request = @client.gets(CRLF)
          if request
            respond handler.handle(request)
          else
            @client.close
            break
          end
        end

Thread Pool

class ThreadPool
  include Common
  CONCURRENCY = 25
  def run
    Thread.abort_on_exception = true
    threads = ThreadGroup.new
    CONCURRENCY.times do
      threads.add spawn_thread
    end
    sleep
  end

def spawn_thread Thread.new do loop do @client = @control_socket.accept respond "220 OHAI" handler = CommandHandler.new(self) loop do request = @client.gets(CRLF) if request respond handler.handle(request) else @client.close break end end

Evented (Reactor)


                1. The server monitors the listening socket for incoming connections.
2. Upon receiving a new connection it adds it to the list of sockets to monitor.
       3. The server now monitors the active connection as well as the listening socket.
    
4. Upon being notified that the active connection is readable the server reads a
    chunk of data from that connection and dispatches the relevant callback.

         5. Upon being notified that the active connection is still readable the server reads
                      another chunk and dispatches the callback again.
                 6. The server receives another new connection; it adds that to the list of sockets to  
                       monitor.

                 7. The server is notified that the first connection is ready for writing, so the
                        response is written out on that connection
                    
class Evented
  CHUNK_SIZE = 1024 * 16
  include Common
  class Connection
    include Common
    attr_reader :client 
def initialize(io) @client = io @request, @response = "", "" @handler = CommandHandler.new(self) @response = "220 OHAI" + CRLF on_writable enc

def on_data(data)
  @request << data
  if @request.end_with?(CRLF)
  # Request is completed.
    @response = @handler.handle(@request) + CRLF
    @request = ""
  end
end

def on_writable
bytes = client.write_nonblock(@response) @response.slice!(0, bytes) end
def monitor_for_reading?
  true
end

def monitor_for_writing? !(@response.empty?) end

def run
  @handles = {}
  loop do
 to_read=@handles.values.select(&:monitor_for_reading?).map(&:client) to_write=@handles.values.select(&:monitor_for_writing?).map(&:client)
readables,writables=IO.select(to_read + [@control_socket], to_write)
readables.each do |socket| if socket == @control_socket io = @control_socket.accept connection = Connection.new(io) @handles[io.fileno] = connection
     else
       connection = @handles[socket.fileno]
       begin
         data = socket.read_nonblock(CHUNK_SIZE)
connection.on_data(data)
rescue Errno::EAGAIN
rescue EOFError
@handles.delete(socket.fileno)
end
end
end
writables.each do |socket|
connection = @handles[socket.fileno]
connection.on_writable
end


TCP sockets

By Dominik Wronski

TCP sockets

How to TCP socket works. Examples how handle TCP Socket in Ruby. Want to learn more, check out Pragmatic Programmer "Working With TCP Sockets" by Jesse Storimer. >>> For DRUG Wrocław 21 Oct '13 <<<

  • 1,748