😸

Neovim + LuaでWEBサーバーを実装する

2023/12/30に公開

NeovimとLuaの勉強がてら、簡単なHTTPサーバーを実装してみました。

「WEBアプリケーションのコードをNeovimで編集する」ではありません。「俺(Neovim)自身がWEBサーバーになることだ」ってやつです。

NVIM v0.9.4

TCP echo

TCPで送られてきたメッセージをそのまま返すシンプルな例です。:help tcp-serverにあるのと同じものです。

ここではLuvrefモジュールを使用しています。これは、LibUVという非同期I/Oをマルチプラットフォームで実現するためのライブラリのLuaラッパーです。LibUVは他にもNode.jsやJuliaでも使用されています。

バージョンによってvim.loopvim.uvだったりするので:help luvref.txtで確認してください。

tcp_echo_server.lua
local uv = vim.loop
-- local uv = vim.uv

local function create_server(host, port, on_connect)
  local server = uv.new_tcp()
  server:bind(host, port)
  server:listen(128, function(err)
    assert(not err, err)  -- Check for errors.
    local sock = uv.new_tcp()
    server:accept(sock)  -- Accept client connection.
    on_connect(sock)  -- Start reading messages.
  end)
  return server
end

local server = create_server('0.0.0.0', 0, function(sock)
  sock:read_start(function(err, chunk)
    assert(not err, err)  -- Check for errors.

    sock:write(chunk)  -- Echo received messages to the channel.
    sock:close()  -- Always close handles to avoid leaks.
  end)
end)

print('TCP echo-server listening on port: ' .. server:getsockname().port)

実行

Neovim中で:luafileで実行して、出力されたポート番号にncなどで接続します。ポートはランダムです。

:luafile tcp_echo_server.lua
TCP echo-server listening on port: 36795
$ nc 0.0.0.0 36795
hello
hello

REST API

REST APIでやりとりできるように後半のserver変数を少しいじりました。

リクエストの先頭(例:GET /hello HTTP/1.1)を解析し、メソッドやURIを元にルーティングしています。今回は簡単な例なのでIF文でゴリ押ししました。

rest_api_server.lua
local uv = vim.loop
-- local uv = vim.uv

local function create_server(host, port, on_connect)
  local server = uv.new_tcp()
  server:bind(host, port)
  server:listen(128, function(err)
    assert(not err, err)  -- Check for errors.
    local sock = uv.new_tcp()
    server:accept(sock)  -- Accept client connection.
    on_connect(sock)  -- Start reading messages.
  end)
  return server
end

local server = create_server('0.0.0.0', 0, function(sock)
  sock:read_start(function(err, chunk)
    assert(not err, err)  -- Check for errors.

    local method, path = string.match(chunk, '(%a+)%s+(/%S+)')
    local response  = ""

    if method and path then
      if method == "GET" and path == "/hello" then
        response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n" .. [[ { "origin": "175.177.49.33" } ]]
      else
        response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n" .. chunk
      end
    else
      response = "HTTP/1.1 200 Ok\r\nContent-Type: text/plain\r\n\r\n" .. chunk
    end

    sock:write(response)
    sock:close()
  end)
end)

print('REST API server listening on port: ' .. server:getsockname().port)

実行

同じように:luafileで実行し、curlやブラウザなどでアクセスしてください。

:luafile rest_api_server.lua
REST API echo-server listening on port: 38625
$ curl 0.0.0.0:38625
GET / HTTP/1.1
Host: 0.0.0.0:38625
User-Agent: curl/8.5.0
Accept: */*

$ curl 0.0.0.0:38625/hello
 { "origin": "175.177.49.33" }

ここまでできたら本格的なWEBサーバーも構築できるような気がしてきませんか?

なお、「denops使えばいいじゃん」は禁句です。

参考

https://libuv.org/

https://zenn.dev/mmomm/articles/ff83eb49a7b642#libuv

https://neovim.io/doc/user/lua.html#tcp-server

https://neovim.io/doc/user/luvref.html

https://github.com/libuv/libuv

https://developer.mozilla.org/ja/docs/Web/HTTP/Messages

Discussion