nimでTCPからHTTPサーバー建ててみた！

# Hello!
こんにちは！
あるときnimは速くてメモリ効率いいと聞いて使い始めた素人です

# [nim](https://nim-lang.org/)ってなんやねん！
- コンパイラ言語
- Python、Ada、Modulaみたいなコンセプト
- 速い！
- メモリ管理がすごい！
- C、C++、JavaScriptにコンパイルできる
- proc便利！
- 効率的で表現豊かで優雅 (by wikipedia)

って感じらしい

# きっかけ
いつも通りYoutubeを見ていた。センチメンタルな気持ちで見ていたわけでは無い。

> How I Made HTTP From Scratch / <img width="12" src="https://cdn-icons-png.flaticon.com/512/484/484633.png"> ゼロからHTTPを作った方法
> https://www.youtube.com/watch?v=4IXJE8sRM-A

という動画が流れてきた。
HTTPはTCPで構文に従ってテキスト転送してるだけって知って(RFCに厳密に従って作ったりしない限り)意外と簡単そうだ！ということで作ることにした

# とりあえず作る
## 全体像
最終的な全体像としては
```nim
import HTTPserver
import std/[
    tables,
    net
]

proc index(req: Request): Response =
    return Response(status: 200, body: "<h1>Hello World!</h1>", headers: {"Content-Type": "text/html"}.toTable)

var server = HTTPserver.create(Port(12345))
server.register("GET", "/index.html", index)
# server.register("GET", "/*", notfound)
server.run()
```
的な感じにしたいね(URLの正規表現対応もしたいけどめんどくさいからやってない)

## とりあえずぱぱっとdirtyなcodeを書いてイメージをつかむ
### まずはTCPサーバーを建てる。
std/netでできるらしい
```nim
let socket = newSocket()

socket.bindAddr(Port(7781))
socket.listen()

echo &"listening at 127.0.0.1:7781"

var client: Socket
var address = ""
while true:
    socket.acceptAddr(client, address)
    echo &"Client connected from: {address}"
```
ということで次、データを受信する

### HTTPリクエストのセマンティクス
HTTPリクエストのセマンティクスは
```
METHOD URI VERSION
HEADER

BODY
```
って感じで送られるらしくHEADERの末尾には`\r\n\r\n`がついてくるらしい

### recv系との格闘

```nim
var data: string
let connState = client.recv(data, 1024)
```
こんなふうにすればできるかなって実行してみたが、第二引数の`size: int`に届くまで値が返されないらしい

ということで次に見つけたのが`recvLine`行ごとに取得できるみたい
しかしこれを使うとリクエストのbodyの最後の行が取得できなかった
困り果てた挙句asynchttpserverというライブラリが標準のライブラリのソースを読んで作ってみることにする
https://github.com/nim-lang/Nim/blob/db9d8003b074cb6d150f7ece467f29006ccefde7/lib/pure/asynchttpserver.nim#L197-L202
```nim:asynchttpserver.nim
197  # We should skip at least one empty line before the request
198  # https://tools.ietf.org/html/rfc7230#section-3.5
199  for i in 0..1:
200    lineFut.mget().setLen(0)
201    lineFut.clean()
202    await client.recvLineInto(lineFut, maxLength = maxLine) # TODO: Timeouts.
```
asyncnetの`recvLineInto`を使ってるみたい
でもrecvLine使えなかったぞ？ということで`recvLineInto`も見てみる
https://github.com/nim-lang/Nim/blob/db9d8003b074cb6d150f7ece467f29006ccefde7/lib/pure/asyncnet.nim#L582-L601
```nim:asyncnet.nim
582    while true:
583      c = await recv(socket, 1, flags)
584      if c.len == 0:
585        resString.mget.setLen(0)
586        resString.complete()
587        return
588      if c == "\r":
589        c = await recv(socket, 1, flags) # Skip \L
590        assert c == "\L"
591        addNLIfEmpty()
592        resString.complete()
593        return
594      elif c == "\L":
595        addNLIfEmpty()
596        resString.complete()
597        return
598      resString.mget.add c
599
600
601      # Verify that this isn't a DOS attack: #3847.
602      if resString.mget.len > maxLength: break
```
なるほど1文字ずつ読んでるのか
ということで実装する

