この記事では、最小の HTTP サーバの実装を紹介しようと思います。この実装は私にとって、改良を基本としたプログラミングを主張するための良い機会にもなりました。
繰り返しになりますが、多くの人々と同様、私とってプログラミングにおける最も挑戦的な課題は、実装されたシステム固有の複雑性を上手く制御することです。これは他の工学でも同様でしょう。皮肉にも、設計をシンプルに保つことは、時にとてつもなく困難です。この困難を軽減させるために、プログラマは ATS の抽象型を使うことができると思います。
はじめに、次のような単純化した抽象サーバの実装を見てみましょう:
// extern fun myserver (): void extern fun myserver_init (): void extern fun myserver_loop (): void (* ****** ****** *) implement myserver () = { // val () = myserver_init () val () = myserver_loop () // } (* end of [myserver] *) (* ****** ****** *) implement myserver_init () = { // // HX: it is a dummy for now // val () = println! ("myserver_init: start") val () = println! ("myserver_init: finish") // } (* end of [myserver_init] *) (* ****** ****** *) abstype request (* ****** ****** *) // extern fun myserver_waitfor_request (): request extern fun myserver_process_request (request): void // (* ****** ****** *) implement myserver_loop () = { // val req = myserver_waitfor_request () // val () = myserver_process_request (req) // val () = myserver_loop ((*void*)) // } (* end of [myserver_loop] *) //
基本的に [myserver] は1つのサーバを実装しています; つまり [myserver_init] 呼び出しでなんらかの初期化を行ない、その後 [myserver_loop] 呼び出しでリクエストを扱うループを開始します。関数 [myserver_waitfor_request] はリクエストが到着するまでブロックし、関数 [myserver_process_request] は与えられたリクエストを処理することになります。
実装すべき3つの関数は、サーバの実行順に、[myserver_init], [myserver_waitfor_request], [myserver_waitfor_process] ということになります。BSD ソケットに馴染みのあるプログラマにとって、次のコードは受け入れやすいものでしょう:
// %{^ int theSockID = -1; %} // end of [%{^] // (* ****** ****** *) #define MYPORT 8888 (* ****** ****** *) implement myserver_init () = { // val inport = in_port_nbo(MYPORT) val inaddr = in_addr_hbo2nbo (INADDR_ANY) // var servaddr : sockaddr_in_struct val ((*void*)) = sockaddr_in_init (servaddr, AF_INET, inaddr, inport) // val sockfd = $extfcall ( int, "socket", AF_INET, SOCK_STREAM, 0 ) (* end of [val] *) val ((*void*)) = assertloc (sockfd >= 0) // extvar "theSockID" = sockfd // val () = $extfcall ( void, "atslib_bind_exn", sockfd, addr@servaddr, socklen_in ) (* end of [val] *) // val () = $extfcall(void, "atslib_listen_exn", sockfd, 5(*LISTENQSZ*)) // } (* end of [myserver_init] *) //
本質的に [myserver_init] は、どこからでも接続を受け付けるサーバ側ソケットを生成し、そのソケットをリッスンします。生成したソケットのファイルディスクリプタはグローバル変数 [theSockID] に保管されていることに、注意してください。関数 [atslib_bind_exn] は [bind] を呼び出し、[bind] 呼び出しがエラーを返したらプログラムは終了し、そうでない場合にはこの関数は返ります。同様に、関数 [atslib_listen_exn] は [listen] を呼び出し、[listen] 呼び出しがエラーを返したらプログラムは終了し、そうでない場合にはこの関数は返ります。
関数 [myserver_waitfor_request] は次のように実装できます:
// implement myserver_waitfor_request ((*void*)) = let // val fd = $extval(int, "theSockID") val fd2 = $extfcall(int, "accept", fd, 0(*addr*), 0(*addrlen*)) // in $UN.cast{request}(fd2) end // end of [myserver_waitfor_request] //
[accept] 呼び出しは、サーバとクライアントの接続が確立されるまでブロックします。 [accept] は、クライアントと通信するためのソケットのファイルディスクリプタを返します。
関数 [myserver_process_request] は次のように実装できます:
// #define BUFSZ 1024 #define BUFSZ2 1280 // (* ****** ****** *) val theRespFmt = "\ HTTP/1.0 200 OK\r\n\ Content-type: text/html\r\n\r\n\ <!DOCTYPE html> <html> <head> <meta charset=\"UTF-8\"> <meta http-equiv=\"Content-Type\" content=\"text/html\"> </head> <body> <h1> Hello from myserver! </h1> <pre> %s </pre> <pre> <u>The time stamp</u>: <b>%s</b> </pre> </body> </html> " // end of [val] (* ****** ****** *) %{^ typedef char *charptr; %} // end of [%{^] abstype charptr = $extype"charptr" (* ****** ****** *) implement myserver_process_request (req) = let // val fd2 = $UN.cast{int}(req) // var buf = @[byte][BUFSZ]() var buf2 = @[byte][BUFSZ2]() // val bufp = addr@buf and bufp2 = addr@buf2 // val nread = $extfcall (ssize_t, "read", fd2, bufp, BUFSZ) // (* val () = println! ("myserver_process_request: nread = ", nread) *) // var time = time_get() val tmstr = $extfcall(charptr, "ctime", addr@time) // val () = if nread >= 0 then let val [n:int] n = $UN.cast{Size}(nread) val () = $UN.ptr0_set_at<char> (bufp, n, '\000') // val nbyte = $extfcall(int, "snprintf", bufp2, BUFSZ2, theRespFmt, bufp, tmstr) // end of [val] // val nwrit = $extfcall(ssize_t, "write", fd2, bufp2, min(nbyte, BUFSZ2)) // in // nothing end // end of [then] // // val err = $extfcall (int, "close", fd2) // in // nothing end // end of [myserver_process_request] //
[myserver_process_request] の実装は、クライアントから送信されたデータをバッファから読み込みます。そして、そのバッファの内容とタイムスタンプを含む HTML ページを生成し、そのページをクライアントに送信します。
この実装のコード全体は myserver.dats から入手できます。サーバをコンパイルするための Makefile もあります。ローカルにサーバを起動した後、リンク http://127.0.0.1:8888 をクリックすればテストできます。
ZMQ に興味がある読者には、ZMQ を使った上記と本質的に等価な HTTP サーバの実装 myserver2.dats もあります。