Overview
The TCP three-way handshake establishes a reliable connection between two network endpoints before data transmission begins. This connection establishment protocol ensures both client and server agree on initial sequence numbers, window sizes, and other TCP parameters necessary for reliable data transfer.
The handshake consists of three packet exchanges: SYN (synchronize), SYN-ACK (synchronize-acknowledge), and ACK (acknowledge). The client initiates the connection by sending a SYN packet with an initial sequence number. The server responds with a SYN-ACK packet containing its own sequence number and acknowledging the client's sequence number. The client completes the handshake by sending an ACK packet acknowledging the server's sequence number.
This process occurs beneath application-layer protocols like HTTP, SMTP, and FTP. When a web browser requests a page, the TCP three-way handshake completes before any HTTP data transfers. The handshake typically completes within milliseconds on local networks and hundreds of milliseconds across the internet, adding one round-trip time (RTT) to connection establishment.
Client Server
| |
|-------SYN seq=X-------->| (Step 1: Client requests connection)
| |
|<----SYN-ACK seq=Y-------| (Step 2: Server acknowledges and responds)
| ack=X+1 |
| |
|-------ACK seq=X+1-------| (Step 3: Client acknowledges)
| ack=Y+1 |
| |
| Connection Established|
Understanding the three-way handshake helps diagnose connection problems, optimize network performance, and implement custom network protocols. Network monitoring tools display handshake packets, making this knowledge essential for troubleshooting connection failures and analyzing network behavior.
Key Principles
The three-way handshake operates on several fundamental principles that ensure reliable connection establishment.
Sequence Number Synchronization
Each TCP connection uses sequence numbers to track bytes transmitted and received. During the handshake, both endpoints exchange their Initial Sequence Numbers (ISNs). The client selects a random ISN and transmits it in the SYN packet. The server generates its own random ISN and sends it in the SYN-ACK packet. This synchronization allows both sides to track data segments and detect missing or duplicate packets.
Modern implementations generate ISNs using cryptographic algorithms that incorporate timestamps and connection parameters. This prevents attackers from predicting sequence numbers and injecting malicious packets. The ISN increases over time, typically incrementing by a fixed amount every 4 microseconds, with additional randomization for security.
Connection State Management
Both client and server maintain connection state throughout the handshake. The client transitions from CLOSED to SYN-SENT after sending the initial SYN packet. The server moves from LISTEN to SYN-RECEIVED after receiving the SYN and sending SYN-ACK. When the client sends the final ACK, both endpoints enter the ESTABLISHED state and data transfer begins.
Client States: Server States:
CLOSED LISTEN
↓ (send SYN) ↓ (receive SYN)
SYN-SENT SYN-RECEIVED
↓ (receive SYN-ACK) ↓ (receive ACK)
ESTABLISHED ESTABLISHED
The state machine prevents invalid transitions and ensures proper cleanup if the handshake fails. If the client receives an unexpected packet while in SYN-SENT state, it discards the packet or sends a RST (reset) to terminate the connection attempt.
Acknowledgment Mechanism
Each packet in the handshake acknowledges previously received data. The acknowledgment number indicates the next sequence number the sender expects to receive. In the SYN-ACK packet, the server sets the acknowledgment number to the client's ISN plus one, confirming receipt of the SYN packet. The client's final ACK packet acknowledges the server's ISN plus one.
This bidirectional acknowledgment confirms both endpoints received the connection parameters. Without successful acknowledgment, the sender retransmits the packet after a timeout period. The retransmission timeout starts at a conservative value (typically 1-3 seconds) and increases exponentially with each failed attempt.
Window Size Negotiation
The handshake packets include a window size field that specifies how many bytes the sender can receive before requiring an acknowledgment. This flow control mechanism prevents fast senders from overwhelming slow receivers. The client advertises its receive window in the SYN packet, and the server advertises its window in the SYN-ACK packet.
Window scaling extends the window size beyond the 16-bit field limit. Both endpoints negotiate window scaling during the handshake using TCP options. If both sides support window scaling, they can advertise windows up to 1 gigabyte, improving throughput on high-bandwidth, high-latency networks.
TCP Options Exchange
The SYN and SYN-ACK packets carry TCP options that negotiate connection parameters. Maximum Segment Size (MSS) specifies the largest data segment the receiver accepts, typically derived from the Maximum Transmission Unit (MTU) minus IP and TCP headers. Selective Acknowledgment (SACK) support allows receivers to acknowledge non-contiguous data blocks. Timestamps enable round-trip time measurement and protect against wrapped sequence numbers.
Ruby Implementation
Ruby's Socket library provides direct access to TCP connections, exposing the three-way handshake through high-level abstractions. The TCPSocket and TCPServer classes handle handshake details automatically, while Socket provides lower-level control.
Basic TCP Client Connection
Creating a TCP client connection triggers the three-way handshake automatically. The socket blocks until the handshake completes or times out.
require 'socket'
# Initiates three-way handshake with remote server
socket = TCPSocket.new('example.com', 80)
# Connection now established, ready for data transfer
socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
response = socket.read
socket.close
The TCPSocket.new method performs DNS resolution, creates a socket, sends the SYN packet, waits for SYN-ACK, sends ACK, and returns the connected socket. Network errors during the handshake raise Errno::ETIMEDOUT if the server doesn't respond, Errno::ECONNREFUSED if the server actively rejects the connection, or Errno::EHOSTUNREACH if routing fails.
TCP Server Implementation
A TCP server listens for incoming connections and accepts each connection through a complete three-way handshake.
require 'socket'
server = TCPServer.new('localhost', 8080)
puts "Server listening on port 8080"
# Accept blocks until client completes handshake
loop do
client = server.accept
# Three-way handshake complete, connection established
puts "Client connected: #{client.peeraddr[3]}"
client.puts "Hello from server"
client.close
end
The accept method blocks in the SYN-RECEIVED state until receiving the final ACK packet. The returned client socket represents an ESTABLISHED connection ready for bidirectional communication.
Non-Blocking Connection
Non-blocking sockets return immediately from connection attempts, allowing applications to perform other work during the handshake.
require 'socket'
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
# Set non-blocking mode
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
remote_addr = Socket.sockaddr_in(80, 'example.com')
begin
socket.connect(remote_addr)
rescue Errno::EINPROGRESS
# Handshake in progress, socket in SYN-SENT state
_, writable, _ = IO.select(nil, [socket], nil, 5)
if writable
# Check if connection succeeded
begin
socket.connect_nonblock(remote_addr)
rescue Errno::EISCONN
puts "Connection established"
rescue => e
puts "Connection failed: #{e.message}"
end
else
puts "Connection timeout"
end
end
socket.close
The EINPROGRESS error indicates the handshake started but hasn't completed. IO.select waits for the socket to become writable, signaling handshake completion. This pattern enables concurrent connection attempts and timeout control.
Connection Timeout Configuration
Ruby allows timeout configuration for connection establishment through the Timeout module or socket options.
require 'socket'
require 'timeout'
def connect_with_timeout(host, port, timeout_seconds)
Timeout.timeout(timeout_seconds) do
TCPSocket.new(host, port)
end
rescue Timeout::Error
raise "Connection to #{host}:#{port} timed out after #{timeout_seconds}s"
rescue Errno::ECONNREFUSED
raise "Connection to #{host}:#{port} refused"
end
# Attempt connection with 5-second timeout
begin
socket = connect_with_timeout('example.com', 80, 5)
puts "Connected successfully"
socket.close
rescue => e
puts "Connection failed: #{e.message}"
end
The timeout covers the entire handshake process, including DNS resolution and packet transmission. Systems with slow DNS resolution may timeout before sending the initial SYN packet.
Socket Option Configuration
Ruby exposes TCP options set during the handshake through setsockopt before calling connect.
require 'socket'
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
# Configure TCP keepalive
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, 60)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 10)
# Set TCP_NODELAY to disable Nagle's algorithm
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
# Configure send and receive buffer sizes
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 65536)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, 65536)
remote_addr = Socket.sockaddr_in(80, 'example.com')
socket.connect(remote_addr)
puts "Connected with custom TCP options"
socket.close
These options take effect during and after the handshake. Buffer size options advertise window sizes in the SYN packets, while TCP_NODELAY affects data transmission after connection establishment.
Practical Examples
Real-world scenarios demonstrate how the three-way handshake affects application behavior and performance.
HTTP Client with Connection Pooling
HTTP clients establish multiple TCP connections to improve throughput. Each connection requires a separate three-way handshake.
require 'socket'
require 'uri'
class SimpleHTTPClient
def initialize(max_connections: 5)
@max_connections = max_connections
@connections = {}
end
def get(url)
uri = URI(url)
socket = get_connection(uri.host, uri.port || 80)
request = "GET #{uri.path.empty? ? '/' : uri.path} HTTP/1.1\r\n"
request += "Host: #{uri.host}\r\n"
request += "Connection: keep-alive\r\n"
request += "\r\n"
socket.write(request)
read_response(socket)
end
private
def get_connection(host, port)
key = "#{host}:#{port}"
# Reuse existing connection if available
if @connections[key] && !@connections[key].closed?
return @connections[key]
end
# Perform three-way handshake for new connection
puts "Establishing new connection to #{host}:#{port}"
@connections[key] = TCPSocket.new(host, port)
end
def read_response(socket)
response = ""
while line = socket.gets
response += line
break if line == "\r\n"
end
response
end
end
client = SimpleHTTPClient.new
# First request performs handshake
response1 = client.get('http://example.com/')
# Second request reuses connection, no handshake
response2 = client.get('http://example.com/page')
Connection reuse eliminates repeated handshakes, reducing latency by one RTT per request. Modern HTTP clients maintain connection pools across multiple hosts, balancing the overhead of maintaining idle connections against handshake costs.
Load Balancer Health Checks
Load balancers probe backend servers by establishing TCP connections and verifying successful handshakes.
require 'socket'
require 'timeout'
class HealthChecker
def initialize(servers, check_interval: 10)
@servers = servers
@check_interval = check_interval
@healthy_servers = []
end
def start
Thread.new do
loop do
check_all_servers
sleep @check_interval
end
end
end
def healthy_servers
@healthy_servers.dup
end
private
def check_all_servers
results = @servers.map do |server|
Thread.new { [server, check_server(server)] }
end
@healthy_servers = results.map(&:value)
.select { |_, healthy| healthy }
.map { |server, _| server }
puts "Healthy servers: #{@healthy_servers.join(', ')}"
end
def check_server(server)
host, port = server.split(':')
Timeout.timeout(3) do
socket = TCPSocket.new(host, port.to_i)
socket.close
true
end
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Timeout::Error, SocketError
false
end
end
servers = ['localhost:8080', 'localhost:8081', 'localhost:8082']
checker = HealthChecker.new(servers, check_interval: 5)
checker.start
sleep 30 # Monitor for 30 seconds
Failed handshakes indicate server unavailability. The checker distinguishes between connection refused (server down), timeout (network issues), and successful connections (server healthy). This pattern forms the basis of load balancer health checking.
Parallel Connection Establishment
Applications often establish multiple connections concurrently to reduce overall latency.
require 'socket'
def parallel_connect(hosts_and_ports, timeout: 5)
threads = hosts_and_ports.map do |host, port|
Thread.new do
begin
start_time = Time.now
socket = TCPSocket.new(host, port)
duration = Time.now - start_time
[host, port, :success, duration, socket]
rescue => e
duration = Time.now - start_time
[host, port, :failed, duration, e]
end
end
end
# Wait for all handshakes to complete
results = threads.map(&:value)
results.each do |host, port, status, duration, result|
if status == :success
puts "Connected to #{host}:#{port} in #{(duration * 1000).round}ms"
result.close
else
puts "Failed to connect to #{host}:#{port}: #{result.message}"
end
end
results
end
servers = [
['example.com', 80],
['google.com', 80],
['github.com', 443]
]
parallel_connect(servers)
Parallel handshakes complete in the time of the slowest connection rather than the sum of all connection times. This technique improves startup latency for applications connecting to multiple services.
Retry Logic with Exponential Backoff
Network issues may cause handshake failures. Retry logic with exponential backoff prevents overwhelming failing servers.
require 'socket'
def connect_with_retry(host, port, max_attempts: 5)
attempt = 1
backoff = 1
while attempt <= max_attempts
begin
puts "Connection attempt #{attempt} to #{host}:#{port}"
socket = TCPSocket.new(host, port)
puts "Connected successfully on attempt #{attempt}"
return socket
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
puts "Attempt #{attempt} failed: #{e.message}"
if attempt == max_attempts
raise "Failed to connect after #{max_attempts} attempts"
end
sleep backoff
backoff *= 2 # Exponential backoff
attempt += 1
end
end
end
begin
socket = connect_with_retry('localhost', 8080)
socket.puts "Hello"
socket.close
rescue => e
puts "Connection failed: #{e.message}"
end
Exponential backoff reduces load on failing servers and gives transient issues time to resolve. The backoff interval doubles after each failure: 1s, 2s, 4s, 8s, preventing retry storms that exacerbate server problems.
Security Implications
The three-way handshake creates several security vulnerabilities that attackers exploit to disrupt services or compromise systems.
SYN Flood Attacks
SYN flood attacks exploit the handshake process by sending numerous SYN packets without completing the handshake. The server allocates resources for each half-open connection (SYN-RECEIVED state), exhausting memory and preventing legitimate connections.
Attackers typically spoof source IP addresses in SYN packets, making the server send SYN-ACK responses to non-existent hosts. The server waits for ACK packets that never arrive, leaving connections in SYN-RECEIVED state until timeout.
Mitigation strategies include:
- SYN Cookies: Generate ISNs cryptographically from connection parameters, eliminating state storage during handshake. The server validates returning ACK packets by recomputing the expected sequence number.
- Connection Limits: Restrict half-open connections per source IP address, blocking excessive SYN packets from single sources.
- Firewall Rules: Drop SYN packets matching attack patterns or rate-limit SYN packets per source.
Ruby applications behind reverse proxies inherit SYN flood protection from the proxy layer. Direct internet-facing Ruby servers require operating system tuning:
require 'socket'
server = TCPServer.new('0.0.0.0', 8080)
# Configure socket options to resist SYN floods
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_DEFER_ACCEPT, 5)
# Limit accept queue to prevent memory exhaustion
server.listen(128) # Limit backlog of pending connections
loop do
# Accept with timeout to prevent indefinite blocking
begin
if IO.select([server], nil, nil, 1)
client = server.accept
Thread.new(client) do |c|
handle_client(c)
end
end
rescue => e
puts "Accept failed: #{e.message}"
end
end
TCP_DEFER_ACCEPT delays accept until data arrives, reducing resource consumption for connection attempts that never send data.
Connection Hijacking
Attackers who predict sequence numbers can inject packets into established connections or hijack connections by sending spoofed ACK packets. Modern sequence number generation algorithms prevent prediction through cryptographic randomness.
Ruby applications cannot directly control ISN generation, relying on kernel implementation. Ensuring current kernel versions with RFC 6528 compliant sequence number generation protects against hijacking.
Reset Attacks
Attackers send RST (reset) packets with spoofed source addresses to terminate legitimate connections. Successful reset attacks require the attacker to guess the current sequence number within the receive window.
TCP implementations validate RST packets strictly, requiring sequence numbers within the current window. Window scaling increases the target range, making reset attacks easier. Applications sensitive to connection termination should detect unexpected resets and attempt reconnection:
require 'socket'
def resilient_connection(host, port)
socket = nil
loop do
begin
socket = TCPSocket.new(host, port)
yield socket
rescue Errno::ECONNRESET, Errno::EPIPE => e
puts "Connection reset: #{e.message}, reconnecting..."
socket.close if socket && !socket.closed?
sleep 1
rescue => e
puts "Fatal error: #{e.message}"
socket.close if socket && !socket.closed?
raise
end
end
end
resilient_connection('example.com', 80) do |socket|
socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
response = socket.read
puts response
end
Amplification Attacks
Attackers spoof victim IP addresses in SYN packets sent to multiple servers, causing those servers to flood the victim with SYN-ACK responses. This amplifies attack traffic through legitimate servers.
Rate limiting and source validation at network boundaries prevent participation in amplification attacks. Ruby servers cannot directly prevent this attack pattern but should implement connection rate limiting:
require 'socket'
class RateLimitedServer
def initialize(port, max_connections_per_ip: 10)
@server = TCPServer.new('0.0.0.0', port)
@max_connections_per_ip = max_connections_per_ip
@connections = Hash.new(0)
end
def start
loop do
client = @server.accept
client_ip = client.peeraddr[3]
if @connections[client_ip] >= @max_connections_per_ip
puts "Rate limit exceeded for #{client_ip}"
client.close
next
end
@connections[client_ip] += 1
Thread.new(client, client_ip) do |c, ip|
begin
handle_client(c)
ensure
@connections[ip] -= 1
c.close
end
end
end
end
private
def handle_client(client)
# Process client request
end
end
Common Pitfalls
Developers encounter recurring issues when working with TCP connections and the three-way handshake.
Ignoring Connection Timeouts
The default TCP connection timeout varies by operating system, often ranging from 20-120 seconds. Applications that don't configure timeouts block indefinitely on unresponsive servers.
# Problematic: blocks for system default timeout
socket = TCPSocket.new('unreachable-host.com', 80)
# Better: explicit timeout control
require 'timeout'
begin
Timeout.timeout(5) do
socket = TCPSocket.new('unreachable-host.com', 80)
end
rescue Timeout::Error
puts "Connection timeout after 5 seconds"
end
Always set explicit connection timeouts appropriate to application requirements. Interactive applications require short timeouts (1-5 seconds), while batch processes tolerate longer waits.
Resource Leaks from Failed Handshakes
Failed connection attempts leave sockets in half-open states, consuming file descriptors. Applications making many connection attempts must close sockets explicitly:
# Leaks socket on connection failure
def connect_unsafe(host, port)
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
addr = Socket.sockaddr_in(port, host)
socket.connect(addr) # May raise exception
socket
end
# Properly handles failures
def connect_safe(host, port)
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
addr = Socket.sockaddr_in(port, host)
begin
socket.connect(addr)
socket
rescue
socket.close
raise
end
end
Ruby's TCPSocket.new handles cleanup automatically, but lower-level Socket usage requires manual cleanup.
Firewall and NAT Traversal Issues
Firewalls block SYN packets or drop responses, causing handshake failures that applications misinterpret. NAT devices track connection state, rejecting packets that don't match existing mappings.
Connection attempts through corporate firewalls may require specific timeout values and retry logic. Testing in production environments reveals firewall behaviors not present in development:
def connect_through_firewall(host, port)
attempt = 1
errors = []
3.times do
begin
puts "Attempt #{attempt}: connecting to #{host}:#{port}"
socket = TCPSocket.new(host, port)
puts "Connection succeeded"
return socket
rescue Errno::ETIMEDOUT => e
errors << "Timeout on attempt #{attempt}"
rescue Errno::ECONNREFUSED => e
errors << "Connection refused on attempt #{attempt}"
return nil # Server explicitly rejected, don't retry
rescue => e
errors << "#{e.class}: #{e.message}"
end
attempt += 1
sleep 2
end
puts "All attempts failed:"
errors.each { |err| puts " #{err}" }
nil
end
Incomplete Error Handling
Different handshake failures produce different exceptions. Applications must handle each error type appropriately:
- ECONNREFUSED: Server actively rejected connection (wrong port, service down)
- ETIMEDOUT: No response received (firewall, routing failure, host down)
- EHOSTUNREACH: No route to host (routing problem)
- ENETUNREACH: Network unreachable (network interface down)
require 'socket'
def connect_with_diagnostics(host, port)
TCPSocket.new(host, port)
rescue Errno::ECONNREFUSED
puts "Connection refused - server not listening on port #{port}"
puts "Check if service is running: lsof -i :#{port}"
rescue Errno::ETIMEDOUT
puts "Connection timeout - no response from #{host}:#{port}"
puts "Check firewall rules and network connectivity"
rescue Errno::EHOSTUNREACH
puts "Host unreachable - no route to #{host}"
puts "Check routing table: netstat -rn"
rescue Errno::ENETUNREACH
puts "Network unreachable - network interface may be down"
puts "Check network interfaces: ifconfig or ip addr"
rescue SocketError => e
puts "DNS resolution failed: #{e.message}"
puts "Check DNS configuration: nslookup #{host}"
rescue => e
puts "Unexpected error: #{e.class} - #{e.message}"
end
Race Conditions in Non-Blocking Connections
Non-blocking connection attempts require careful state checking. Testing for connection completion without proper synchronization causes race conditions:
# Problematic: races between connect_nonblock and IO.select
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
addr = Socket.sockaddr_in(80, 'example.com')
begin
socket.connect_nonblock(addr)
rescue Errno::EINPROGRESS
# Race: socket might complete between exception and select
IO.select(nil, [socket], nil, 5)
end
# Better: check socket state after select
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
addr = Socket.sockaddr_in(80, 'example.com')
begin
socket.connect_nonblock(addr)
rescue Errno::EINPROGRESS
_, writable, _ = IO.select(nil, [socket], nil, 5)
if writable
begin
socket.connect_nonblock(addr)
rescue Errno::EISCONN
# Connection succeeded
rescue => e
puts "Connection failed: #{e.message}"
socket.close
end
end
end
Misunderstanding Connection Establishment vs. Authentication
A successful handshake establishes transport-layer connectivity but doesn't validate application-layer credentials. SSL/TLS handshakes occur after TCP handshake completion:
require 'socket'
require 'openssl'
# TCP handshake completes here
socket = TCPSocket.new('example.com', 443)
# SSL/TLS handshake begins after TCP establishment
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
begin
# SSL handshake may fail even with successful TCP connection
ssl_socket.connect
puts "SSL handshake complete"
rescue OpenSSL::SSL::SSLError => e
puts "SSL handshake failed: #{e.message}"
ensure
ssl_socket.close if ssl_socket
end
Applications must distinguish between transport failures (TCP handshake) and authentication failures (SSL/TLS or application protocol).
Tools & Ecosystem
Several Ruby gems and system tools facilitate working with TCP connections and analyzing handshake behavior.
TCPSocket and Socket Libraries
Ruby's standard library provides TCPSocket for high-level connection management and Socket for low-level control. TCPSocket abstracts handshake details:
require 'socket'
# High-level: TCPSocket handles all handshake details
tcp = TCPSocket.new('example.com', 80)
# Low-level: Socket provides explicit control
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
addr = Socket.sockaddr_in(80, 'example.com')
sock.connect(addr)
Socket enables advanced options like non-blocking mode and fine-grained timeout control unavailable in TCPSocket.
EventMachine and Async Networking
EventMachine provides an event-driven networking library built on the Reactor pattern, managing multiple connections concurrently:
require 'eventmachine'
module EchoClient
def connection_completed
puts "TCP handshake completed"
send_data("Hello server\n")
end
def receive_data(data)
puts "Received: #{data}"
close_connection
end
def unbind
puts "Connection closed"
EventMachine.stop
end
end
EventMachine.run do
EventMachine.connect('example.com', 80, EchoClient)
end
EventMachine triggers the connection_completed callback after handshake completion, separating connection establishment from data handling.
Wireshark and Packet Analysis
Wireshark captures and displays TCP packets, making handshake sequences visible. Filtering by tcp.flags.syn shows SYN packets, tcp.flags.syn and tcp.flags.ack shows SYN-ACK packets:
# Wireshark display filter for handshake packets
tcp.flags.syn == 1
# tcpdump command-line equivalent
tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' -nn
Analyzing packet captures reveals handshake timing, retransmissions, and connection failures. Time deltas between SYN, SYN-ACK, and ACK indicate network latency.
netstat and ss Commands
The netstat and ss commands display socket states, including connections in SYN-SENT and SYN-RECEIVED states:
# Show all TCP sockets with numeric addresses
netstat -tan
# Show sockets in SYN-SENT state
ss -tan state syn-sent
# Show listening sockets with process information
ss -tlnp
# Count connections by state
netstat -tan | awk '{print $6}' | sort | uniq -c
Applications with high SYN-SENT counts may indicate connection problems. SYN-RECEIVED states accumulate during SYN flood attacks.
Net::Ping and Connection Testing
The net-ping gem provides connection testing functionality:
require 'net/ping'
# Test TCP connectivity by completing handshake
check = Net::Ping::TCP.new('example.com', 80, 5)
if check.ping?
puts "Host is reachable, handshake completed"
puts "Duration: #{check.duration}s"
else
puts "Host unreachable: #{check.exception}"
end
Net::Ping attempts TCP handshakes without sending application data, measuring connectivity and latency.
Middleware and Reverse Proxies
Nginx, HAProxy, and other reverse proxies handle handshakes on behalf of Ruby applications. Connection pooling between proxy and application servers amortizes handshake costs:
# Unicorn configuration for socket reuse
worker_processes 4
listen 8080, backlog: 512, tcp_nopush: true, tcp_nodelay: true
before_fork do |server, worker|
# Close database connections before forking
defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
end
after_fork do |server, worker|
# Reestablish database connections after fork
defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end
Proxies maintain persistent connections to upstream servers, converting many client handshakes into fewer backend handshakes.
Connection Pool Implementations
The connection_pool gem manages connection reuse, reducing handshake overhead:
require 'connection_pool'
require 'socket'
pool = ConnectionPool.new(size: 5, timeout: 5) do
TCPSocket.new('example.com', 80)
end
# Checkout connection from pool (reuses existing connection)
pool.with do |socket|
socket.puts "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
response = socket.gets
puts response
end
Connection pools maintain established connections, eliminating handshake latency for subsequent requests. Pool exhaustion causes new handshakes when no idle connections exist.
Reference
TCP Handshake Packet Structure
| Packet | Flags | Sequence Number | Acknowledgment Number | Purpose |
|---|---|---|---|---|
| SYN | SYN=1, ACK=0 | Client ISN | 0 | Request connection |
| SYN-ACK | SYN=1, ACK=1 | Server ISN | Client ISN + 1 | Accept connection |
| ACK | SYN=0, ACK=1 | Client ISN + 1 | Server ISN + 1 | Confirm connection |
Connection State Transitions
| Current State | Event | Next State |
|---|---|---|
| CLOSED | Application opens connection | SYN-SENT |
| LISTEN | Receive SYN | SYN-RECEIVED |
| SYN-SENT | Receive SYN-ACK | ESTABLISHED |
| SYN-RECEIVED | Receive ACK | ESTABLISHED |
Common Error Codes
| Error Code | Meaning | Typical Cause |
|---|---|---|
| ECONNREFUSED | Connection refused | Server not listening on port |
| ETIMEDOUT | Connection timeout | No response from server |
| EHOSTUNREACH | Host unreachable | Routing failure |
| ENETUNREACH | Network unreachable | Network interface down |
| EINPROGRESS | Operation in progress | Non-blocking connect started |
| EISCONN | Already connected | Connect called on connected socket |
Ruby Socket Methods for Handshake Control
| Method | Description | Use Case |
|---|---|---|
| TCPSocket.new | Create connected socket | Simple client connections |
| Socket.connect | Initiate connection | Low-level control |
| Socket.connect_nonblock | Non-blocking connect | Async connection establishment |
| TCPServer.accept | Accept incoming connection | Server implementation |
| IO.select | Wait for socket readiness | Monitor handshake completion |
| Socket.setsockopt | Configure socket options | Tune handshake behavior |
TCP Options Negotiated During Handshake
| Option | Purpose | Impact |
|---|---|---|
| MSS | Maximum segment size | Determines packet fragmentation |
| Window Scale | Extend window size range | Improves high-bandwidth performance |
| SACK | Selective acknowledgment | Improves loss recovery |
| Timestamp | RTT measurement | Enables performance optimization |
| TCP Fast Open | Data in SYN packet | Reduces latency for repeat connections |
Handshake Timing Metrics
| Metric | Typical Value | Significance |
|---|---|---|
| Local handshake | <1ms | Minimal latency overhead |
| Regional handshake | 10-50ms | One regional RTT |
| Cross-continent | 100-300ms | Intercontinental latency |
| Satellite link | 500-700ms | Geostationary orbit delay |
| Retransmission timeout | 1-3s initial | Connection failure detection |
Socket Options Affecting Handshake
| Option | Level | Description |
|---|---|---|
| SO_REUSEADDR | SOL_SOCKET | Allow immediate port reuse |
| SO_KEEPALIVE | SOL_SOCKET | Enable connection keepalive |
| TCP_NODELAY | IPPROTO_TCP | Disable Nagle's algorithm |
| TCP_DEFER_ACCEPT | IPPROTO_TCP | Delay accept until data arrives |
| SO_SNDBUF | SOL_SOCKET | Set send buffer size |
| SO_RCVBUF | SOL_SOCKET | Set receive buffer size |