Landin library reference discuss source

core/net

Shared: available on every enabled target.

The network concept, whose connections are readers and writers, and internet addresses with their text.

The network is one more concept outside world, network, so an image that never connects carries none of it. It is a reader and a writer of its own sockets as well, so a connection is read and written through read and write, a line reader and a buffered writer among them, whose failures are io_failed; send and receive say more, timed_out when a limit has passed and disconnected when the peer has gone, and a receive or read of zero is the end of what the peer sends. listen takes an address and returns a listener, port 0 for a free port that local_address then gives, and may take an address its own closed connections still hold, never one another socket listens at, which is address_in_use; accept takes the next connection; connect makes one, refused where nothing listens and unreachable where no route leads; open_datagram binds a UDP socket, send_to sends a datagram whole and receive_from gives the next with its sender, cutting one longer than the buffer; shutdown stops receiving, sending or both_directions; peer_address names a connection's other end; set_timeouts limits how long a receive, an accept and a send wait, zero for no limit; set_no_delay turns Nagle's algorithm off; and close_socket closes a socket. An address the system does not have, or a family it lacks, is address_unavailable. Every call waits as a blocking socket does, so one thread can connect to its own listener and send before it accepts, since the system completes the handshake, but a receive waits for its peer. An address is a family, ipv4 or ipv6, sixteen bytes and a port: loopback, unspecified, ipv4_address, ipv6_address and with_port make one, same_address and is_unspecified compare, parse_ip reads 192.0.2.1 or 2001:db8::1 and parse_address 192.0.2.1:80 or [2001:db8::1]:80, refusing a leading zero, a zone and anything else with invalid_address, and append_ip and append_address write them back as RFC 5952 does, lower case, the longest run of zero groups shortened and an IPv4-mapped address in dotted decimal; an address is text.formattable. Nothing here resolves a name. network_over makes a fake.network over the caller's network_socket records, each made by socket_over from a buffer that is its receive buffer, and a network_space whose counters a test reads: a machine of loopback addresses on which a connection and a datagram arrive at once, whole and in order, a datagram no socket takes is lost and counted, and a call that would wait answers timed_out at once, since nothing in one thread could ever end the wait. A test may take IPv6 away with without_ipv6 and fail a call with fail_at. runtime/network-echo-memory runs the published echo over it, and runtime/network-addresses-firmware reads and writes addresses on Cortex-M0.

Items

Executable example

This complete program is maintained in the repository runtime tests. View source.

--  Internet addresses read from their text and written back: IPv4's four
--  decimal parts, IPv6's groups as RFC 5952 writes them, shortened and in
--  lower case, an IPv4-mapped address in dotted decimal, and a port after
--  either, an IPv6 address then in brackets.  Text that is no address is
--  `invalid_address`, a zone among it.  It runs on every target, firmware
--  included, since an address is plain data.
import core/cmp
import core/net
import core/text

--  Text read without a port and what it is written back as, or "" when it
--  is no address.
case: type = struct
    given: []u8
    written: []u8
end case

ips: [30]case =
    [(given: "127.0.0.1", written: "127.0.0.1"),
     (given: "0.0.0.0", written: "0.0.0.0"),
     (given: "255.255.255.255", written: "255.255.255.255"),
     (given: "192.0.2.10", written: "192.0.2.10"),
     (given: "256.1.1.1", written: ""),
     (given: "01.2.3.4", written: ""),
     (given: "1.2.3", written: ""),
     (given: "1.2.3.4.5", written: ""),
     (given: "1..2.3", written: ""),
     (given: " 1.2.3.4", written: ""),
     (given: "::", written: "::"),
     (given: "::1", written: "::1"),
     (given: "1::", written: "1::"),
     (given: "2001:DB8:0:0:0:0:0:1", written: "2001:db8::1"),
     (given: "2001:0db8:0000:0000:0000:ff00:0042:8329",
      written: "2001:db8::ff00:42:8329"),
     (given: "2001:db8::1:0:0:1", written: "2001:db8::1:0:0:1"),
     (given: "2001:0:0:1:0:0:0:1", written: "2001:0:0:1::1"),
     (given: "1:2:3:4:5:6:7::", written: "1:2:3:4:5:6:7:0"),
     (given: "::ffff:192.0.2.1", written: "::ffff:192.0.2.1"),
     (given: "::ffff:c000:201", written: "::ffff:192.0.2.1"),
     (given: "64:ff9b::192.0.2.33", written: "64:ff9b::c000:221"),
     (given: "1:2:3:4:5:6:7:8", written: "1:2:3:4:5:6:7:8"),
     (given: "fe80::1%eth0", written: ""),
     (given: "1::2::3", written: ""),
     (given: ":1", written: ""),
     (given: "1:", written: ""),
     (given: "12345::", written: ""),
     (given: "1:2:3:4:5:6:7:8:9", written: ""),
     (given: "::g", written: ""),
     (given: "", written: "")]

