Packages

net

Open TCP and UDP sockets, listen for connections, and resolve host names.

import "std:net"

std:net is the low-level network layer: TCP connections, TCP listeners, UDP sockets and name lookups. Reach for it when you speak a protocol that isn't HTTP, such as a game server, a line-based text protocol or a custom binary format. For web requests use requests, for web servers use serve, and for URLs use url.

Every example runs both ends in one program over the loopback address. Asking for port 0 lets the system pick a free one, and localAddr() tells you which.

A TCP echo#

net.listen("tcp", address) returns a listener. listener.accept() waits for one connection, so serve in an async function and connect from the main thread. net.connect opens the client side:

main.osl
import "std:net"

auto listener = net.listen("tcp", "127.0.0.1:0").unwrap()
string address = listener.localAddr()

async serveOne() string (
  auto conn = listener.accept().unwrap()
  string data = conn.read(1024).unwrap()
  conn.write("echo: " ++ data).unwrap()
  conn.close().unwrap()
  return data
)
auto server = serveOne()

auto client = net.connect("tcp", address, {timeout: 2000}).unwrap()
client.write("hello").unwrap()
log client.read(1024).unwrap()
client.close().unwrap()

log "server saw" server.wait()
listener.close().unwrap()
Click the code to edit it

Change "hello" to something longer than 1024 characters, or shorter than one, and see how read behaves.

read(maxBytes) waits for data and returns up to that many bytes as text. It returns whatever has arrived, which may be less than the other side wrote in one go, because TCP is a stream and has no message boundaries. write sends all of its data or fails.

Reading until the end#

A peer that closes its side ends the stream, and the next read fails with a StateError. A loop that collects data until then reads a whole reply:

main.osl
import "std:net"

auto listener = net.listen("tcp", "127.0.0.1:0").unwrap()

async serveOne() int (
  auto conn = listener.accept().unwrap()
  conn.write("first line\nsecond line\n").unwrap()
  conn.close().unwrap()
  return 1
)
auto server = serveOne()

auto client = net.connect("tcp", listener.localAddr()).unwrap()
string text = ""
while true (
  auto part = client.read(8)
  if part.isErr() break
  text = text ++ part.unwrap()
)
client.close().unwrap()
server.wait()
listener.close().unwrap()

log text.len
log text.trim().split("\n")
Click the code to edit it

A protocol with several messages on one connection needs its own framing: a newline, a length prefix or a fixed size. Collect bytes and cut them at the boundaries.

Timeouts#

connect, read and write take a timeout option in milliseconds. A read that times out fails with a TimeoutError, and the connection stays usable:

main.osl
import "std:net"

auto listener = net.listen("tcp", "127.0.0.1:0").unwrap()
auto client = net.connect("tcp", listener.localAddr()).unwrap()

auto quiet = client.read(10, {timeout: 100})
log quiet.isErr() quiet.unwrapErr().kind

client.close().unwrap()
listener.close().unwrap()
Click the code to edit it

Without a timeout, a read waits for as long as it takes. Closing a connection from another thread wakes a waiting read with an error, which is how you stop a reader.

Connection errors#

net.connect returns an error with a kind: NetworkError when the connection is refused, NotFoundError for a host name that doesn't exist and TimeoutError when it takes too long:

main.osl
import "std:net"

auto refused = net.connect("tcp", "127.0.0.1:1", {timeout: 1000})
log refused.isErr() refused.unwrapErr().kind

switch net.connect("tcp", "127.0.0.1:1") (
  case ok(conn)
    log "connected to" conn.remoteAddr()
  case err(e)
    log "failed:" e.kind
)
Click the code to edit it

UDP#

UDP sends separate datagrams with no connection. Bind a socket with net.listenUDP, send with write(data, address) and receive with read, which gives {data, addr}:

main.osl
import "std:net"

auto receiver = net.listenUDP("udp", "127.0.0.1:0").unwrap()
auto sender = net.listenUDP("udp", "127.0.0.1:0").unwrap()

sender.write("ping", receiver.localAddr()).unwrap()
auto packet = receiver.read(100).unwrap()
log packet.data
log packet.addr == sender.localAddr()

receiver.write("pong", packet.addr).unwrap()
log sender.read(100).unwrap().data

log receiver.read(100, {timeout: 100}).unwrapErr().kind

receiver.close().unwrap()
sender.close().unwrap()
Click the code to edit it

Datagrams can be lost, duplicated or arrive out of order, and a datagram bigger than maxBytes is cut. Use UDP when losing one doesn't matter, and TCP when it does.

Binary data#

read and write work on text. For raw bytes, readBytes(maxBytes) returns byte[] and write also accepts byte[]:

main.osl
import "std:net"

auto listener = net.listen("tcp", "127.0.0.1:0").unwrap()

async serveOne() int (
  auto conn = listener.accept().unwrap()
  byte[] data = conn.readBytes(16).unwrap()
  conn.write(data).unwrap()
  conn.close().unwrap()
  return data.len
)
auto server = serveOne()

auto client = net.connect("tcp", listener.localAddr()).unwrap()
byte[] payload = [1, 2, 3, 255]
client.write(payload).unwrap()
byte[] back = client.readBytes(16).unwrap()
int total = 0
for b in back (
  total = total + b
)
log back.len total
log "server read" server.wait() "bytes"

client.close().unwrap()
listener.close().unwrap()
Click the code to edit it

Looking up names#

net.lookupIP, net.lookupHost, net.lookupPort and net.addressInfo ask the system's resolver. Names that are already addresses resolve to themselves, so these examples need no network:

main.osl
import "std:net"

log net.lookupIP("127.0.0.1").unwrap()
log net.lookupPort("https", "tcp").unwrap()
log net.lookupPort("http", "tcp").unwrap()
log net.lookupHost("localhost").unwrap().len > 0
Click the code to edit it

A name that doesn't resolve fails with a NotFoundError. A lookup waits on a real resolver, so expect it to take time and handle the failure.

Quick check

You call conn.read(1024) and get 5 bytes, but the other side wrote a 20 byte message. Is that a bug?

Your turn

Shout over UDP

Send the text hello from sender to receiver and print what the receiver gets, upper-cased.

main.osl
Ctrl Enter runs
Expected output
HELLO

API reference#

25 members, generated from the std:net source of OSL 9.3.0.

Functions

#net.addressInfo(string hostname) result<{hostname: string, hosts: string[], ips: string[]}, error>

addressInfo resolves hostname and returns {hostname, hosts, ips}: the name asked for, its addresses as net.lookupHost returns them, and its IP addresses as net.lookupIP returns them. Fails like net.lookupHost.

#net.connect(string network, string address, object? options) result<*net.TCPConn, error>

connect opens a stream connection to address, such as "example.com:80" or "127.0.0.1:8080" (a socket path for "unix"). network is "tcp", "tcp4", "tcp6" or "unix". Options: timeout (milliseconds to wait for the connection, default 5000). Fails with NetworkError when the connection is refused or unreachable, NotFoundError when the host doesn't resolve, TimeoutError when it takes too long and ValueError for a malformed address or unknown network.

#net.dial(any network, any address) *net.TCPConn

Deprecated. Deprecated: use net.connect(network, address) instead, which reports why the connection failed.

dial opens a stream connection, or returns null when it fails.

#net.getAddressInfo(any hostname) object

Deprecated. Deprecated: use net.addressInfo(hostname) instead, which fails with a typed error.

getAddressInfo resolves hostname into {hostname, hosts, ips}, with empty arrays when it fails.

#net.listen(string network, string address) result<*net.Listener, error>

listen starts accepting stream connections on address, such as "127.0.0.1:0" (port 0 picks a free port; read it back with localAddr()). network is "tcp", "tcp4", "tcp6" or "unix". Fails with NetworkError when the address is in use or can't be bound and ValueError for a malformed address or unknown network.

#net.listenUDP(string network, string address) result<*net.UDPConn, error>

listenUDP binds a UDP socket to address, such as "127.0.0.1:0". network is "udp", "udp4" or "udp6". Fails with NetworkError when the address can't be bound and ValueError for a malformed address or unknown network.

#net.lookupHost(string hostname) result<string[], error>

lookupHost resolves hostname to its addresses, as strings such as "93.184.216.34". Fails with NotFoundError when the host doesn't exist, TimeoutError when the resolver doesn't answer and NetworkError otherwise.

#net.lookupIP(string hostname) result<string[], error>

lookupIP resolves hostname to its IPv4 and IPv6 addresses. Fails like net.lookupHost.

#net.lookupPort(string service, string network) result<int, error>

lookupPort returns the port number of a service name such as "https" on network ("tcp" or "udp"); a numeric service is returned as a number. Fails with NotFoundError for an unknown service.

*net.TCPConn

#tCPConn.close() result<unit, error>

close closes the connection; a read waiting in another thread then fails with StateError. Closing twice is a no-op.

#tCPConn.localAddr() string

localAddr returns this end's address, such as "127.0.0.1:51514".

#tCPConn.read(int maxBytes, object? options) result<string, error>

read waits for data and returns up to maxBytes bytes of it as a string. Once the peer has closed the connection and everything it sent has been read, it fails with StateError, so a read loop ends with string chunk = conn.read(4096) catch (err) ( break ). Options: timeout (milliseconds to wait for data; by default it waits indefinitely). Fails with TimeoutError when the timeout passes and NetworkError when the connection breaks. Throws ValueError when maxBytes is less than 1 and StateError on a connection closed with close(). Reads are capped at 16 MiB.

#tCPConn.readBytes(int maxBytes, object? options) result<byte[], error>

readBytes is read returning byte[]. It takes the same options and fails the same ways, including StateError at the end of the stream.

#tCPConn.remoteAddr() string

remoteAddr returns the peer's address, such as "93.184.216.34:80".

#tCPConn.setTimeout(any seconds) boolean

Deprecated. Deprecated: pass {timeout: milliseconds} to read or write instead.

setTimeout sets a deadline seconds from now for every later read and write, and reports whether it worked.

#tCPConn.write(any data, object? options) result<unit, error>

write sends data, a string or byte[], in full. Options: timeout (milliseconds; by default it waits indefinitely). Fails with TimeoutError when the timeout passes and NetworkError when the connection breaks. Throws TypeError for other data types and StateError on a closed connection.

#tCPConn.writeBytes(byte[] data) boolean

Deprecated. Deprecated: use conn.write(data) instead, which accepts byte[] and fails with a typed error.

writeBytes sends bytes and reports whether it worked.

*net.Listener

#listener.accept() result<*net.TCPConn, error>

accept waits for the next incoming connection and returns it. Fails with StateError when the listener is closed while waiting, which is how another thread stops an accept loop. Throws StateError when the listener was already closed.

#listener.close() result<unit, error>

close stops accepting connections and wakes any accept() call waiting on this listener. Closing twice is a no-op.

#listener.localAddr() string

localAddr returns the address the listener is bound to, such as "127.0.0.1:41234".

*net.UDPConn

#uDPConn.close() result<unit, error>

close closes the socket; a read waiting in another thread then fails with StateError. Closing twice is a no-op.

#uDPConn.localAddr() string

localAddr returns the address the socket is bound to, such as "127.0.0.1:41234".

#uDPConn.read(int maxBytes, object? options) result<{addr: string, data: string}, error>

read waits for the next datagram and returns {data, addr}: its payload as a string, truncated to maxBytes, and the sender's address. Options: timeout (milliseconds; by default it waits indefinitely). Fails with TimeoutError when the timeout passes and StateError when the socket is closed while waiting. Throws ValueError when maxBytes is less than 1.

#uDPConn.readBytes(int maxBytes, object? options) result<{addr: string, data: byte[]}, error>

readBytes is read with the payload as byte[]: it returns {data, addr}.

#uDPConn.write(any data, string address) result<unit, error>

write sends data, a string or byte[], as one datagram to address, such as "127.0.0.1:9000". Fails with NotFoundError when the host doesn't resolve, ValueError for a malformed address and NetworkError when sending fails. Throws TypeError for other data types and StateError on a closed socket.