blog
blog copied to clipboard
为 Luci 编写 Docker Engine Api 过程记录
HTTP ON UNIX SOCKET
Docker 采用 unix socket 方式进行通信,使用 luci 自带 nixio 库中的 socket
-- 新建 UNIX SOCKET
sock= nixio.socket("unix", "stream")
-- 连接 docker.sock
sock:connect("/var/run/docker.sock")
使用 HTTP 协议进行传输
-- 生成请求头 记得在 path 上使用 http.urlencode()
request="GET /containers/json HTTP/1.1\r\nHost: localhost\r\n\r\n"
-- 如果存在 body 需要加入Content-Length, 用 json.encode() 将 table 转 json 数据
-- 发送请求
sock:sendall(request)
接受数据, 并处理header
-- 接受返回数据
-- response, err_code, err_msg, response_f = sock:readall() -- 使用此方法必须在请求头中加`Connection: close`, 否则会一直等待
linesrc=sock:linesource() -- 读取 socket 将用 source http://w3.impa.br/~diego/software/luasocket/ltn12.html http://lua-users.org/wiki/FiltersSourcesAndSinks
-- handle response header
local line = linesrc()
if not line then
docker_socket:close()
return {code = 554}
end
local response = {code = 0, headers = {}, body = {}}
local p, code, msg = line:match('^([%w./]+) ([0-9]+) (.*)')
response.protocol = p
response.code = tonumber(code)
response.message = msg
line = linesrc()
while line and line ~= '' do
local key, val = line:match('^([%w-]+)%s?:%s?(.*)')
if key and key ~= 'Status' then
if type(response.headers[key]) == 'string' then
response.headers[key] = {response.headers[key], val}
elseif type(response.headers[key]) == 'table' then
response.headers[key][#response.headers[key] + 1] = val
else
response.headers[key] = val
end
end
line = linesrc()
end
-- 处理 response body
...
Docker返回的基本都是json数据,最后对 response body 进行处理,就可以拿到数据了。
Chunked transfer encoding
当然 Docker 返回的不都是json数据,在查询 containers logs,以及 Attach to a container 文档中标识返回的是 stream.
经过测试发现使用的是分块传输编码(Chunked transfer encoding), response.header
中有Transfer-Encoding = chunked
标记.
格式:
如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。
每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。
消息最后以CRLF结尾。
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0
这里弯路走了很多,后来发现 luci.httpclient 已经提供了 decoding 的方法:httpclient.chunksource(source,buffer)
,所以只需要:
local body_buffer = linesrc(true)
response.body = {}
if response.headers['Transfer-Encoding'] == 'chunked' then
local source = chunksource(docker_socket, body_buffer)
code = ltn12.pump.all(source, (ltn12.sink.table(response.body))) and response.code or 555
response.code = code
else
local body_source = ltn12.source.cat(ltn12.source.string(body_buffer), docker_socket:blocksource())
code = ltn12.pump.all(body_source, (ltn12.sink.table(response.body))) and response.code or 555
response.code = code
end
-- handle output
...
不过貌似http.client.chunksource
在解码Chunked transfer的时候存在一些bug,所以重新修复了一下:
local chunksource = function(sock, buffer)
buffer = buffer or ''
return function()
local output
local _, endp, count = buffer:find('^([0-9a-fA-F]+);?.-\r\n')
if not count then -- lua ^ only match start of stirng,not start of line
_, endp, count = buffer:find('\r\n([0-9a-fA-F]+);?.-\r\n')
end
while not count do
local newblock, code = sock:recv(1024)
if not newblock then
return nil, code
end
buffer = buffer .. newblock
_, endp, count = buffer:find('^([0-9a-fA-F]+);?.-\r\n')
if not count then
_, endp, count = buffer:find('\r\n([0-9a-fA-F]+);?.-\r\n')
end
end
count = tonumber(count, 16)
if not count then
return nil, -1, 'invalid encoding'
elseif count == 0 then -- finial
return nil
elseif count + 2 <= #buffer - endp then
output = buffer:sub(endp + 1, endp + count)
buffer = buffer:sub(endp + count + 3) -- don't forget handle buffer
return output
else
output = buffer:sub(endp + 1, endp + count)
buffer = ''
if count > #output then
local remain, code = sock:recvall(count - #output) --need read remaining
if not remain then
return nil, code
end
output = output .. remain
count, code = sock:recvall(2) --read \r\n
else
count, code = sock:recvall(count + 2 - #buffer + endp)
end
if not count then
return nil, code
end
return output
end
end
end
处理body
至此,已经得到了Docker Engine Api 回传的数据了,大多数api都是返回的json数据,转table后直接可以使用,stream格式的数据需要额外处理。 即,每个数据包的头部有8个字节,第一个自交表示stream的type,接下来3个为空,剩余4个字节组成一个int32的整型数据,表示这个数据包中有效数据的长度。剩余的即是数据。 Docker API Doc 介绍如下:
Stream format
When the TTY setting is disabled in POST /containers/create, the stream over the hijacked connected is multiplexed to separate out stdout and stderr. The stream consists of a series of frames, each containing a header and a payload.
The header contains the information which the stream writes (stdout or stderr). It also contains the size of the associated frame encoded in the last four bytes (uint32).
It is encoded on the first eight bytes like this:
header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
STREAM_TYPE can be:
0: stdin (is written on stdout)
1: stdout
2: stderr
SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian.
Following the header is the payload, which is the specified number of bytes of STREAM_TYPE.
实现方法:
function docker_stream_filter(buffer)
buffer = buffer or ""
if #buffer <8 then return "" end
local stream_type=((string.byte(buffer,1) == 1) and 'stdout') or ((string.byte(buffer,1) == 2) and 'stderr') or ((string.byte(buffer,1) == 0) and 'stdin') or 'stream_err'
local valid_length = tonumber(string.byte(buffer,5)) * 256 * 256 * 256 + tonumber(string.byte(buffer,6)) * 256 * 256 + tonumber(string.byte(buffer,7)) * 256 + tonumber(string.byte(buffer,8))
if valid_length > #buffer+8 then return "" end
return stream_type .. ': ' .. string.sub(buffer, 9, valid_length + 8)
end
stream_data={}
for i,v in ipairs(response.body) do
stream_data[#stream_data+1] = docker_stream_filter(response.body[i])
end