OC-Copper/relib.lua

174 lines
5.3 KiB
Lua

-- I, 20kdc, release this into the public domain.
-- No warranty is provided, implied or otherwise.
-- Copper Reliability Layer
-- Notably, this should be instantiated rather than the normal Copper instance.
-- The Copper node constructor (which could be replaced with something else,
-- should you decide to run the Reliability layer over another transport)
-- is passed as function (hostname, transmit, onReceive, time),
-- where hostname is the hostname given,
-- transmit is the (target/nil (for broadcast), data) function as given,
-- onReceive is the (nfrom. nto, ndata) function
-- internal to the Reliability Layer to translate packets for onRReceive
-- and acknowledge packets that need to be acknowledged,
-- and time is the function-that-returns-current-time-in-seconds
-- function as given.
-- The resulting Copper-like node should have the following fields:
-- node.hostname: Editable hostname.
-- node.refresh(): Do various timed cleanup tasks.
-- node.input(...): relib.input is set to this for convenience.
-- Ought to be function (opaqueFrom, data).
-- node.output(nFrom, nTo, data): The normal Copper send function.
-- onRReceive is now: (from, to, port, data, unreliablePacket)
-- where to can be anything for unreliable packets,
-- but otherwise is the current hostname.
return function (hostname, transmit, onRReceive, time, culib)
-- node.hostname should be used for hostname generally.
local node
-- The maximum amount of timers (used to cap memory usage).
-- Assuming 16 bytes per key, this should be 64k.
local tuningMaxTimers = 0x400
local tuningClearAntiduplicate = 60
local tuningAttempts = 12
local tuningAttemptTime = 2.5
-- Just an array, no special index.
-- Contents : {
-- trigger function,
-- expiry time
-- }
local timers = {}
-- Indexes are globalIds, values are timers for deleting entries out of this table.
local weAcked = {}
-- Indexes are globalIds, values are { successFunc, deathTimer }
local needsAck = {}
local function addTimer(trig, expi)
if #timers < tuningMaxTimers then
local t = {trig, time() + expi}
table.insert(timers, t)
return t
end
return nil
end
local function killTimer(t)
for i = 1, #timers do
if timers[i] == t then
table.remove(timers, i)
return
end
end
end
local function gen3Random()
return string.char(math.random(256) - 1) .. string.char(math.random(256) - 1) .. string.char(math.random(256) - 1)
end
local function genGlobalId(port)
local low = math.abs(math.floor(port)) % 256
local high = math.abs(math.floor(port / 256)) % 256
local portD = string.char(high) .. string.char(low)
return portD .. gen3Random()
end
local onReceive = function (nfrom, nto, data)
if data:len() < 7 then return end
local port = data:byte(2) + (data:byte(1) * 256)
local tp = data:byte(7)
local globalId = data:sub(1, 5)
if (tp == 0x01) or (tp == 0x00) then
-- Only send one acknowledgement per packet,
-- but only receive the packet once.
-- (This is why timers are counted - to prevent the weAcked pool from getting too big.)
if not weAcked[nto .. globalId] then
onRReceive(nfrom, nto, port, data:sub(8), tp == 0x00)
else
killTimer(weAcked[nto .. globalId])
end
weAcked[nto .. globalId] = addTimer(function ()
weAcked[nto .. globalId] = nil
end, tuningClearAntiduplicate)
-- Check if this should actually be ACKed
if tp ~= 0x01 then return end
if nto ~= node.hostname then return end
node.output(nto, nfrom, data:sub(1, 6) .. "\x02")
end
-- Ignore ACKs for our packets that aren't going our way.
-- (Just a fringe case of packet ID reuse)
if nto ~= node.hostname then
return
end
if (tp == 0x02) and needsAck[nfrom .. globalId] then
needsAck[nfrom .. globalId][1](nfrom)
killTimer(needsAck[nfrom .. globalId][2])
needsAck[nfrom .. globalId] = nil
end
end
-- Instantiate the node or lookalike.
node = culib(hostname, transmit, onReceive, time)
local relib = {}
relib.setHostname = function (h)
node.hostname = h
end
relib.getHostname = function ()
return node.hostname
end
relib.refresh = function ()
node.refresh()
local i = 1
local t = time()
while i <= #timers do
if timers[i][2] <= t then
table.remove(timers, i)[1]()
else
i = i + 1
end
end
end
relib.input = node.input
-- can be reduced to output(nto, port, data) safely
relib.output = function (nto, port, data, unreliable, onSucceed, onFailure)
onSucceed = onSucceed or (function () end)
onFailure = onFailure or (function () end)
local gid = genGlobalId(port)
local tp = "\x01"
-- Unreliable packets:
-- 1. Can't be ACKed (not in the needsAck table)
-- 2. Are otherwise subject to the same rules as regular packets
if unreliable then
tp = "\x00"
end
local na = {onSucceed}
local attempt = -1
local doAttempt
doAttempt = function ()
attempt = attempt + 1
if attempt == tuningAttempts then
if not unreliable then
needsAck[nto .. gid] = nil
onFailure()
end
return
end
node.output(node.hostname, nto, gid .. string.char(attempt) .. tp .. data)
na[2] = addTimer(doAttempt, tuningAttemptTime)
if not na[2] then
needsAck[nto .. gid] = nil
if not unreliable then
onFailure()
end
end
end
if not unreliable then
needsAck[nto .. gid] = na
end
doAttempt()
end
return relib
end