TCP sockets
&
Ruby
http://slid.es/dominikwronski/tcp-sockets
dominik.wronski@gmail.com
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. BIND3. 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 = {} enddef 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
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
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
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
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)
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
monitor.
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_writablebytes = 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,808