Skip to content

Instantly share code, notes, and snippets.

@dwayne
Last active August 9, 2020 07:57
Show Gist options
  • Save dwayne/6237511 to your computer and use it in GitHub Desktop.
Save dwayne/6237511 to your computer and use it in GitHub Desktop.
My notes from the book "Working with TCP Sockets by Jesse Storimer"

Chapter 0

Network programming is ultimately about sharing and communication.

Audience: Ruby devs on Unix or Unix-like systems.

Uses Ruby 1.9.

Part 1 - Introduction to the primitives of Socket programming

  • Create sockets
  • Connect sockets together
  • Share data

Part 2 - Advanced topics in Socket programming

Part 3 - Applications of the first two parts to a "real world" scenario

  • Apply concurrency to your network programs
  • Learn about various architectural patterns

Focuses on Berkeley Sockets API

  • Appeared with version 4.2 of the BSD operating system in 1983
  • First implementation of TCP
  • Implemented in C

Benefit: You can use sockets without having to know the details of the underlying protocol.

netcat

nc - A Unix utility for creating arbitrary TCP (and UDP) connections and listens. It's a useful tool to have in your toolbox when working with sockets. See man 1 nc for more details.

Chapter 1 - Your First Socket

Everything you need can be imported with require 'socket'.

See the Index of Classes & Methods in socket.

# creating a socket

require 'socket'

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

Socket::AF_INET implies a socket in the IPv4 family of protocols. The Socket::SOCK_STREAM part says you'll be communicating using a stream which is provided by TCP. For a UDP socket you would use Socket::SOCK_DGRAM.

Socket::AF_INET is equivalent to :INET. Socket::SOCK_STREAM is equivalent to :STREAM.

A host is identified by a unique IP address. Think of an IP address as the host's phone number.

People don't remember IP addresses so they use words. DNS brings the two together. See How DNS Works.

The loopback interface:

  • a virtual interface, it's not attached to any hardware
  • any data sent to it is immediately received by it
  • it's host name is localhost
  • it's IP address is 127.0.0.1

The 'hosts' file: http://en.wikipedia.org/wiki/Hosts_(file).

TODO: Research IPv6.

If you want to have a conversation with someone in an office building you'll have to call their phone number, then dial their extension. The port number is the 'extension' of a socket endpoint.

N.B.: The combination of IP address and port number must be unique for each socket.

Docs

  1. manpages
  2. ri

TODO: Find out how to install the Ruby docs locally.

Chapter 2 - Establishing Connections

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

  1. initiator
  2. listener

Both roles are required. Without a listener socket no connection can be initiated. Similarly, without an initiator there's no need for a listener.

In network programming:

  • a socket that listens is a server
  • a socket that initiates a connection is a client

Chapter 3 - Server Lifecycle

The typical lifecycle of a server socket looks like:

  1. create
  2. bind
  3. listen
  4. accept
  5. close

Servers Bind

Following is a low-level implementation showing how to bind a TCP socket to a local port.

require 'socket'

# First, create a new TCP 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')  

# Bind to it.
socket.bind(addr)

A server binds to a specific, agreed-upon port number which a client socket can then connect to.

N.B.: Ruby provides syntactic sugar so that you never have to actually use Socket.pack_sockaddr_in or Socket#bind directly.

What port should I bind to?

  • a port number can range from 1-65,535
  • port numbers in the range 0-1024 are 'well-known' ports and are reserved for system use. For example HTTP traffic defaults to port 80 and SMTP traffic defaults to port 25
  • port numbers in the range 49,000-65,535 are ephemeral ports. They are used by services that don't operate on a predefined port number but need ports for temporary purposes. They are also an integral part of the connection negotiation process.
  • port numbers in the range 1025-48999 is fair game for your uses. If you are planning on claiming one of those ports as the port for your server then you should have a look at the IANA list of registered ports and make sure that your choice does not conflict with some other popular service out there.

What address should I bind to?

When you bind to a specific interface, represented by its IP address, your socket is only listening on that interface. It will ignore the others.

If you bind to 127.0.0.1 then your socket will only be listening on the loopback interface. In this case, only connections made to localhost or 127.0.0.1 will be routed to your server socket. Since this interface is only available locally, no external connections will be allowed.

If you want to listen on all interfaces then you can use 0.0.0.0. This will bind to any available interface, loopback or otherwise.

Servers Listen

After creating a socket, and binding to a port, the socket needs to be told to listen for incoming connections.

require 'socket'

# Create a socket and bind it to port 4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')  
socket.bind(addr)

# Tell it to listen for incoming connections
socket.listen(5)

The number passed to the listen method represents the maximum number of pending connections your server socket is willing to tolerate. This list of pending connections is called the listen queue.

If your server is busy processing a client connection, then when any new client connections arrive they'll be put into the listen queue. If a new client connection arrives and the listen queue is full then the client will raise Errno::ECONNREFUSED.

How big should the listen queue be?

  • Why wouldn't we want to set it to 10,000?
  • Why would we ever want to refuse a connection?

First, we need to understand the limits. You can get the current maximum allowed listen queue size by inspecting Socket::SOMAXCONN at runtime. On my machine it is 128. So I'm not able to use a number larger than that. The root user is able to increase this limit at the system level for servers that need it.

Let's say you're running a server and you're getting reports of Errno::ECONNREFUSED. Increasing the size of the listen queue would be a good starting point. But ultimately you don't want to have connections waiting in your listen queue. That means that users of your service are having to wait for their responses. This may be an indication that you need more server instances or that you need a different architecture.

Generally you don't want to be refusing connections. You can set the listen queue to the maximum allowed queue size using server.listen(Socket::SOMAXCONN).

Server Accept

A server handles an incoming connection by calling the accept method. Here's how to create a listening socket and receive the first connection:

require 'socket'

# Create the server socket.
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')  
socket.bind(addr)
socket.listen(128)

# Accept a connection.
connection, _ = server.accept

The accept call is a blocking call. It will block the current thread indefinitely until it receives a new connection.

Remember the listen queue? accept simply pops the next pending connection off of that queue. If none are available it waits for one to be pushed onto it.

The accept method returns an Array. The Array contains two elements:

  1. the connection, and
  2. an Addrinfo object. This represents the remote address of the client connection.

See Addrinfo.

Although accept returns a 'connection', a connection is actually an instance of Socket. Each connection is represented by a new Socket object so that the server socket can remain untouched and continue to accept new connections. The connection object knows about two addresses: the local address and the remote address. The remote address is the second return value returned from accept but can also be accessed as remote_address on the connection.

  • The local_address of the connection refers to the endpoint on the local machine.
  • The remote_address of the connection refers to the endpoint at the other end, which might be on another host or the same machine.

N.B.: The combination of local-host, local-port, remote-host and remote-port must be unique for each TCP connection.

N.B.: Sockets are files. In UNIX everything is a file.

The Accept Loop

accept returns one connection. When writing a production server we usually want to continually listen for incoming connections so long there are more available. This can be easily accomplished with a loop:

require 'socket'

# Create the server socket.
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')  
server.bind(addr)
server.listen(128)

# Enter an endless loop of accepting and
# handling connections.
loop do
  connection, _ = server.accept
  # handle connection
  connection.close
end

Servers Close

Once a server has accepted a connection and finished processing it, the last thing for it to do is to close that connection as it rounds out the create-process-close lifecycle of a connection.

Closing on Exit

TODO: Take notes.

@slothC0der
Copy link

Where did you buy the book from?

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