Sockets

Table of contents

  1. Introduction
  2. TCP clients and servers
  3. Unix socket clients and servers
  4. Handling blocking operations
  5. Parsing IP addresses
  6. Sending sockets across processes

Introduction

Sockets allow one to communicate with other programs, either on the same machine or across the internet. Inko provides support for using sockets with the following modules:

  • std::net::socket
  • std::net::unix
  • std::net::ip

The module std::net::socket provides access to TCP and UDP sockets, while std::net::unix provides access to Unix domain sockets. The module std::net::ip can be used to parse and generate IPv4 and IPv6 addresses.

The modules std::net::socket and std::net::unix share a similar layout: they both offer a low-level Socket type, and various high-level types such as UdpSocket and UnixDatagram. These high-level types in turn allow you to get the low-level Socket type they wrap, which can be used for setting options such as the TCP keepalive time.

TCP clients and servers

We will start with a simple example: a TCP server that accepts incoming connections and writes a response, and a TCP client that connects to this server and sends a message. We will start by importing the necessary types:

import std::net::socket::(TcpListener, TcpStream)

The TcpListener type will be used for our TCP server by accepting incoming connections. The TcpStream type will be our TCP client, connecting to the TcpListener. We will do so as follows:

import std::net::socket::(TcpListener, TcpStream)

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 40_000)
let stream = try! TcpStream.new(ip: '127.0.0.1', port: 40_000)

Here we create a listener listening on address 127.0.0.1, port 40 000. The stream in turn will connect to the same address. We're using try! here so that any errors will result in a panic, terminating the program.

With the listener and stream in place, let's write some data to the stream:

import std::net::socket::(TcpListener, TcpStream)

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 40_000)
let stream = try! TcpStream.new(ip: '127.0.0.1', port: 40_000)

try! stream.write_string('ping')

Here we write the string 'ping' to the stream, using try! to panic if an error were to occur.

To accept a new connection, send accept to a TcpListener:

import std::net::socket::(TcpListener, TcpStream)

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 40_000)
let stream = try! TcpStream.new(ip: '127.0.0.1', port: 40_000)

try! stream.write_string('ping')

let connection = try! listener.accept

The method TcpListener.accept returns a TcpStream that can be read from and written to. With the connection in place, we can read the message that was sent earlier:

import std::net::socket::(TcpListener, TcpStream)
import std::stdio::stdout

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 40_000)
let stream = try! TcpStream.new(ip: '127.0.0.1', port: 40_000)

try! stream.write_string('ping')

let connection = try! listener.accept
let message = try! connection.read_string(4)

stdout.print(message)

Here we use TcpListener.read_string to read the message into a String. We could also use TcpListener.read_bytes if we wanted to read the data into an existing ByteArray.

Running the code we have written so far will result in "ping" being written to STDOUT. This is done using the method print from the module std::stdio::stdout.

Unix socket clients and servers

Unix domain sockets are provided by the module std::net::unix and provide an interface similar as std::net::socket. The TCP example shown above would look as follows when using Unix domain sockets:

import std::net::unix::(UnixListener, UnixStream)
import std::stdio::stdout

let listener = try! UnixListener.new('/tmp/test.sock')
let stream = try! UnixStream.new('/tmp/test.sock')

try! stream.write_string('ping')

let connection = try! listener.accept
let message = try! connection.read_string(4)

stdout.print(message)

Keep in mind that closing a UnixListener does not automatically remove the socket file, so you have to do so manually if you want to run the above code more than once.

Handling blocking operations

The socket APIs provided by Inko are built on top of non-blocking sockets, but without the need for using callbacks or promises. This allows you to write code in a linear and easy to understand way, without having to worry about it not being able to scale to tens of thousands of connections.

This means you don't have to (and should not) use std::process.blocking when using the socket APIs provided by Inko.

Parsing IP addresses

The module std::net::ip can be used to generate and parse IPv4 and IPv6 addresses. For example, we can parse an IP address as follows:

import std::net::ip

let address = try! ip.parse('1.2.3.4')

This would produce an instance of the Ipv4Address and store it in the address local variable. You can also convert a String to an IP address by importing std::net::ip and sending to_ip_address to a String:

import std::net::ip

let address = try! '1.2.3.4'.to_ip_address

You can also manually create IPv4 and IPv6 addresses:

import std::net::ip::(Ipv4Address, Ipv6Address)

# For the IPv4 address '127.0.0.1':
Ipv4Address.new(127, 0, 0, 1)

# For the IPv6 address '::1':
Ipv6Address.new(0, 0, 0, 0, 0, 0, 0, 1)

Both these types implement the IpAddress trait. These types can also be converted back to a String by sending to_string to an IpAddress type:

import std::net::ip::(Ipv4Address, Ipv6Address)

Ipv4Address.new(127, 0, 0, 1).to_string           # => '127.0.0.1'
Ipv6Address.new(0, 0, 0, 0, 0, 0, 0, 1).to_string # => '::1'

Sending sockets across processes

Sockets can be sent from one process to another. This allows you to write code that accepts incoming connections in one or more processes, then uses those sockets in separate processes. This allows us to write a simple HTTP server that uses separate processes to send a response back to a client:

import std::net::socket::(TcpListener, TcpStream)
import std::process
import std::string_buffer::StringBuffer

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 8080)

{
  let client = try! listener.accept
  let proc = process.spawn {
    let client = process.receive as TcpStream
    let reply = 'Hello, HTTP!'
    let output = StringBuffer.new(
      "HTTP/1.1 200 OK\r\n",
      "Content-Type: text/plain\r\n",
      'Content-Length: ',
      reply.bytesize.to_string,
      "\r\n",
      "Connection: close\r\n",
      "\r\n",
      reply
    )

    try! client.write_string(output.to_string)
    try! client.shutdown

    # While the socket will be closed when it is garbage collected, this may
    # take a little while, so we close it right away.
    client.close
  }

  proc.send(client)

  # Since the socket is copied, we need to close it here so we don't run out of
  # file descriptors.
  client.close
}.loop

You can then send requests to it using curl like so:

curl http://127.0.0.1:8080

We can also send the TcpListener to different processes, allowing different processes to accept incoming connections in parallel:

import std::net::socket::(TcpListener, TcpStream)
import std::process
import std::string_buffer::StringBuffer

let listener = try! TcpListener.new(ip: '127.0.0.1', port: 8080)
let mut to_start = 4

{ to_start.positive? }.while_true {
  let proc = process.spawn {
    let listener = process.receive as TcpListener
    let reply = 'Hello, HTTP!'

    {
      let client = try! listener.accept
      let output = StringBuffer.new(
        "HTTP/1.1 200 OK\r\n",
        "Content-Type: text/plain\r\n",
        'Content-Length: ',
        reply.bytesize.to_string,
        "\r\n",
        "Connection: close\r\n",
        "\r\n",
        reply
      )

      try! client.write_string(output.to_string)
      try! client.shutdown

      client.close
    }.loop
  }

  proc.send(listener)

  to_start -= 1
}

# This prevents the program from terminating right away, instead requiring the
# user to manually terminate it.
process.receive