Petrichor.Net#
Petrichor.Net provides TCP and UDP helpers for Luau mods.
- TCP
connectandrecvrun on background threads and accept an optional timeout. Results resume on the main thread through Petrichor.Task. - TCP
sendandcloserun synchronously on the calling thread. - UDP
udp_open,udp_send,udp_connect,udp_send_data,udp_bind,udp_recv_nonblock, andudp_closeare synchronous.udp_recvis async like TCPrecvand also accepts an optional timeout. - UDP sockets default to IPv4 but can be opened as IPv6 via
Net.TYPES.INET6: see Address families below.
Loading#
Return values#
Async calls (connect, recv, udp_recv) resume with two values: result, err.
- On success,
errisnil. - On failure,
resultisnilanderris a string. For timed-out calls,erris exactly the string"timeout".
Check if err then (or if not result then) before using the result.
TCP#
Net.connect(host, port, timeout?) -> thunk#
Starts a TCP connect to host:port. timeout is in seconds and defaults to 30. Use with Task.await:
import * as Log from "Petrichor.Log"
Task.async(() => {
const [sock, err] = Task.await(Net.connect("localhost", 9000, 5))
if (err) return
Net.send(sock, "hello from petrichor\n")
const [data, recvErr] = Task.await(Net.recv(sock))
if (recvErr) return
Log.info("my-mod", `echo: ${data}`)
Net.close(sock)
})()
On success, sock is TCP socket userdata. If the timeout elapses before a connection succeeds, err is "timeout".
If host resolves to multiple addresses (e.g. a dual-stack hostname like localhost, which commonly resolves to both an IPv6 and an IPv4 address), connect tries each resolved address in turn until one succeeds, splitting the timeout across remaining candidates. You don't need to pick a family yourself for TCP. This only matters for UDP, see below.
Net.recv(sock, max_bytes?, timeout?) -> thunk#
Reads up to max_bytes (default 4096) on a background thread, with an optional timeout in seconds (default 30). Resumes with data, err. If the timeout elapses with no data, err is "timeout".
Closing sock (via Net.close) is safe to do immediately after a recv timeout or any other resolved recv. The background thread won't touch the socket again once it has reported a result.
Net.send(sock, data)#
Sends a string on the TCP socket.
Net.close(sock)#
Closes the TCP socket.
UDP#
Address families#
UDP sockets are bound to a single address family for their lifetime, set when they're opened. Use Net.TYPES to pick one explicitly, or pass nothing for the default:
| Constant | Value | Family |
|---|---|---|
Net.TYPES.INET | "inet" | IPv4 (default) |
Net.TYPES.INET6 | "inet6" | IPv6 |
A plain string ("inet" / "inet6") works too: Net.TYPES is just a named alternative to typing the string yourself. Any other value falls back to Net.TYPES.INET.
If you try to udp_send or udp_connect to a host that doesn't resolve to an address in your socket's family, for example an INET6 socket targeting a bare IPv4 literal like "127.0.0.1", you'll get back an explicit "address family mismatch" error rather than a generic connect/send failure.
Net.udp_open(family?) -> sock?, err?#
Creates a UDP socket. family is Net.TYPES.INET (default) or Net.TYPES.INET6. On failure returns nil and an error string.
Net.udp_send(sock, host, port, data) -> err?#
Sends a datagram. Returns an error string on failure, otherwise nil. Failures include both resolution problems and actual send-level errors (e.g. a datagram larger than the OS will send). err describes whichever occurred.
Net.udp_connect(sock, host, port) -> err?#
Sets the socket's default peer with connect(). After this, use udp_send_data instead of udp_send. Returns an error string on failure.
Net.udp_send_data(sock, data) -> err?#
Sends a datagram to the peer set by udp_connect. Returns an error string on a send-level failure (e.g. an oversized datagram), otherwise nil.
Net.udp_bind(sock, port) -> err?#
Binds the socket so you can receive datagrams to 0.0.0.0:port for an INET socket, or [::]:port for an INET6 socket. Returns an error string on failure.
Net.udp_recv(sock, max_bytes?, timeout?) -> thunk#
Receives up to max_bytes (default 4096) on a background thread, with an optional timeout in seconds (default 30). Use with Task.await:
On success, resumes with the datagram string and nil error. If the timeout elapses with no datagram, err is "timeout".
Net.udp_recv_nonblock(sock, max_bytes?) -> {string}#
Drains all datagrams currently waiting on the socket without blocking. Returns a Luau array (1-based table) of strings. max_bytes caps each read (default 4096).
Use this from tick when you want to poll UDP without Task.async. For a single blocking-style receive on a background thread, use udp_recv instead.
Net.udp_close(sock)#
Closes the UDP socket.
Full example (TCP + UDP + Task)#
local ExampleNet = mod("example-net")
local Task = require("Petrichor.Task")
local Net = require("Petrichor.Net")
local Log = require("Petrichor.Log")
function ExampleNet:init()
Task.async(function()
local sock, err = Task.await(Net.connect("localhost", 9000, 5))
if err then
Log.err("example-net", "connect failed: " .. err)
return
end
Net.send(sock, "hello from petrichor\n")
local data, recv_err = Task.await(Net.recv(sock))
Net.close(sock)
if not recv_err then
self.log(Levels.INFO, "echo: " .. data)
end
end)()
local udp, udp_err = Net.udp_open()
if not udp_err then
Net.udp_send(udp, "localhost", 9001, "hello from petrichor UDP")
Net.udp_close(udp)
end
-- IPv6 UDP works the same way, just pass Net.TYPES.INET6 to udp_open
local udp6, udp6_err = Net.udp_open(Net.TYPES.INET6)
if not udp6_err then
Net.udp_send(udp6, "::1", 9002, "hello over IPv6 UDP")
Net.udp_close(udp6)
end
end
return ExampleNet
import * as Task from "Petrichor.Task"
import * as Net from "Petrichor.Net"
import * as Log from "Petrichor.Log"
declare function mod(name: string): any
declare const Levels: { INFO: string; WARN: string; ERR: string }
const ExampleNet = mod("example-net")
ExampleNet.init = function () {
Task.async(() => {
const [sock, err] = Task.await(Net.connect("localhost", 9000, 5))
if (err) {
Log.err("example-net", `connect failed: ${err}`)
return
}
Net.send(sock, "hello from petrichor\n")
const [data, recvErr] = Task.await(Net.recv(sock))
Net.close(sock)
if (!recvErr) {
this.log(Levels.INFO, `echo: ${data}`)
}
})()
const [udp, udpErr] = Net.udp_open()
if (!udpErr) {
Net.udp_send(udp, "localhost", 9001, "hello from petrichor UDP")
Net.udp_close(udp)
}
// IPv6 UDP works the same way, just pass Net.TYPES.INET6 to udp_open
const [udp6, udp6Err] = Net.udp_open(Net.TYPES.INET6)
if (!udp6Err) {
Net.udp_send(udp6, "::1", 9002, "hello over IPv6 UDP")
Net.udp_close(udp6)
}
}
export = ExampleNet
self.log(Levels.INFO, ...) comes from Petrichor.Standard (injected on every mod() instance). Levels is a global from the same module.