### やっとできた

```nim
import asyncdispatch
import std/[
    net,
    strformat,
    strutils,
    tables,
    options
]

proc ClientConnection(client:Socket) {.async.}=
    var stack: string
    while true:
        var data: string
        let connState = client.recv(data, 1)
        if connState == 0:
            client.close()
            return
        stack &= data

        if data == "":
            client.close()
            echo "Connection closed"
            return

        if "\r\n\r\n" in stack:
            let headerContent = stack.split("\r\n", 1)[1].split("\r\n\r\n", 1)[0].toLower
            if ("content-length: " in headerContent and headerContent.split("content-length: ")[1].split("\r\n")[0].parseInt == stack.split("\r\n\r\n", 1)[1].len) or "content-length: " notin headerContent:
                echo [stack]

var client: Socket
var address = ""
while true:
    socket.acceptAddr(client, address)
    echo &"Client connected from: {address}"
    discard ClientConnection(client)
```
こんな感じでデータの受け取り部分が完成
(
書いてる途中に気づいたけど
`recv(body, headerContent.split("content-length: ")[1].split("\r\n")[0].parseInt, 10)`
とかにしてもよかったな timeoutも指定できるし
)

次に受け取ったデータをパースする部分をつくる
とりあえずいっぱいsplit
```nim
type
    Request = object
        reqMethod: string
        path: string
        headers: Table[string, string]
        body: string

proc `?`(a:auto, b:tuple): auto =
    if bool(a):
        return b[0]
    else:
        return b[1]

proc readRequest(data: string): Request =
    let d = data.split("\r\n\r\n", 1)

    let control = d[0].split("\r\n", 1)[0]
    let control_data = control.split(" ")
    if control_data.len == 3:
        result.reqMethod = control_data[0]
        result.path = control_data[1]
    else:
        result.reqMethod = ""
        result.path = ""

    let headers = d[0].split("\r\n", 1)[1]
    for line in headers.splitlines():
        result.headers[line.split(": ", 1)[0]] = line.split(": ", 1)[1]

    result.body = d.len == 2 ? (d[1], "")
```
した
### レスポンスの送信
レスポンスの送信は
```nim
client.send("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 20\r\n\r\n<h1>Hello World</h1>")
```
でできた
レスポンスのセマンティクスもリクエストのと同じような感じで
```
VERSION STATUS STATUS_MSG
HEADER

BODY
```
の形式で送ればいい