--  Text read with a port and what it is written back as, or "".
addresses: [11]case =
    [(given: "127.0.0.1:80", written: "127.0.0.1:80"),
     (given: "0.0.0.0:0", written: "0.0.0.0:0"),
     (given: "[::1]:8080", written: "[::1]:8080"),
     (given: "[2001:DB8::1]:65535", written: "[2001:db8::1]:65535"),
     (given: "[::ffff:10.0.0.1]:443", written: "[::ffff:10.0.0.1]:443"),
     (given: "[::1]", written: ""),
     (given: "1.2.3.4:65536", written: ""),
     (given: "1.2.3.4:", written: ""),
     (given: "::1:80", written: ""),
     (given: "[fe80::1%eth0]:80", written: ""),
     (given: "1.2.3.4:x", written: "")]

--  Whether reading C's text, with or without a port, writes it back as it
--  says, or refuses it when it says "".
agrees: (item: case, port: bool) -> (yes: bool) ! text.no_space =
    mut room: [64]u8 = zeroed
    mut line := text.over(room[0..<64])
    wanted: []u8 = item.written
    yes = lenof wanted == 0
    at: net.address = read(item.given, port) else (problem)
        _ = problem
        return
    end
    yes = false
    return when lenof wanted == 0
    if port then
        try net.append_address(line, at)
    else
        try net.append_ip(line, at)
    end if
    yes = cmp.compare(text.used(line), wanted) == cmp.equal
    return when not yes
    --  What is written reads back as the same address.
    again: net.address = read(text.used(line), port) else (problem)
        _ = problem
        yes = false
        return
    end
    yes = net.same_address(at, again)
end agrees

read: (source: []u8, port: bool) -> (at: net.address) ! net.invalid_address =
    if port then
        at = try net.parse_address(source)
    else
        at = try net.parse_ip(source)
    end if
end read

--  The constructors, the comparisons and the builder's rule that a piece
--  is written whole or not at all.
made: () -> (yes: bool) ! text.no_space | text.invalid_format =
    groups: [8]u16 = [0x2001, 0xdb8, 0, 0, 0, 0, 0, 1]
    v6: net.address = net.ipv6_address(groups, 443)
    v4: net.address = net.ipv4_address(192, 0, 2, 1, 80)
    mut room: [64]u8 = zeroed
    mut line := text.over(room[0..<64])
    try text.format(line, "{} {}", [any(addr v4), any(addr v6)])
    yes = cmp.compare(text.used(line), "192.0.2.1:80 [2001:db8::1]:443")
          == cmp.equal
    yes = yes and net.same_address(net.loopback(net.ipv4, 7),
                                   net.ipv4_address(127, 0, 0, 1, 7))
    yes = yes and not net.same_address(net.loopback(net.ipv4, 7),
                                       net.loopback(net.ipv4, 8))
    yes = yes and not net.same_address(net.unspecified(net.ipv4, 0),
                                       net.unspecified(net.ipv6, 0))
    yes = yes and net.is_unspecified(net.unspecified(net.ipv6, 9))
          and not net.is_unspecified(net.loopback(net.ipv6, 9))
    moved: net.address = net.with_port(v4, 81)
    yes = yes and moved.port == 81 and moved.octets[3] == 1
    --  Twelve bytes have room for `[::1]` but not for its port.
    mut small: [12]u8 = zeroed
    mut short := text.over(small[0..<12])
    try text.append(short, "at ")
    mut full: bool = false
    net.append_address(short, v6) else (problem)
        full = problem == text.no_space
    end
    yes = yes and full and text.length(short) == 3
end made

public main: () -> (code: i32) =
    code = 1
    for index in usize(0)..<lenof ips do
        good: bool = agrees(ips[index], false) else (problem)
            _ = problem
            return
        end
        if not good then
            code = 10 + i32(index)
            return
        end if
    end for
    code = 2
    for index in usize(0)..<lenof addresses do
        good: bool = agrees(addresses[index], true) else (problem)
            _ = problem
            return
        end
        if not good then
            code = 50 + i32(index)
            return
        end if
    end for
    code = 3
    built: bool = made() else (problem)
        _ = problem
        return
    end
    return when not built
    code = 42
