http协议的服务分为http服务端与http客户端,skynet服务作为http服务端的时候,可以像其他的web服务器一样,接收http请求并给与应答。skynet的服务作为http客户端的时候,可以通过http协议像远端发送请求并等待得到应答。
18.1http服务端
skynet 从 v0.5.0 开始提供了简单的 http 服务器的支持。skynet.httpd
是一个独立于 skynet 的,用于 http 协议解析的库,它本身依赖 socket api 的注入。使用它,你需要把读写 socket 的 API 封装好,注入到里面就可以工作。
skynet.sockethelper 模块将 skynet 的 Socket API 封装成 skynet.httpd 可以接受的形式:阻塞读写指定的字节数、网络错误以异常形式抛出。
下面是一个简单的范例:testhttpd.lua
local skynet = require "skynet"
local socket = require "skynet.socket"
skynet.start(function()
local agent = {}
for i= 1, 20 do
-- 启动 20 个代理服务用于处理 http 请求
agent[i] = skynet.newservice("testhttpagent")
end
local balance = 1
-- 监听一个 web 端口
local id = socket.listen("0.0.0.0", 8001)
socket.start(id , function(id, addr)
-- 当一个 http 请求到达的时候, 把 socket id 分发到事先准备好的代理中去处理。
skynet.error(string.format("%s connected, pass it to agent :%08x", addr, agent[balance]))
skynet.send(agent[balance], "lua", id)
balance = balance + 1
if balance > #agent then
balance = 1
end
end)
end)
http代理服务代码:testagenthttp.lua
-- examples/simpleweb.lua
local skynet = require "skynet"
local socket = require "skynet.socket"
local httpd = require "http.httpd"
local sockethelper = require "http.sockethelper"
local urllib = require "http.url"
local string = string
local function response(id, ...)
local ok, err = httpd.write_response(sockethelper.writefunc(id), ...)
if not ok then
-- if err == sockethelper.socket_error , that means socket closed.
skynet.error(string.format("fd = %d, %s", id, err))
end
end
skynet.start(function()
skynet.dispatch("lua", function (_,_,id)
socket.start(id) -- 开始接收一个 socket
-- limit request body size to 8192 (you can pass nil to unlimit)
-- 一般的业务不需要处理大量上行数据,为了防止攻击,做了一个 8K 限制。这个限制可以去掉。
local code, url, method, header, body = httpd.read_request(sockethelper.readfunc(id), 8192)
if code then
if code ~= 200 then -- 如果协议解析有问题,就回应一个错误码 code 。
response(id, code)
else
-- 这是一个示范的回应过程,你可以根据你的实际需要,解析 url, method 和 header 做出回应。
if header.host then
skynet.error("header host", header.host)
end
local path, query = urllib.parse(url)
skynet.error(string.format("path: %s", path))
local color,text = "red", "hello"
if query then
local q = urllib.parse_query(query) --获取请求的参数
for k, v in pairs(q) do
skynet.error(string.format("query: %s= %s", k,v))
if(k == "color") then
color = v
elseif (k == "text") then
text = v
end
end
end
local resphtml = "<body bgcolor=\""..color.."\">"..text.."</body>\n" --返回一张网页
response(id, code, resphtml) --返回状态码200,并且跟上内容
end
else
-- 如果抛出的异常是 sockethelper.socket_error 表示是客户端网络断开了。
if url == sockethelper.socket_error then
skynet.error("socket closed")
else
skynet.error(url)
end
end
socket.close(id)
end)
end)
这个 httpd 模块最初是用于服务器内部管理,以及和其它平台对接。所以只提供了最简单的功能。如果是重度的业务要使用,可以考虑再其上做进一步的开发。
运行httpd服务,然后在浏览器地址栏上输入http://127.0.0.1:8001/?color=blue&text=abc
,httpd服务输出如下:
$ ./skynet examples/conf testhttpd [:01000010] LAUNCH snlua testhttpd [:01000012] LAUNCH snlua testhttpagent [:01000019] LAUNCH snlua testhttpagent [:0100001a] LAUNCH snlua testhttpagent [:0100001b] LAUNCH snlua testhttpagent [:0100001c] LAUNCH snlua testhttpagent [:0100001d] LAUNCH snlua testhttpagent [:0100001e] LAUNCH snlua testhttpagent [:0100001f] LAUNCH snlua testhttpagent [:01000020] LAUNCH snlua testhttpagent [:01000022] LAUNCH snlua testhttpagent [:01000029] LAUNCH snlua testhttpagent [:01000031] LAUNCH snlua testhttpagent [:01000033] LAUNCH snlua testhttpagent [:01000034] LAUNCH snlua testhttpagent [:01000035] LAUNCH snlua testhttpagent [:01000036] LAUNCH snlua testhttpagent [:01000037] LAUNCH snlua testhttpagent [:01000038] LAUNCH snlua testhttpagent [:01000039] LAUNCH snlua testhttpagent [:0100003a] LAUNCH snlua testhttpagent [:01000010] 127.0.0.1:50614 connected, pass it to agent :01000012 [:01000012] header host 127.0.0.1:8001 [:01000012] path: / [:01000012] query: color= blue [:01000012] query: text= abc
18.2 http客户端
skynet 提供了一个非常简单的 http 客户端模块。你可以用:
httpc.request(method, host, uri, recvheader, header, content)
来提交一个 http 请求,其中
method 是 "GET" "POST" 等。
host 为目标机的地址
uri 为请求的 URI
recvheader 可以是 nil 或一张空表,用于接收回应的 http 协议头。
header 是自定义的 http 请求头。注:如果 header 中没有给出 host ,那么将用前面的 host 参数自动补上。
content 为请求的内容。
它返回状态码和内容。如果网络出错,则抛出 error 。
httpc.dns(server, port)
可以用来设置一个异步查询 dns 的服务器地址。如果你不给出地址,那么将从 /etc/resolv.conf
查找地址。如果你没有调用它设置异步 dns 查询,那么 skynet 将在网络底层做同步查询。这很有可能阻塞住整个 skynet 的网络消息处理(不仅仅阻塞单个 skynet 服务)。
示例代码:httpclient.lua
local skynet = require "skynet"
local httpc = require "http.httpc"
local dns = require "skynet.dns"
local function main()
httpc.dns() -- set dns server
httpc.timeout = 100 -- set timeout 1 second
print("GET baidu.com")
local respheader = {}
local status, body = httpc.request("GET", "baidu.com", "/", respheader, { host = "baidu.com" })
--local status, body = httpc.get("baidu.com", "/", respheader, { host = "baidu.com" })
print("[header] =====>")
for k,v in pairs(respheader) do
print(k,v)
end
print("[body] =====>", status)
print(body)
end
skynet.start(function()
print(pcall(main))
skynet.exit()
end)
运行结果:
$ ./skynet examples/conf testhttpclient [:01000010] LAUNCH snlua testhttpclient GET baidu.com [header] =====> #百度返回的响应头部分 connection Keep-Alive date Fri, 09 Feb 2018 16:49:34 GMT last-modified Tue, 12 Jan 2010 13:48:00 GMT expires Sat, 10 Feb 2018 16:49:34 GMT cache-control max-age=86400 accept-ranges bytes content-type text/html content-length 81 etag "51-47cf7e6ee8400" server Apache [body] =====> 200 #状态码,以及响应体 <html> <meta http-equiv="refresh" content="0;url=http://www.baidu.com/"> </html> true #执行成功 [:01000010] KILL self
尝试连接一下上一节的http服务,修改testhttpclient.lua:
local skynet = require "skynet"
local httpc = require "http.httpc"
local dns = require "skynet.dns"
local function main()
httpc.dns() -- set dns server
httpc.timeout = 100 -- set timeout 1 second
print("GET 127.0.0.1:8001")
local respheader = {}
local status, body = httpc.request("GET", "127.0.0.1:8001", "/?color=blue&text=abc", respheader)
print("[header] =====>")
for k,v in pairs(respheader) do
print(k,v)
end
print("[body] =====>", status)
print(body)
end
skynet.start(function()
print(pcall(main))
skynet.exit()
end)
先运行testhttpd再运行testhttpclient:
$ ./skynet examples/conf testhttpd [:01000010] LAUNCH snlua testhttpd [:01000012] LAUNCH snlua testhttpagent [:01000019] LAUNCH snlua testhttpagent [:0100001a] LAUNCH snlua testhttpagent [:0100001b] LAUNCH snlua testhttpagent [:0100001c] LAUNCH snlua testhttpagent [:0100001d] LAUNCH snlua testhttpagent [:0100001e] LAUNCH snlua testhttpagent [:0100001f] LAUNCH snlua testhttpagent [:01000020] LAUNCH snlua testhttpagent [:01000022] LAUNCH snlua testhttpagent [:01000029] LAUNCH snlua testhttpagent [:01000031] LAUNCH snlua testhttpagent [:01000033] LAUNCH snlua testhttpagent [:01000034] LAUNCH snlua testhttpagent [:01000035] LAUNCH snlua testhttpagent [:01000036] LAUNCH snlua testhttpagent [:01000037] LAUNCH snlua testhttpagent [:01000038] LAUNCH snlua testhttpagent [:01000039] LAUNCH snlua testhttpagent [:0100003a] LAUNCH snlua testhttpagent testhttpclient [:0100003b] LAUNCH snlua testhttpclient GET 127.0.0.1:8001 [:01000010] 127.0.0.1:50646 connected, pass it to agent :01000012 [:01000012] header host 127.0.0.1:8001 #testhttpd服务获取GET请求 [:01000012] path: / [:01000012] query: color= blue [:01000012] query: text= abc [header] =====> #testhttpclient得到响应 content-length 32 [body] =====> 200 <body bgcolor="blue">abc</body> true [:0100003b] KILL self