# ライブラリっぽくする(完成)
あとはいい感じにライブラリっぽくして使えるようにする
```nim:HTTPserver.nim
import std/[
    net,
    strformat,
    strutils,
    tables
]
import asyncdispatch


proc `?`(a:auto, b:tuple): auto =
    if bool(a):
        return b[0]
    else:
        return b[1]


type
    Request* = object
        reqMethod: string
        path: string
        headers: Table[string, string]
        body: string
    Response* = object
        status*: int16
        statusText*: string
        headers*: Table[string, string]
        body*: string
    Handler = proc(req: Request): Response
    HTTPServer = object
        socket: Socket
        endpoints: Table[string, Table[string, Handler]]

proc readRequest(data: string): Request =
    let d = data.split("\r\n\r\n", 1)

    let control = d[0].split("\r\n", 1)[0]
    let control_data = control.split(" ")
    if control_data.len == 3:
        result.reqMethod = control_data[0]
        result.path = control_data[1]
    else:
        result.reqMethod = ""
        result.path = ""

    let headers = d[0].split("\r\n", 1)[1]
    for line in headers.splitlines():
        result.headers[line.split(": ", 1)[0]] = line.split(": ", 1)[1]

    result.body = d.len == 2 ? (d[1], "")

proc parseResponse(data: Response): string =
    result = &"HTTP/1.1 {data.status}" & (data.statusText=="" ? ("", " ")) & data.statusText & "\r\n"
    for k, v in data.headers.pairs:
        result &= &"{k}: {v}\r\n"
    if data.body != "":
        result &= &"Content-Length: {data.body.len}\r\n\r\n"
        result &= data.body
    else:
        result &= "\r\n"

proc ClientConnection(server: HTTPServer, client: Socket) {.async.}=
    var stack: string
    while true:
        var data: string
        discard client.recv(data, 1)
        stack &= data

        if data == "":
            client.close()
            echo "Connection aborted"
            break

        if "\r\n\r\n" in stack:
            let headerContent = stack.split("\r\n", 1)[1].split("\r\n\r\n", 1)[0].toLower
            if ("content-length: " in headerContent and headerContent.split("content-length: ")[1].split("\r\n")[0].parseInt == stack.split("\r\n\r\n", 1)[1].len) or "content-length: " notin headerContent:
                let request = readRequest(stack)
                if request.path notin server.endpoints:
                    echo &"No endpoint found ({request.path})"
                    client.send(parseResponse(Response(status:404)))
                else:
                    if request.reqMethod notin server.endpoints[request.path]:
                        var allowedMethods = ""
                        for k in server.endpoints[request.path].keys:
                            allowedMethods &= k & ", "
                        allowedMethods.removeSuffix(", ")
                        client.send(parseResponse(Response(status:405, statusText: "Method Not Allowed", headers: {"Allow": allowedMethods}.toTable)))
                    else:
                        let response = server.endpoints[request.path][request.reqMethod](request)
                        client.send(parseResponse(response))
                stack = ""
                client.close()
                return

proc create*(port: Port, address: string=""): HTTPServer =
    result.socket = newSocket()
    result.socket.bindAddr(port, address)

proc run*(server: var HTTPServer) =
    server.socket.listen()
    echo "Start server"
    var client: Socket
    var address = ""
    while true:
        server.socket.acceptAddr(client, address)
        echo &"Client connected: {address}"
        discard ClientConnection(server, client)

proc register*(server: var HTTPServer, httpMethod:string, path: string, callback: Handler) =
    if path notin server.endpoints:
        server.endpoints[path] = initTable[string, Handler]()
    server.endpoints[path][httpMethod] = callback
```

```nim:server.nim
import HTTPserver
import std/[
    tables,
    net
]

proc CreateHTMLResponse(html:string): Response =
    return Response(status: 200, body: html, headers: {"Content-Type": "text/html"}.toTable)


proc index(req: Request): Response =
    return Response(status: 200, body: "<h1>Hello World!</h1>", headers: {"Content-Type": "text/html"}.toTable)

var server = HTTPserver.create(Port(8848))
server.register("GET", "/index.html", index)
server.register("GET", "/hello", proc (req:Request): Response = Response(status: 200, body: "Hello!")))
server.register("POST", "/api/v1/users", proc (req:Request): Response = Response(status: 200, body: """["kaggle", "dii", "norm", "coline"]""", headers: {"Content-Type": "application/json"}.toTable))
server.run()
```

# お し ま い
ということでnimでHTTPサーバーを建ててみました！
nim始めたばっかなのでもっとこうしたら効率的だったりパフォーマンスが上がったりとかまだまだですね
初めて記事書いたので結構読みにくかったりいろいろ違和感感じるとこ多いと思いますがどうしようか？
個人的には自分の知識が浅く詳しく書けなかったのが悔しいですね

あとこれ非同期呼び出ししてやってるけどスレッド毎回立ててもいいのかな？
今回のgithub: https://github.com/wew-ptr/nimmyttp