作者:shihuaping0918@163.com,轉(zhuǎn)載請注明作者
httpc.lua和httpd.lua提供的功能比較簡陋土铺,函數(shù)也比較少丢早,代碼量比較少,一百多行坯辩。在對http協(xié)議有一定認(rèn)識的前提下馁龟,分析這兩個文件的代碼是比較簡單的。
httpc.lua是http客戶端代碼漆魔,支持get/post請求屁柏,發(fā)送完請求以后等待回應(yīng)。并解析回應(yīng)包有送。
local skynet = require "skynet"
local socket = require "http.sockethelper"
local url = require "http.url"
local internal = require "http.internal"
local dns = require "skynet.dns"
local string = string
local table = table
local httpc = {}
--發(fā)送請求并等待回應(yīng)
local function request(fd, method, host, url, recvheader, header, content)
local read = socket.readfunc(fd)
local write = socket.writefunc(fd)
local header_content = ""
if header then
if not header.host then
header.host = host
end
for k,v in pairs(header) do --http頭組成字符串
header_content = string.format("%s%s:%s\r\n", header_content, k, v)
end
else
header_content = string.format("host:%s\r\n",host)
end
if content then --有消息體
local data = string.format("%s %s HTTP/1.1\r\n%scontent-length:%d\r\n\r\n", method, url, header_content, #content)
--content-length為消息體長度
write(data)
write(content)
else --無消息體
local request_header = string.format("%s %s HTTP/1.1\r\n%scontent-length:0\r\n\r\n", method, url, header_content)
--content-length為消息體長度淌喻,沒有消息體就為0
write(request_header)
end
--等待回應(yīng)
local tmpline = {}
local body = internal.recvheader(read, tmpline, "")
if not body then
error(socket.socket_error)
end
--取出狀態(tài)碼,200是ok
local statusline = tmpline[1]
local code, info = statusline:match "HTTP/[%d%.]+%s+([%d]+)%s+(.*)$"
code = assert(tonumber(code))
--取消息頭
local header = internal.parseheader(tmpline,2,recvheader or {})
if not header then
error("Invalid HTTP response header")
end
--取content-length
local length = header["content-length"]
if length then
length = tonumber(length)
end
--取消息體編碼方式
local mode = header["transfer-encoding"]
if mode then
if mode ~= "identity" and mode ~= "chunked" then
error ("Unsupport transfer-encoding")
end
end
--讀取消息體
if mode == "chunked" then
body, header = internal.recvchunkedbody(read, nil, header, body)
if not body then
error("Invalid response body")
end
else
-- identity mode
if length then
if #body >= length then
body = body:sub(1,length)
else
local padding = read(length - #body)
body = body .. padding
end
else
-- no content-length, read all
body = body .. socket.readall(fd)
end
end
return code, body
end
local async_dns
function httpc.dns(server,port)
async_dns = true
dns.server(server,port)
end
function httpc.request(method, host, url, recvheader, header, content)
local timeout = httpc.timeout -- get httpc.timeout before any blocked api
local hostname, port = host:match"([^:]+):?(%d*)$"
if port == "" then --默認(rèn)端口80
port = 80
else
port = tonumber(port)
end
--如果是域名雀摘,而不是ip
if async_dns and not hostname:match(".*%d+$") then
hostname = dns.resolve(hostname)
end
--連接服務(wù)器裸删,如果timeout>0,就是異步等待
local fd = socket.connect(hostname, port, timeout)
local finish
if timeout then
skynet.timeout(timeout, function()
if not finish then
socket.shutdown(fd) -- shutdown the socket fd, need close later.
end
end)
end
--調(diào)用上面定義的request函數(shù)阵赠,在保護模式下進行
local ok , statuscode, body = pcall(request, fd,method, host, url, recvheader, header, content)
finish = true
socket.close(fd)
if ok then
return statuscode, body
else
error(statuscode)
end
end
--get方法
function httpc.get(...)
return httpc.request("GET", ...)
end
--轉(zhuǎn)換為百分號表示
local function escape(s)
return (string.gsub(s, "([^A-Za-z0-9_])", function(c)
return string.format("%%%02X", string.byte(c))
end))
end
--post方法
function httpc.post(host, url, form, recvheader)
local header = {
["content-type"] = "application/x-www-form-urlencoded"
}
local body = {}
for k,v in pairs(form) do
table.insert(body, string.format("%s=%s",escape(k),escape(v)))
end
return httpc.request("POST", host, url, recvheader, header, table.concat(body , "&"))
end
return httpc
httpd.lua涯塔,功能只有讀取請求,解析請求清蚀。發(fā)送回應(yīng)匕荸。
local internal = require "http.internal"
local table = table
local string = string
local type = type
local httpd = {}
--錯誤狀態(tài)碼定義
local http_status_msg = {
[100] = "Continue",
[101] = "Switching Protocols",
[200] = "OK",
[201] = "Created",
[202] = "Accepted",
[203] = "Non-Authoritative Information",
[204] = "No Content",
[205] = "Reset Content",
[206] = "Partial Content",
[300] = "Multiple Choices",
[301] = "Moved Permanently",
[302] = "Found",
[303] = "See Other",
[304] = "Not Modified",
[305] = "Use Proxy",
[307] = "Temporary Redirect",
[400] = "Bad Request",
[401] = "Unauthorized",
[402] = "Payment Required",
[403] = "Forbidden",
[404] = "Not Found",
[405] = "Method Not Allowed",
[406] = "Not Acceptable",
[407] = "Proxy Authentication Required",
[408] = "Request Time-out",
[409] = "Conflict",
[410] = "Gone",
[411] = "Length Required",
[412] = "Precondition Failed",
[413] = "Request Entity Too Large",
[414] = "Request-URI Too Large",
[415] = "Unsupported Media Type",
[416] = "Requested range not satisfiable",
[417] = "Expectation Failed",
[500] = "Internal Server Error",
[501] = "Not Implemented",
[502] = "Bad Gateway",
[503] = "Service Unavailable",
[504] = "Gateway Time-out",
[505] = "HTTP Version not supported",
}
--讀請求
local function readall(readbytes, bodylimit)
local tmpline = {}
local body = internal.recvheader(readbytes, tmpline, "")
if not body then
return 413 -- Request Entity Too Large
end
local request = assert(tmpline[1])
--請求url/method,http版本號,start line對應(yīng)的內(nèi)容
local method, url, httpver = request:match "^(%a+)%s+(.-)%s+HTTP/([%d%.]+)$"
assert(method and url and httpver)
httpver = assert(tonumber(httpver))
if httpver < 1.0 or httpver > 1.1 then --http版本錯誤
return 505 -- HTTP Version not supported
end
local header = internal.parseheader(tmpline,2,{})
if not header then
return 400 -- Bad request
end
local length = header["content-length"] --消息體長度枷邪,所有的field name被轉(zhuǎn)成小寫了
if length then
length = tonumber(length)
end
local mode = header["transfer-encoding"] --消息體編碼格式
if mode then
if mode ~= "identity" and mode ~= "chunked" then
return 501 -- Not Implemented
end
end
if mode == "chunked" then --chunked方式
body, header = internal.recvchunkedbody(readbytes, bodylimit, header, body)
if not body then
return 413
end
else
-- identity mode
if length then
if bodylimit and length > bodylimit then
return 413
end
if #body >= length then
body = body:sub(1,length)
else
local padding = readbytes(length - #body) --讀指定的長度
body = body .. padding
end
end
end
return 200, url, method, header, body
end
--讀取http請求
function httpd.read_request(...)
local ok, code, url, method, header, body = pcall(readall, ...)
if ok then
return code, url, method, header, body
else
return nil, code
end
end
--發(fā)應(yīng)答包
local function writeall(writefunc, statuscode, bodyfunc, header)
--http start line
local statusline = string.format("HTTP/1.1 %03d %s\r\n", statuscode, http_status_msg[statuscode] or "")
writefunc(statusline)
if header then --發(fā)http頭
for k,v in pairs(header) do
if type(v) == "table" then --表中表榛搔,或者表中key對應(yīng)的是數(shù)組
for _,v in ipairs(v) do
writefunc(string.format("%s: %s\r\n", k,v))
end
else
writefunc(string.format("%s: %s\r\n", k,v))
end
end
end
local t = type(bodyfunc) --wtf,這個名字取得很誤導(dǎo)
if t == "string" then
writefunc(string.format("content-length: %d\r\n\r\n", #bodyfunc)) --消息體長度
writefunc(bodyfunc) --bodyfunc是字符串啊,所以說名字取得很誤導(dǎo)人
elseif t == "function" then
writefunc("transfer-encoding: chunked\r\n")
while true do
local s = bodyfunc() --取消息體的一部分践惑,應(yīng)該是個generator才對
if s then
if s ~= "" then
writefunc(string.format("\r\n%x\r\n", #s)) --chunk size
writefunc(s) --chunk data
end
else
writefunc("\r\n0\r\n\r\n") --last chunk
break
end
end
else
assert(t == "nil")
writefunc("\r\n")
end
end
function httpd.write_response(...)
return pcall(writeall, ...)
end
return httpd