Hello!
こんにちは!
あるときnimは速くてメモリ効率いいと聞いて使い始めた素人です
nimってなんやねん!
- コンパイラ言語
- Python、Ada、Modulaみたいなコンセプト
- 速い!
- メモリ管理がすごい!
- C、C++、JavaScriptにコンパイルできる
- proc便利!
- 効率的で表現豊かで優雅 (by wikipedia)
って感じらしい
きっかけ
いつも通りYoutubeを見ていた。センチメンタルな気持ちで見ていたわけでは無い。
How I Made HTTP From Scratch / ゼロからHTTPを作った方法
https://www.youtube.com/watch?v=4IXJE8sRM-A
という動画が流れてきた。
HTTPはTCPで構文に従ってテキスト転送してるだけって知って(RFCに厳密に従って作ったりしない限り)意外と簡単そうだ!ということで作ることにした
とりあえず作る
全体像
最終的な全体像としては
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でできるらしい
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系との格闘
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
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
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文字ずつ読んでるのか
ということで実装する
やっとできた
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
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], "")
した
レスポンスの送信
レスポンスの送信は
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
の形式で送ればいい
ライブラリっぽくする(完成)
あとはいい感じにライブラリっぽくして使えるようにする
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
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