Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

为 Luci 编写 Docker Engine Api 过程记录 #5

Open
lisaac opened this issue Sep 22, 2019 · 0 comments
Open

为 Luci 编写 Docker Engine Api 过程记录 #5

lisaac opened this issue Sep 22, 2019 · 0 comments

Comments

@lisaac
Copy link
Owner

lisaac commented Sep 22, 2019

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant