Async TCP, TLS, UDP, and Unix-domain sockets. Every operation returns a promise.
socket.resolve(host)→ A promise resolving to the list of every address of the host as strings, in the order the resolver of the system answers them. A numeric address resolves to itself. A host that does not exist rejects withThe host "name" could not be resolved..- A host name given to any call is looked up on the I/O pool, so a connect, a listen, a bind or a datagram never blocks the loop thread while a resolver answers. A numeric address is used as it is. A name resolves to the address the system prefers, which can be IPv6, so a listener and a client meant to meet bind and connect through the same name or the same numeric address.
socket.tcp.connect(host, port, opts?)→ A promise resolving to a socket. The tableoptstakestimeoutMs(from0to3600000), which bounds the lookup of the name and the connect together and rejects withThe connection to host:port timed out.instead of waiting for the system timeout, andmaxQueuedBytes.socket.tcp.listen(host, port, opts?)→ A promise resolving to a listener. The port may be0, which binds a free port the listener then reports. The tableoptstakesbacklog(from1to4096, default64),reusePortandmaxQueuedBytes, which every socket the listener accepts takes.- A listener binds without
SO_REUSEPORT, so a second listener on a busy port fails withThe address 127.0.0.1:41000 is already in use.instead of sharing it. On POSIX it binds withSO_REUSEADDR, so a restarted program binds a port whose connections are still inTIME_WAIT, which Windows allows on its own, and on Windows it binds withSO_EXCLUSIVEADDRUSE, so no other socket can take the port over. - With
reusePort = truethe listener binds withSO_REUSEPORT, and listeners that all ask for it share one port while the system spreads the connections across them. Windows has no such option, so there the listen rejects withThe option "reusePort" is not available on Windows.. - Listener:
listener:accept()(resolves to a socket) andlistener:close(), and the fieldslistener.hostandlistener.porthold the address it bound. - Socket:
sock:send(data),sock:receive(maxBytes?)(default65536),sock:shutdown("send"),sock:setNoDelay(enabled),sock:setKeepAlive(enabled)andsock:close()all return promises. - The method
sendresolves once the system took every byte, so an awaited send applies backpressure. The bytes of the sends not yet handed to the system are counted insock.pendingBytes, and a send that would raise that count pastmaxQueuedBytes(default67108864, 64 MiB) rejects at once withThe socket already queues 4194304 bytes, so a send of 1048576 bytes would pass its limit of 4194304 bytes.while the queued sends still leave. - The method
receiveresolves to the empty string once the peer closed its side of the stream. sock:shutdown("send")shuts the sending side down once every send queued before it left, so the peer reads the end of the stream while this socket still receives. A send after it rejects withThe socket was shut down for sending.. The argument names the side, and only"send"is accepted.sock:setNoDelay(enabled)setsTCP_NODELAY, which turns Nagle's algorithm off, andsock:setKeepAlive(enabled)setsSO_KEEPALIVE. Both take a boolean and resolve to"ok".- The fields
sock.localHost,sock.localPort,sock.peerHostandsock.peerPorthold the two ends of the connection.
socket.tls.connect(host, port, opts?)→ A promise resolving to a secure socket with the same methods and fields as a TCP socket. The connection completes the TLS handshake before resolving.sock:startTls(host, opts?)→ Upgrade an already-connected plaintext TCP socket to TLS in place, for protocols that negotiate TLS mid-stream (such as MySQL). Resolves once the handshake completes. The tableoptstakesinsecure = trueto skip certificate verification.- The argument
optsis an optional table:timeoutMsbounds the connect like the TCP variant,insecure = trueskips certificate verification andmaxQueuedBytesbounds the queued sends. - Certificates are verified against the system trust store by default. An invalid certificate rejects the connect with
The TLS handshake with host failed: .... Useinsecure = trueonly for self-signed endpoints under your control. - TLS requires a build with TLS enabled. Otherwise
socket.tls.connectrejects with a clear error. - On a secure socket the reads, writes, the shutdown and the handshake are serialized per connection, so overlapping operations on the same TLS socket run in order rather than in parallel. Issue a concurrent send and receive on separate connections if you need true full-duplex.
socket.unix.connect(path, opts?)→ A promise resolving to a socket connected to the filesystem path, with the same methods as a TCP socket. The tableoptstakesmaxQueuedBytes.socket.unix.listen(path, opts?)→ A promise resolving to a listener bound to the path. The tableoptstakesbacklog(default64) andmaxQueuedBytes. The path must not already exist. Remove a stale socket file before listening.- On a Unix-domain socket or listener the host fields hold the path and the port fields are
nil.
socket.udp.bind(host, port)→ A promise resolving to a UDP socket bound to the address. The port may be0, and the fieldsudp.hostandudp.porthold the address it bound. A UDP socket binds withoutSO_REUSEADDRorSO_REUSEPORT, since on Linux either lets a second socket share a busy port, and withSO_EXCLUSIVEADDRUSEon Windows, so a busy port fails withThe address 127.0.0.1:41000 is already in use..- UDP socket:
sock:sendTo(host, port, data),sock:recvFrom(maxBytes?)(default65536), andsock:close()all return promises. A datagram addressed by a name leaves once the name resolved, so it may leave after a later one addressed by a number. - The method
recvFromresolves to a table{ data, host, port }carrying the payload and the sender's address. sock:connect(host, port)→ Set the only peer the socket sends to withsendand receives from.sock:send(data)→ Send a datagram to that peer, which rejects withThe socket is not connected. Connect it with "connect" before calling "send".before a connect. A connected socket sends withsend, since some systems refuse asendToon it.sock:sendMany(list)→ Send a list of datagrams, each a table{ host, port, data }, in order, and resolve to how many were sent once every one left. A malformed entry raises before anything is sent, naming its index, a name that does not resolve rejects with its index before anything is sent, and a datagram that fails rejects with its index.sock:recvMany(maxDatagrams?, maxBytes?)→ Wait for a datagram, then resolve to a list of every datagram already waiting, up tomaxDatagrams(default64), each a table{ data, host, port }of at mostmaxBytesbytes (default65536). A game reads the whole burst of a frame in one call instead of one datagram per await.sock:setBroadcast(enabled)→ SetSO_BROADCAST, without which a datagram to a broadcast address such as255.255.255.255is refused by the system.sock:joinGroup(address)andsock:leaveGroup(address)→ Join or leave a multicast group, given as a numeric IPv4 or IPv6 group address, on the interface the system picks for it. Bind the socket to the wildcard address of its family to receive what the group carries. An address that is not a group rejects withThe address "x" is not a multicast group..
A socket that is ready serves every operation that waits on it, in order, until one would block, so several pending receives, sends or accepts on one socket all complete in the same turn of the loop. The HTTP server likewise reads a connection until it would block, up to a bound per turn. A host that polls the runtime with a budget repeats those turns while they make progress, so a task that reads again after a receive reads in the same poll rather than in the next frame.
- A failure names the address it concerns and carries the text of the error and the system error, such as
The connection to 127.0.0.1:9 was refused.,The connection to 192.0.2.1:80 timed out.orThe send to 127.0.0.1:5000 failed: Connection reset by peer (system error 54).. The number of a system error differs between systems. - A mistaken argument, such as a port outside its range or an option of the wrong type, raises at once instead of rejecting.
- Calling
close()is safe while an operation is still pending. A socket or listener is marked closed before its pending operations settle, so a pendingacceptrejects withThe listener was closed.and a pendingreceive,send,recvFromorrecvManyrejects withThe socket was closed.. An operation started after the close rejects the same way. - The empty string stays the end of a stream the peer closed, so a
receivetells a local close, which rejects, from the end of the data, which resolves to"". - The methods
receiveandrecvFromcap the read buffer (16 MB for TCP, 64 KB for UDP) regardless of the requestedmaxBytes, so an oversized request cannot drive a huge allocation. - The methods
sendManyandrecvManytake at most 1024 datagrams per call. - The arguments
host/portare validated before use: a port to connect or send to must be in1..65535, a port to listen or bind in0..65535, and the full integer is checked, not a truncated value.
-- Closing a socket rejects a pending receive with the close, closing a listener rejects a pending accept, and the peer reads the end of the stream.
local async = require("async")
local socket = require("socket")
local host = "127.0.0.1"
async.run(function()
-- A listener with no incoming connection keeps accept pending until closed.
local listener = socket.tcp.listen(host, 0):await()
async.spawn(function()
async.sleep(80):await()
listener:close():await()
end)
print("accept after close:", listener:accept():await())
-- A connected socket with an idle peer keeps receive pending until closed.
local server = socket.tcp.listen(host, 0):await()
local conn = socket.tcp.connect(host, server.port):await()
local peer = server:accept():await()
async.spawn(function()
async.sleep(80):await()
conn:close():await()
end)
print("receive after close:", conn:receive(4096):await())
print("peer reads the end:", #peer:receive():await())
peer:close():await()
server:close():await()
print("socket close pending ok")
end)-- A listener on port 0 reports the free port it bound, and a second listener on that port fails by its address.
local async = require("async")
local socket = require("socket")
async.run(function()
local listener = socket.tcp.listen("127.0.0.1", 0):await()
print("listening on", listener.host, listener.port)
local second, err = socket.tcp.listen("127.0.0.1", listener.port):await()
print("second listener:", second, err)
local udp = socket.udp.bind("127.0.0.1", 0):await()
print("udp bound to", udp.host, udp.port)
udp:close():await()
listener:close():await()
print("socket ports ok")
end)-- A name resolves on the I/O pool for a lookup, a listener and a connect, so the loop never waits for a resolver.
local async = require("async")
local socket = require("socket")
async.run(function()
print("localhost is", table.concat(socket.resolve("localhost"):await(), ", "))
print("unknown host:", select(2, socket.resolve("host.invalid"):await()))
local listener = socket.tcp.listen("localhost", 0):await()
local conn = socket.tcp.connect("localhost", listener.port, { timeoutMs = 2000 }):await()
local peer = listener:accept():await()
print("connected", conn.localHost, conn.localPort, "to", conn.peerHost, conn.peerPort)
conn:close():await()
peer:close():await()
listener:close():await()
print("socket names ok")
end)-- A client turns Nagle's algorithm off, bounds what it queues, and shuts its sending side down while it still reads the reply.
local async = require("async")
local socket = require("socket")
async.run(function()
local listener = socket.tcp.listen("127.0.0.1", 0):await()
local conn = socket.tcp.connect("127.0.0.1", listener.port, { maxQueuedBytes = 1024 }):await()
local peer = listener:accept():await()
conn:setNoDelay(true):await()
conn:setKeepAlive(true):await()
local queued = conn:send(string.rep("a", 1000))
print("pending bytes:", conn.pendingBytes)
print("over the bound:", select(2, conn:send(string.rep("b", 100)):await()))
queued:await()
-- The peer reads the request up to the end of the stream and answers on the side that is still open.
conn:shutdown("send"):await()
local request = ""
while true do
local chunk = peer:receive():await()
if chunk == "" then
break
end
request = request .. chunk
end
peer:send("got " .. #request .. " bytes"):await()
print("reply:", conn:receive():await())
conn:close():await()
peer:close():await()
listener:close():await()
print("socket options ok")
end)-- A connected UDP socket sends with `send`, broadcast is turned on before a broadcast, and a socket joins and leaves a multicast group.
local async = require("async")
local socket = require("socket")
async.run(function()
local server = socket.udp.bind("127.0.0.1", 0):await()
local client = socket.udp.bind("127.0.0.1", 0):await()
client:connect("127.0.0.1", server.port):await()
client:send("hello"):await()
local packet = server:recvFrom():await()
print("server got", packet.data, "from port", packet.port)
local lan = socket.udp.bind("0.0.0.0", 0):await()
lan:setBroadcast(true):await()
print("broadcast:", lan:sendTo("255.255.255.255", lan.port, "anyone there?"):await())
print("join:", lan:joinGroup("239.255.42.99"):await())
print("leave:", lan:leaveGroup("239.255.42.99"):await())
lan:close():await()
client:close():await()
server:close():await()
print("socket udp options ok")
end)-- Echo service with an overridable listen port through the environment.
local async = require("async")
local socket = require("socket")
local host = os.getenv("VARN_SOCKET_HOST") or "127.0.0.1"
local port = tonumber(os.getenv("VARN_SOCKET_PORT") or "9000")
local function handleClient(sock)
while true do
local chunk, err = sock:receive(4096):await()
if err then
print("client receive error:", err)
break
end
if #chunk == 0 then
break
end
local _, sendErr = sock:send("You sent: " .. chunk):await()
if sendErr then
print("client send error:", sendErr)
break
end
end
sock:close():await()
end
async.spawn(function()
local listener, lerr = socket.tcp.listen(host, port, { backlog = 128 }):await()
if lerr then
error(lerr)
end
print(string.format("tcp echo listening on %s:%d", host, port))
while true do
local client, aerr = listener:accept():await()
if aerr then
print("accept error:", aerr)
break
end
async.spawn(function()
handleClient(client)
end)
end
listener:close():await()
end)-- An in-process TCP server echoes one message back to a client and both shut down.
local async = require("async")
local socket = require("socket")
local host = "127.0.0.1"
local port = 9831
async.spawn(function()
local listener = socket.tcp.listen(host, port, { backlog = 16 }):await()
local client = listener:accept():await()
local chunk = client:receive(4096):await()
client:send("echo:" .. chunk):await()
client:close():await()
listener:close():await()
end)
async.run(function()
async.sleep(80):await()
local conn = socket.tcp.connect(host, port):await()
conn:send("hello"):await()
local reply = conn:receive(4096):await()
print("tcp reply:", reply)
conn:close():await()
print("socket tcp round-trip ok")
end)-- Opens a verified TLS connection and speaks a minimal HTTP request over it.
local async = require("async")
local socket = require("socket")
local host = os.getenv("VARN_TLS_HOST") or "example.com"
local port = tonumber(os.getenv("VARN_TLS_PORT") or "443")
async.run(function()
local conn, cerr = socket.tls.connect(host, port, { timeoutMs = 5000 }):await()
if cerr then
print("tls connect error:", cerr)
return
end
local request = "GET / HTTP/1.0\r\nHost: " .. host .. "\r\nConnection: close\r\n\r\n"
conn:send(request):await()
local reply, rerr = conn:receive(512):await()
if rerr then
print("tls receive error:", rerr)
else
local statusLine = reply:match("^[^\r\n]*")
print("tls status:", statusLine)
end
conn:close():await()
print("socket tls client ok")
end)-- UDP echo service with an overridable bind address through the environment.
local async = require("async")
local socket = require("socket")
local host = os.getenv("VARN_SOCKET_HOST") or "127.0.0.1"
local port = tonumber(os.getenv("VARN_SOCKET_PORT") or "9000")
async.spawn(function()
local sock, berr = socket.udp.bind(host, port):await()
if berr then
error(berr)
end
print(string.format("udp echo listening on %s:%d", host, port))
while true do
local packet, rerr = sock:recvFrom(4096):await()
if rerr then
print("recv error:", rerr)
break
end
local _, serr = sock:sendTo(packet.host, packet.port, "You sent: " .. packet.data):await()
if serr then
print("send error:", serr)
break
end
end
sock:close():await()
end)-- An in-process UDP server echoes one datagram back and both sockets close.
local async = require("async")
local socket = require("socket")
local host = "127.0.0.1"
local serverPort = 9841
local clientPort = 9842
async.spawn(function()
local server = socket.udp.bind(host, serverPort):await()
local packet = server:recvFrom(4096):await()
server:sendTo(packet.host, packet.port, "echo:" .. packet.data):await()
server:close():await()
end)
async.run(function()
async.sleep(80):await()
local client = socket.udp.bind(host, clientPort):await()
client:sendTo(host, serverPort, "hello"):await()
local reply = client:recvFrom(4096):await()
print("udp reply:", reply.data, "from", reply.host, reply.port)
client:close():await()
print("socket udp round-trip ok")
end)-- One socket sends a burst of datagrams in one call and the other reads every datagram that waits in one call.
local async = require("async")
local socket = require("socket")
local host = "127.0.0.1"
local serverPort = 9843
local clientPort = 9844
async.run(function()
local server = socket.udp.bind(host, serverPort):await()
local client = socket.udp.bind(host, clientPort):await()
local burst = {}
for i = 1, 10 do
burst[i] = { host = host, port = serverPort, data = "state " .. i }
end
print("udp sent:", client:sendMany(burst):await())
local received = 0
while received < 10 do
local datagrams = server:recvMany(64):await()
received = received + #datagrams
print("udp batch of", #datagrams, "starting with", datagrams[1].data)
end
server:close():await()
client:close():await()
print("socket udp batches ok")
end)-- An in-process Unix-domain server echoes one message back to a client and both shut down.
local async = require("async")
local socket = require("socket")
local path = os.tmpname() .. ".sock"
os.remove(path)
async.spawn(function()
local listener = socket.unix.listen(path, { backlog = 16 }):await()
local client = listener:accept():await()
local chunk = client:receive(4096):await()
client:send("echo:" .. chunk):await()
client:close():await()
listener:close():await()
end)
async.run(function()
async.sleep(80):await()
local conn = socket.unix.connect(path):await()
conn:send("hello"):await()
local reply = conn:receive(4096):await()
print("unix reply:", reply)
conn:close():await()
os.remove(path)
print("socket unix round-trip ok")
end)Built on the Poco C++ networking libraries, with every plaintext socket multiplexed on the event loop so a blocked accept or receive never ties up a worker. Host names resolve on the I/O pool. TLS connections use Poco's SecureStreamSocket and run their handshake, reads and writes blocking on the I/O pool, one operation at a time per connection.