Sockets
Sockets are used to communicate with other programs over the internet. Inko provides the following modules for using sockets:
Module | Provides |
---|---|
std::net::socket |
TCP and UDP sockets |
std::net::unix |
UNIX sockets |
std::net::ip |
Types for 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 allow you to get the
low-level Socket
type they wrap, which can be used for setting options such as
the TCP keep-alive time.
TCP clients and servers
We'll 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 as our TCP server. The TcpStream
is our
TCP client, connecting to the TcpListener
. We 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 TcpListener
listening on address 127.0.0.1
, port 40 000
.
The stream connects to the same address. We're using try!
here so that any
errors will result in a panic, stopping 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 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.
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 sacrificing performance.
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
is 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 create IPv4 and IPv6 addresses yourself:
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 them:
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 process, then sends those sockets to a separate processes. This allows us to write a simple HTTP server that uses separate processes for accepting requests and writing a response:
import std::loop::loop
import std::net::socket::(TcpListener, TcpStream)
import std::process
let listener = try! TcpListener.new(ip: '127.0.0.1', port: 8080)
loop {
let client = try! listener.accept
let proc = process.spawn {
let client = process.receive as TcpStream
let reply = 'Hello, HTTP!'
let output = `HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: {reply.bytesize}\r
Connection: close\r
\r
{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
}
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::loop::(loop, while)
import std::net::socket::(TcpListener, TcpStream)
import std::process
let listener = try! TcpListener.new(ip: '127.0.0.1', port: 8080)
let mut to_start = 4
while({ to_start.positive? }) {
let proc = process.spawn {
let listener = process.receive as TcpListener
let reply = 'Hello, HTTP!'
loop {
let client = try! listener.accept
let output = `HTTP/1.1 200 OK\r
Content-Type: text/plain\r
Content-Length: {reply.bytesize}\r
Connection: close\r
\r
{reply}`
try! client.write_string(output)
try! client.shutdown
client.close
}
}
proc.send(listener)
to_start -= 1
}
# This prevents the program from terminating right away, instead requiring the
# user to manually terminate it.
process.receive