Source

luaeio / eio.lua

Full commit
local libeio = require("libeio")

local http = {}
http.__index = http

--
-- Create new HTTP server
-- 
function libeio.http_server(port, handlers)
	local f = libeio.listen(port)
	if f < 0 then
		return nil, f
	end

	local s = {}
	s.fd = f
	s.fds = {}
	s.crs = {}
	s.handlers = handlers
	setmetatable(s, http)
	return s
end

--
-- Run server loop once and dispatch all events
--
function http:dispatch_once()
	local n
	local rfds, wfds = {}, {}
	-- add listening socket itself
	table.insert(rfds, self.fd)

	-- add other file descriptors to rfds/wfds
	for _, fd in pairs(self.fds) do
		if fd.r then table.insert(rfds, fd.fd) end
		if fd.w then table.insert(wfds, fd.fd) end
	end

	-- wait for the events
	n, rfds, wfds = libeio.select(rfds, wfds)
	if n < 0 then error("select(): "..n) end

	-- special case: read on listening socket (accept event)
	if rfds[self.fd] ~= nil then
		self:accept()
	end

	-- dispatch the rest of the events
	n = 0
	for fd, cr in pairs(self.crs) do
		self.fds[fd].can_read = (rfds[fd] ~= nil)
		self.fds[fd].can_write = (wfds[fd] ~= nil)
		coroutine.resume(cr)
		n = n + 1
		if coroutine.status(cr) == 'dead' then
			libeio.close(fd)
			self.crs[fd] = nil
			self.fds[fd] = nil
		end
	end
	return n
end

--
-- Dispatch HTTP clients in an endless loop
--
function http:serve()
	while true do 
		if self:dispatch_once() < 0 then
			break
		end
	end
end

local function http_parse_header(h)
	-- the first line is "METHOD URI VERSION"
	local header = {}
	local method, uri = h:match("^(%w+)%s+([^%s]+)")
	h = h:sub(h:find("\n")+1) -- skip 1st line

	for k,v in h:gmatch("([%w-]+)%s*:%s*(.-)\r\n") do
		header[k] = v
	end

	return method, uri, header
end

local function default_http_404_handler(r)
	r:write("HTTP 404 Not Found\r\n\r\n")
end

--
-- Process new HTTP connection
--
function http:accept()
	local fd = libeio.accept(self.fd)

	self.fds[fd] = {fd = fd}

	local read_header = function()
		local request = ""

		while request:find("\r\n\r\n") == nil do
			local n, buf = self:read(fd, 1)
			if n > 0 then 
				request = request..tostring(buf)
			elseif n == 0 then
				return -- connection was closed
			end
		end

		local m, u, h = http_parse_header(request)

		local r = {
			method = m,
			uri = u,
			header = h,
			read = function(r,n) return self:read(fd, n) end,
			write = function(r,s) return self:write(fd, s) end
		}

		local handler = default_http_404_handler
		local len = 0
		for uri, h in pairs(self.handlers) do
			-- check if URI starts with handler URI
			if r.uri:sub(1, uri:len()) == uri then
				if uri:len() > len then
					handler = h
					len = uri:len()
				end
			end
		end

		handler(r)
	end

	self.crs[fd] = coroutine.create(read_header)
end

--
-- Read from HTTP client socket in co-routine
--
function http:read(fd, size)
	if self.fds[fd].can_read then
		self.fds[fd].can_read = false
		self.fds[fd].r = false
		return libeio.read(fd, size)
	else
		self.fds[fd].r = true
		coroutine.yield()
		return -1, nil
	end
end

--
-- Write to the HTTP client socket in co-routine
--
function http:write(fd, buf)
	while true do
		if self.fds[fd].can_write then
			self.fds[fd].can_write = false
			self.fds[fd].w = false
			print("writing"..#buf)
			local n = libeio.write(fd, buf)
			if n < 0 then return n end
			if n == #buf then return 0 end
			buf = buf:sub(n+1)
		else
			print("write yield")
			self.fds[fd].w = true
			coroutine.yield()
		end
	end
end

return libeio