Skip to content

Instantly share code, notes, and snippets.

@airstruck
Last active May 15, 2017 06:22
Show Gist options
  • Save airstruck/5e0d0336231cfdefd3f6424afe6e75dc to your computer and use it in GitHub Desktop.
Save airstruck/5e0d0336231cfdefd3f6424afe6e75dc to your computer and use it in GitHub Desktop.
RPC in Lua
-- RPC for Lua
-- From: https://gist.github.com/airstruck/5e0d0336231cfdefd3f6424afe6e75dc
-- License: MIT https://opensource.org/licenses/MIT
local socket = require 'socket' -- https://github.com/diegonehab/luasocket
local binser = require 'binser' -- https://github.com/bakpakin/binser
local DEFAULT_HOST = '127.0.0.1'
local DEFAULT_PORT = 4135
local unpack = unpack or table.unpack
-- Escape and unescape data for network transfer.
-- Escapes line breaks, they signal end of data.
local ESC = string.char(27)
local ESC_ESC = ESC .. '1'
local ESC_LF = ESC .. '2'
local function escape (data)
return (data:gsub(ESC, ESC_ESC):gsub('\n', ESC_LF))
end
local function unescape (data)
return (data:gsub(ESC_LF, '\n'):gsub(ESC_ESC, ESC))
end
-- Serialize and deserialize data.
local function serialize (...)
return escape(binser.serialize(...)) .. '\n'
end
local function deserialize (data)
local results, len = binser.deserialize(unescape(data))
return unpack(results, 1, len)
end
-- Client stuff.
-- Call an RPC method and return the results.
local function call (client, ...)
local tcp = socket.tcp()
tcp:settimeout(1)
tcp:connect(client.host, client.port)
assert(tcp:send(serialize(...)))
local result = assert(tcp:receive())
tcp:close()
return select(2, assert(deserialize(result)))
end
-- Indexing an undefined field creates an RPC method.
local clientMeta = {
__index = function (t, k)
t[k] = function (t, ...) return call(t, k, ...) end
return t[k]
end,
}
-- Client constructor.
local function createClient (host, port)
local client = {}
client.host = host or DEFAULT_HOST
client.port = port or DEFAULT_PORT
return setmetatable(client, clientMeta)
end
-- Server stuff
-- Execute an RPC method.
local function execute (server, ok, proc, ...)
if not ok then return ok, proc, ... end
return pcall(server.exports[proc], server, ...)
end
-- Handles clients. Call server:update() in a loop.
local function update (server)
local tcp = server.socket:accept()
if not tcp then return end
local data = tcp:receive()
if data then
rawset(server, 'client', tcp) -- expose client socket to remote procs
tcp:send(serialize(execute(server, pcall(deserialize, data))))
else
tcp:send(serialize(false, 'no data'))
end
tcp:close()
end
-- User-defined methods are exposed to RPC clients.
local serverMeta = {
__newindex = function (t, k, v) t.exports[k] = v end,
__index = function (t, k) return t.exports[k] end,
}
-- Server constructor
local function createServer (host, port)
local server = {}
server.update = update
server.host = host or DEFAULT_HOST
server.port = port or DEFAULT_PORT
server.exports = {}
server.socket = assert(socket.bind(server.host, server.port))
server.socket:settimeout(1)
print('RPC on', server.socket:getsockname())
return setmetatable(server, serverMeta)
end
return { client = createClient, server = createServer }
local rpc = require 'rpc' .client()
print(rpc:multiply(3, 4))
print(rpc:concat('hello', 'world'))
local rpc = require 'rpc' .server()
function rpc:multiply (a, b)
return a * b
end
function rpc:concat (a, b)
return a .. b
end
while 1 do rpc:update() end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment