#!/usr/bin/luajit -- Minimal IPv4 TCP Lua server for LE Linux -- -- Usage summary: --[[ salisten = require "salisten" -- load this module server = salisten.server(5900) -- port 5900 client = server:accept() req = client:read(53) -- max 53 bytes client:write("you said "..req) client:close() server:close() --]] -- --[[ I’m using the LuaJIT FFI instead of LuaSocket, which isn’t in the Termux repos. At this point I seem to have the code pretty much working, so it’s worthwhile to reflect a bit on the last hour and a half. I got a confusing message from setmetatable; I was mistakenly trying to set a metatable on a file descriptor number instead of a table wrapping it, which failed because you can’t do that to a number. The confusing part was that the traceback only went down to accept(), the caller of the caller of setmetatable, so it was hard to figure out why accept was expecting a table rather than a number. Another problem is that I can’t set a `__gc` metamethod on a table, only a userdata (or in LuaJIT a cdata). I could totally wrap the fd in a cdata instead of a table in order to get that functionality, and maybe I should. But really I think the thing to do is to provide a pcall wrapper method that guarantees closing the connection. That should cover the needs of basic kinds of networking servers well enough. This was a surprising amount of code, over 100 lines. I’ve written networking servers in three lines of obfuscated C before, but using the sockets API the “proper” way involves a lot of bureaucracy. Because I’m not using the system header files, I’m not even getting the portability payoff from the bureaucracy; the magic numerical constants are hardcoded, as is the byte swapping, and will probably only work on little-endian Linux. And I was screwing it up, applying SO_REUSEADDR too late, after the bind() had already happened; the API said nothing. No error messages, nothing. Error messages were a significant part of the effort, and I’m still not happy with them; Lua’s resorting to just a string for the traceback and the error type is a real problem. And I used the possibly-not-thread-safe (?) strerror function, too. I’m not sure if I will have trouble if I fork to handle clients. Current Unix has a lot of threading stuff that interacts very poorly with forking. But LuaJIT seems like the kind of thing that would be careful not to impose extra load on you with stuff like multithreading. I learned more about the LuaJIT FFI. I used ffi.string and ffi.errno() for the first time, and got a lot more comfortable with the implicit conversion to pointers. Or lack thereof, in the case of non-const char pointers and strings. On one occasion I was puzzled for a while about why I was still getting a casting error passing a sockaddr_in to accept; it was because I had used ffi.cast to get a struct sockaddr pointer, but forgot to actually pass it to accept. I ended up removing that code anyway because it was segfaulting and not really needed for a *minimal* TCP server, which was my goal. I had actually forgotten that accept() had the option to pass you the peer address. And I forgot to initialize the length of the sockaddr buffer, so at first I wasn’t getting it, because LuaJIT initialized the size to 0. I figured out the problem looking at strace, which was overall a huge help. There might be some way to convert a raw Unix socket file descriptor number into a Lua file descriptor object, but I wasn’t able to find it. So I just wrote a quick connection class with read, write, and close methods. My sneaky motivation for writing this code was that I would kind of like to have a VNC server written in Lua for some experiments wifh Yeso. The immediate obstacle was whether I could get a TCP listening socket running in Lua; I figured that, with the LuaJIT FFI, it would be trivial. It took me almost two hours, which was a lot harder than I expected, but now it’s working! The rest of a VNC server is just a simple matter of logic; decoding packets and composing images and whatnot. I’ve done it before in Golang, including on this cellphone. The Golang version was about 200 lines of code, so maybe I can get a LuaJIT VNC server written tomorrow, using this code to get the socket open. In terms of usability, it might be a good idea to accept an IP address in some other form than a 32-bit integer. --]] ffi = require "ffi" ffi.cdef[[ enum { AF_INET = 2, SOCK_STREAM = 1, SOL_SOCKET = 1, SO_REUSEADDR = 2, }; typedef unsigned short sa_family_t; typedef uint32_t socklen_t; typedef uint16_t in_port_t; struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; /* Internet address */ struct in_addr { /* address in network byte order */ uint32_t s_addr; }; struct sockaddr_in { /* address family: AF_INET */ sa_family_t sin_family; /* port in network byte order */ in_port_t sin_port; /* internet address */ struct in_addr sin_addr; }; int socket(int domain, int type, int protocol); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); int listen(int sockfd, int backlog); int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); char *strerror(int errnum); ssize_t read(int fd, char *buf, size_t count); ssize_t write(int fd, const char *buf, size_t count); int close(int fd); ]] local salisten = {} -- module -- Convert a C errno error into a Lua error local function syserr(context) local err = ffi.errno() local s = ffi.string(ffi.C.strerror(err)) error(context .. ": " .. s) end local connmethods = {} -- connection methods -- Read up to a maximum number of bytes function connmethods.read(conn, maxlen) local buf = ffi.new('char[?]', maxlen) local rv = ffi.C.read(conn.fd, buf, maxlen) if rv < 0 then syserr("socket read") end return ffi.string(buf, rv) end -- Write up to a maximum number of bytes function connmethods.write(conn, buf) local rv = ffi.C.write(conn.fd, buf, #buf) if rv < 0 then syserr("socket write") end return rv end -- Close a socket function connmethods.close(conn) if 0 ~= ffi.C.close(conn.fd) then syserr("socket close") end end -- Metatable local connmeta = {__index = connmethods} -- Wrap a Unix numeric file descriptor function salisten.conn(fd) return setmetatable({fd=fd}, connmeta) end -- Open a TCP socket not yet bound to a port function salisten.tcp_socket() return ffi.C.socket(ffi.C.AF_INET, ffi.C.SOCK_STREAM, 0) end -- Byte-swap port number from little-endian -- into network byte order local function htons(ushort) return bit.rshift(ushort, 8) + bit.band(ushort, 255) * 256 end -- Bind a socket to a port and optional IP address -- (which must be an integer in network byte order) function salisten.bind_ip_port(sockfd, port, nbo) if nbo == nil then nbo = 0 end local sain = ffi.new("struct sockaddr_in") sain.sin_family = ffi.C.AF_INET sain.sin_port = htons(port) sain.sin_addr = {nbo} local sa = ffi.cast("struct sockaddr*", sain) if 0 ~= ffi.C.bind(sockfd, sa, 16) then syserr("bind") end end -- Set SO_REUSEADDR on a socket -- so you don’t get “Address already in use” if -- you restart. function salisten.set_reuse(sockfd) local optval = ffi.new("uint32_t[1]") optval[0] = 1 if 0 ~= ffi.C.setsockopt(sockfd, ffi.C.SOL_SOCKET, ffi.C.SO_REUSEADDR, optval, 4) then syserr("SO_REUSEADDR") end end -- Make a socket listen function salisten.listen(s, backlog) if backlog == nil then backlog = 64 end if 0 ~= ffi.C.listen(s, backlog) then syserr("listen") end end -- Methods and metatable for server sockets local servermethods = {} local servermeta = {__index=servermethods} -- Main entry point: listen on a port, return a -- server socket function salisten.server(port, nbo) local s = salisten.tcp_socket() salisten.set_reuse(s) salisten.bind_ip_port(s, port, nbo) salisten.listen(s) return setmetatable({fd=s}, servermeta) end -- Main method for server sockets: accept a conn, -- return a connection socket function servermethods.accept(sock) local fd = ffi.C.accept(sock.fd, nil, nil) if -1 == fd then syserr("accept") end return salisten.conn(fd) end -- Closing a server socket is the same as closing -- a conn servermethods.close = connmethods.close function salisten.demo_greet_server(port) local s = salisten.server(port) print('Listening on '..port) while true do local c = s:accept() print('Got a connection') c:write("hi, who are you? ") local name = c:read(80) c:write("hello, " .. name .. "\n") c:close() print('Done') end end -- Are we being invoked from the command line or -- required as a module? XXX kind of crappy local modname = ... if modname ~= 'salisten' then salisten.demo_greet_server(53053) end -- If we’re being required as a module: return salisten