end main

network concept

public network: type = concept (provider: type) is io.reader, io.writer
    --- Connect to a listener at `to` and return the connection.  A port
    --- nothing listens on is `refused`.
    connect: (self: provider, to: address) -> (connected: io.file)
             ! refused | unreachable | address_unavailable | timed_out
             | io.io_failed
    --- Listen for connections at `at`, port 0 for a free one, which
    --- `local_address` then gives.  A listener may take an address its
    --- earlier connections still hold while they close, never one another
    --- socket listens at, `address_in_use`.
    listen: (self: provider, at: address) -> (listener: io.file)
            ! address_in_use | address_unavailable | io.no_access | io.io_failed
    --- Take the next connection made to a listener, waiting for one.
    accept: (self: provider, listener: io.file) -> (accepted: io.file)
            ! timed_out | io.io_failed
    --- Open a UDP socket bound to `at`, port 0 for a free one.
    open_datagram: (self: provider, at: address) -> (socket: io.file)
                   ! address_in_use | address_unavailable | io.no_access
                   | io.io_failed
    --- Send all of `bytes` over a connection, waiting while its buffers
    --- are full.  A failure may follow a part of them.
    send: (self: provider, connection: io.file, bytes: []u8) -> none
          ! disconnected | timed_out | io.io_failed
    --- Receive what arrives next on a connection into `into`, waiting for
    --- something: its count, zero once the peer has finished sending.
    receive: (self: provider, connection: io.file, into: []mut u8)
             -> (count: usize) ! disconnected | timed_out | io.io_failed
    --- Send `bytes` as one datagram to `to`.  One that cannot be sent
    --- whole, longer than the system takes, is `io_failed`.
    send_to: (self: provider, socket: io.file, bytes: []u8, to: address)
             -> none ! unreachable | timed_out | io.io_failed
    --- Receive the next datagram into `into`, waiting for one, and say
    --- who sent it.  A datagram longer than `into` is cut to it, the rest
    --- thrown away.
    receive_from: (self: provider, socket: io.file, into: []mut u8)
                  -> (received: datagram) ! timed_out | io.io_failed
    --- Stop receiving, sending or both on a connection.
    shutdown: (self: provider, connection: io.file, stopping: direction)
              -> none ! io.io_failed
    --- The address and port a socket is bound to.
    local_address: (self: provider, socket: io.file) -> (at: address)
                   ! io.io_failed
    --- The address and port of a connection's peer.
    peer_address: (self: provider, connection: io.file) -> (at: address)
                  ! io.io_failed
    --- Limit how long a receive or an accept, and a send, waits on
    --- `socket`; a span of zero is no limit.
    set_timeouts: (self: provider, socket: io.file, receive_limit: time.duration,
                   send_limit: time.duration) -> none ! io.io_failed
    --- Send a connection's small writes at once, `on`, rather than gather
    --- them while an earlier one is unacknowledged.
    set_no_delay: (self: provider, connection: io.file, on: bool) -> none
                  ! io.io_failed
    --- Close a socket, which is consumed even when this fails.
    close_socket: (self: provider, sink socket: io.file) -> none ! io.io_failed
end network

core/net/net.ldn:94

The network: TCP connections and UDP datagram sockets over IPv4 and IPv6, each a file of the provider. It is a reader and a writer of them as well, so a connection is read and written through io.read and io.write, a line reader and a buffered writer among them, whose failures are io_failed with the provider's detail; send and receive say more, timed_out and disconnected. A connection's read and receive return zero once its peer has finished sending.

Every call waits until it can do what it was asked, as a blocking socket does: accept for a connection, receive for bytes, send for room, unless set_timeouts gave the socket a limit. A listener takes connections whose handshake the system completes on its own, so one thread can connect to its own listener and then accept, and a connection's sends wait only once the system's buffers are full. Close every socket with close_socket; one dropped unclosed is open until the process ends, since there are no destructors.

address type

public address: type = struct
    family: address_family
    octets: [16]u8
    port: u16
end address

core/net/net.ldn:54

An internet address and a port: TCP's and UDP's endpoint. family says which; an IPv4 address is octets' first four bytes, in the order its text writes them, and the other twelve are zero; an IPv6 address is all sixteen, in network order. port is a number from 0 to 65535, and 0 asks the system for a free one when a socket is bound. There is no zone: an IPv6 link-local address that needs one, fe80::1%eth0, cannot be written. Make one with ipv4_address, ipv6_address, loopback, unspecified or parse_address, and compare two with same_address.

address_family type

public address_family: type = ipv4 | ipv6

core/net/net.ldn:43

What an address is: ipv4 or ipv6.

datagram type

public datagram: type = struct
    count: usize
    sender: address
end datagram

core/net/net.ldn:62

What arrived in one receive_from: how many bytes, and the address and port of the socket that sent them.

direction type

public direction: type = receiving | sending | both_directions

core/net/net.ldn:76

Which way of a connection shutdown stops: receiving, sending or both_directions.

address_in_use atom

public address_in_use: atom

core/net/net.ldn:24

Another socket already holds the address and port: a listener or a datagram socket bound there.

address_unavailable atom

public address_unavailable: atom

core/net/net.ldn:28

The address cannot be used here: no interface of this system holds it, or the system does not have its family at all, as one without IPv6 does not.

both_directions atom

public both_directions: atom

core/net/net.ldn:73

Stop both receiving and sending.

disconnected atom

public disconnected: atom

core/net/net.ldn:36

The connection's peer has gone: it closed or reset the connection, so what is sent cannot be delivered. A write to such a connection is this atom on a hosted system, never the end of the process.

invalid_address atom

public invalid_address: atom

core/net/net.ldn:15

The text of an address could not be read as one: not four decimal parts from 0 to 255, not an IPv6 address's groups, a zone, or a port that is missing, above 65535 or not decimal.

ipv4 atom

public ipv4: atom

core/net/net.ldn:39

An IPv4 address: four bytes, 127.0.0.1 in its text.

ipv6 atom

public ipv6: atom

core/net/net.ldn:41

An IPv6 address: sixteen bytes, ::1 in its text.

receiving atom

public receiving: atom

core/net/net.ldn:68

Stop receiving: what arrives later is thrown away.

refused atom

public refused: atom

core/net/net.ldn:18

Nothing listens at the address a connection was asked for: the system answered it at once, as a closed port is answered.

sending atom

public sending: atom

core/net/net.ldn:71

Stop sending: the peer reads the end of the stream once what was sent before has arrived, while this side may still receive.

timed_out atom

public timed_out: atom

core/net/net.ldn:32

The time limit set_timeouts gave passed before anything arrived or could be sent, or a connection's handshake took longer than the system waits. The socket is still usable.

unreachable atom

public unreachable: atom

core/net/net.ldn:21

No route leads to the address: its network or its host cannot be reached from here.

accept function

public accept: (provider: type is network, host: provider, listener: io.file)
               -> (accepted: io.file) ! timed_out | io.io_failed

core/net/net.ldn:496

Take the next connection made to listener, waiting for one unless a receive limit was set.

append_address function

public append_address: (inout into: text.builder, at: address)
                       -> none ! text.no_space

core/net/net.ldn:444

Append an address's text with its port: 127.0.0.1:8080, or an IPv6 address in brackets, [::1]:8080. parse_address reads it back. It is written whole or not at all.

append_ip function

public append_ip: (inout into: text.builder, at: address)
                  -> none ! text.no_space

core/net/net.ldn:432

Append an address's text without its port: 127.0.0.1, or an IPv6 address as RFC 5952 writes it, ::1 and 2001:db8::1, lower case with the longest run of zero groups shortened and an IPv4-mapped address as ::ffff:192.0.2.1. parse_ip reads it back. It is written whole or not at all.

close_socket function

public close_socket: (provider: type is network, host: provider,
                      sink socket: io.file) -> none ! io.io_failed

core/net/net.ldn:575

Close a socket. Treat it as consumed even if closing reports io_failed.

connect function

public connect: (provider: type is network, host: provider, to: address)
                -> (connected: io.file)
                ! refused | unreachable | address_unavailable | timed_out
                | io.io_failed

core/net/net.ldn:478

Connect to a listener at to through network evidence. Close the connection with close_socket.

ipv4_address function

public ipv4_address: (a: u8, b: u8, c: u8, d: u8, port: u16)
                     -> (at: address)

core/net/net.ldn:152

An IPv4 address a.b.c.d and a port.

ipv6_address function

public ipv6_address: (groups: [8]u16, port: u16) -> (at: address)

core/net/net.ldn:163

An IPv6 address of eight 16-bit groups, in the order its text writes them, and a port.

is_unspecified function

