Hello, sockets!

In the previous tutorial we looked at reading from and writing to files. In this tutorial we'll instead look at reading from and writing to network sockets.

To start things off, create a file called socket.inko with the following contents:

import std.net.ip.IpAddress
import std.net.socket.UdpSocket
import std.stdio.STDOUT

class async Main {
  fn async main {
    let stdout = STDOUT.new
    let server = UdpSocket.new(IpAddress.v4(0, 0, 0, 0), port: 0).unwrap
    let client = UdpSocket.new(IpAddress.v4(0, 0, 0, 0), port: 0).unwrap
    let addr = server.local_address.unwrap

    client.connect(addr.ip.unwrap, addr.port).unwrap
    client.write_string('Hello, world!').unwrap

    let bytes = ByteArray.new

    server.read(into: bytes, size: 32).unwrap
    stdout.write_bytes(bytes).unwrap
  }
}

Now run it using inko run socket.inko, and the output should be:

Hello, world!

Explanation

Compared to previous tutorials there's quite a bit going on here, so let's take a look at what this code does.

First, we import two types not seen before: IpAddress and UdpSocket. The first is used to represent IPv4 and IPv6 addresses, the second is used for UDP sockets. We use UDP sockets in this example as it keeps things as simple as possible.

Our sockets are created as follows:

let server = UdpSocket.new(IpAddress.v4(0, 0, 0, 0), port: 0).unwrap
let client = UdpSocket.new(IpAddress.v4(0, 0, 0, 0), port: 0).unwrap

What happens here is that we create two sockets that bind themselves to IP address 0.0.0.0, using port 0. Using port 0 results in the operating system assigning the socket a random unused port number. This way we don't need to worry about using a port that's already in use.

Next, we encounter the following:

let addr = server.local_address.unwrap

client.connect(addr.ip.unwrap, addr.port).unwrap
client.write_string('Hello, world!').unwrap

Here we get the address of the server we need to connect the client to, which we do using connect(). We then write the string "Hello, world!" to the client, sending it to the server.

We then read the data back from the server:

let bytes = ByteArray.new

server.read(into: bytes, size: 32).unwrap
stdout.write_bytes(bytes).unwrap

When using sockets you shouldn't use read_all as we did in the files tutorial, because read_all won't return until the socket is disconnected.

Just as in the files tutorial, we use unwrap to handle errors for the sake of brevity.

Using TCP sockets

Let's change the program to use TCP sockets instead. We'll start by changing sockets.inko to the following:

import std.net.ip.IpAddress
import std.net.socket.TcpServer
import std.stdio.STDOUT

class async Main {
  fn async main {
    let stdout = STDOUT.new
    let server = TcpServer.new(IpAddress.v4(0, 0, 0, 0), port: 9999).unwrap
    let client = server.accept.unwrap
    let bytes = ByteArray.new

    client.read(into: bytes, size: 32).unwrap
    stdout.write_bytes(bytes).unwrap
  }
}

This time we're using a fixed port number (9999) as that makes this particular example a little easier.

Next, we'll create another file called client.inko with the following contents:

import std.net.ip.IpAddress
import std.net.socket.TcpClient

class async Main {
  fn async main {
    let client = TcpClient.new(IpAddress.v4(0, 0, 0, 0), port: 9999).unwrap

    client.write_string('Hello, world!').unwrap
  }
}

To run these programs, run inko run server.inko first in one terminal window, then open a separate terminal window and run inko run client.inko in this new window. If all went well, the server.inko program writes "Hello, world!" to the terminal, then terminates.

What we did here is create a simple TCP server using the aptly named TcpServer type, connected to address 0.0.0.0 and port 9999, then connected a client to it using the similarly aptly named TcpClient type.