http://slid.es/dominikwronski/tcp-sockets
dominik.wronski@gmail.com
Socket
Server lifecycle
Client lifecycle
Client / Server app
Architectural solutions:
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.
require 'socket'
socket=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM)
require 'socket'
socket = Socket.new(:INET6, :STREAM)
STREAM vs DGRAM
TCP vs UDP
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 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
require 'socket'
socket = Socket.new(:INET, :STREAM)
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)
require 'socket'
server = Socket.new(:INET, :STREAM)
addr =Socket.pack_sockaddr_in(4481,'0.0.0.0')
server.bind(addr)
server.listen(Socket::SOMAXCONN)
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
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
require 'socket'
Socket.tcp_server_loop(4481) do |connection|
# handle connection
connection.close
end
The client lifecycle:
1. CREATE
2. BIND
3. CONNECT
4. CLOSE
require 'socket'
socket = Socket.new(:INET, :STREAM)
require 'socket'
socket = Socket.new(:INET, :STREAM) remote_addr= Socket.pack_sockaddr_in(80,'google.com')
require 'socket'
socket = Socket.new(:INET, :STREAM)
remote_addr= Socket.pack_sockaddr_in(80,'google.com')
socket.connect(remote_addr)
nothing new here ;)
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
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'
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
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
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
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
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
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
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