Effective ATS:
最小の HTTP サーバを実装する

この記事では、最小の 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 もあります。


この記事は Hongwei Xi によって書かれ、Japan ATS User Group によって翻訳されています。