public is_unspecified: (at: address) -> (yes: bool)

core/net/net.ldn:207

Whether an address is its family's unspecified address, whatever its port.

listen function

public listen: (provider: type is network, host: provider, at: address)
               -> (listener: io.file)
               ! address_in_use | address_unavailable | io.no_access
               | io.io_failed

core/net/net.ldn:487

Listen for connections at at, port 0 for a free one; close the listener with close_socket.

local_address function

public local_address: (provider: type is network, host: provider,
                       socket: io.file) -> (at: address) ! io.io_failed

core/net/net.ldn:548

The address and port a socket is bound to: a listener's or a datagram socket's port when it was bound to port 0.

loopback function

public loopback: (family: address_family, port: u16) -> (at: address)

core/net/net.ldn:173

The loopback address of family, 127.0.0.1 or ::1, and a port: this system, reached without a network.

open_datagram function

public open_datagram: (provider: type is network, host: provider,
                       at: address) -> (socket: io.file)
                      ! address_in_use | address_unavailable | io.no_access
                      | io.io_failed

core/net/net.ldn:502

Open a UDP socket bound to at, port 0 for a free one.

parse_address function

public parse_address: (source: []u8) -> (at: address) ! invalid_address

core/net/net.ldn:336

Read an address and a port from their text: 127.0.0.1:8080, or an IPv6 address in brackets, [::1]:8080, as a URL writes them. The port is decimal, from 0 to 65535; an address is read as parse_ip reads one. Anything else is invalid_address.

parse_ip function

public parse_ip: (source: []u8) -> (at: address) ! invalid_address

core/net/net.ldn:319

Read an address without a port from its text, 127.0.0.1 or 2001:db8::1, with port 0: four decimal parts from 0 to 255 without a leading zero, or an IPv6 address's groups of one to four hexadecimal digits in either case, at most one :: and an IPv4 address in the last 32 bits. Anything else, a zone such as %eth0 and surrounding spaces among it, is invalid_address.

peer_address function

public peer_address: (provider: type is network, host: provider,
                      connection: io.file) -> (at: address) ! io.io_failed

core/net/net.ldn:554

The address and port of a connection's peer.

receive function

public receive: (provider: type is network, host: provider,
                 connection: io.file, into: []mut u8)
                -> (count: usize) ! disconnected | timed_out | io.io_failed

core/net/net.ldn:518

Receive what arrives next on a connection into into: its count, zero once the peer has finished sending.

receive_from function

public receive_from: (provider: type is network, host: provider,
                      socket: io.file, into: []mut u8)
                     -> (received: datagram) ! timed_out | io.io_failed

core/net/net.ldn:532

Receive the next datagram into into and say who sent it.

same_address function

public same_address: (left: address, right: address) -> (same: bool)

core/net/net.ldn:196

Whether two addresses are the same family, address and port.

send function

public send: (provider: type is network, host: provider, connection: io.file,
              bytes: []u8) -> none ! disconnected | timed_out | io.io_failed

core/net/net.ldn:511

Send all of bytes over a connection. A connection whose peer has gone is disconnected.

send_to function

public send_to: (provider: type is network, host: provider, socket: io.file,
                 bytes: []u8, to: address)
                -> none ! unreachable | timed_out | io.io_failed

core/net/net.ldn:525

Send bytes as one datagram to to.

set_no_delay function

public set_no_delay: (provider: type is network, host: provider,
                      connection: io.file, on: bool) -> none ! io.io_failed

core/net/net.ldn:568

Send a connection's small writes at once rather than gather them.

set_timeouts function

public set_timeouts: (provider: type is network, host: provider,
                      socket: io.file, receive_limit: time.duration,
                      send_limit: time.duration) -> none ! io.io_failed

core/net/net.ldn:561

Limit how long a receive or an accept, and a send, waits on socket; zero is no limit. A limit that passes is timed_out.

shutdown function

public shutdown: (provider: type is network, host: provider,
                  connection: io.file, stopping: direction)
                 -> none ! io.io_failed

core/net/net.ldn:540

Stop receiving, sending or both on a connection: io.sending lets the peer read the end of what was sent.

unspecified function

public unspecified: (family: address_family, port: u16) -> (at: address)

core/net/net.ldn:185

The unspecified address of family, 0.0.0.0 or ::, and a port: a socket bound to it takes what arrives at the port on every interface.

with_port function

public with_port: (at: address, port: u16) -> (result: address)

core/net/net.ldn:190

The address at with another port.