なんとなく、時々ちょっとしたテストとかで使っているダミーのHTTPサーバ用のスクリプトをまとめたくなりまして。
で、せっかくなのでプラスアルファも入れて。
ホントに簡単なものなので、以下のスペックで。
- リクエストの内容を解析することは、必須としない
- 起動時にはポート番号を指定できる。しなかった場合は、8080で起動
- ヘッダ、ボディに何か出力する
- サーバの終了は、Ctrl-cなどで
- できれば並行処理可能に
ターゲットは、GroovyとClojureでJDK付属のものを使った版、Undertowを使った版を、あとはPerlで1本書きました。
実行結果は、いずれもこんな感じになります。
$ curl -i http://localhost:8080/test HTTP/1.1 200 OK Date: Sat, 12 Apr 2014 08:46:39 GMT Content-type: text/html; charset=UTF-8 X-dummy-header: Value Content-length: 80 <html> <header><title>タイトル</title></header> <body>本文</body> </html>
ヘッダの内容は、選んだ実装次第でけっこうブレますが…。
また、起動方法は各種ファイルの先頭付近にコメントとして書いておきました。
では、行ってみましょう。
最終的なコードは、こちらへ。
https://gist.github.com/kazuhira-r/10524638
JDK付属のHTTPサーバで実装
JDK 6から、HTTPサーバ用のクラスが付属しています。こちらを使っての実装。
APIリファレンスは、こちら
http://docs.oracle.com/javase/8/docs/jre/api/net/httpserver/spec/index.html
Groovy
まずは、お手軽Groovy。
simple-jdk-httpd.groovy
// 使い方: $ groovy simple-jdk-httpd.groovy [ポート番号(デフォルト8080)] import java.nio.charset.StandardCharsets import java.util.concurrent.Executors import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpServer def responseHandler = { exchange -> println("[${new Date()}] Accept: Client[$exchange.remoteAddress], Url[$exchange.requestURI]") def bodyText = """|<html> |<header><title>タイトル</title></header> |<body>本文</body> |</html> |""".stripMargin().getBytes(StandardCharsets.UTF_8) try { // 追加ヘッダは先に書くこと exchange.responseHeaders.with { add("Content-Type", "text/html; charset=UTF-8") add("X-Dummy-Header", "Value") } exchange.sendResponseHeaders(200, bodyText.size()) exchange.responseBody.withStream { it.write(bodyText) } } catch (e) { e.printStackTrace() } } def server = HttpServer.create(new InetSocketAddress(args.length > 0 ? args[0].toInteger() : 8080), 0) server.executor = Executors.newCachedThreadPool() server.createContext("/", responseHandler as HttpHandler) server.start() println("[${new Date()}] SimpleJdkHttpd Startup[${server.address}]")
ヘッダとボディの処理の分離が、ちょっとやり辛い感じ。そんなに困ることはありませんけどね。追加ライブラリは、特に不要です。
並行処理は、Executorに任せています。
Clojure
続いて、Clojureでリライト。
simple-jdk-httpd.clj
;; 使い方: $ lein exec simple-jdk-httpd.clj [ポート番号(デフォルト8080)] (require '[leiningen.exec :as exec]) (ns simple-jdk-httpd (:import (java.net InetSocketAddress) (java.nio.charset StandardCharsets) (java.util Date) (java.util.concurrent Executors) (com.sun.net.httpserver HttpHandler HttpServer))) (defn response-handler [exchange] (println (format "[%s] Accept: Client[%s], Url[%s]" (Date.) (. exchange getRemoteAddress) (. exchange getRequestURI))) (try (let [body-text (-> (reduce #(str %1 (System/lineSeparator) %2) ["<html>" "<header><title>タイトル</title></header>" "<body>本文</body>" "</html>" ""]) (.getBytes (StandardCharsets/UTF_8)))] (doto (. exchange getResponseHeaders) ;; 追加ヘッダは先に書くこと (.add "Content-Type" "text/html; charset=UTF-8") (.add "X-Dummy-Header" "Value")) (. exchange sendResponseHeaders 200 (count body-text)) (with-open [stream (. exchange getResponseBody)] (. stream write body-text))) (catch Exception e (. e printStackTrace)))) (let [port (if (> (count *command-line-args*) 1) ;; 最初の引数には、スクリプト名が入っているため (Integer/parseInt (second *command-line-args*)) 8080) server (HttpServer/create (InetSocketAddress. port) 0)] (doto server (.setExecutor (Executors/newCachedThreadPool)) (.createContext "/" (proxy [HttpHandler] [] (handle [exchange] (response-handler exchange)))) (.start)) (println (format "[%s] SimpleJdkHttpd Startup[%s]" (Date.) (. server getAddress))) ;; EnterかCtrl-cを入力されるまで、待ち合わせ (read-line))
こちらはlein-execで実行するのですが、そのまま終了してしまうので、最後にread-line関数で待っています…。
Undertow
JBoss系の、NIOベースのHTTPサーバ。WildFlyで使われているそうな。
Undertow
http://undertow.io/index.html
こちらを使用しての実装。
Groovy
こんな感じになりました。
simple-undertow-httpd.groovy
@Grab('io.undertow:undertow-core:1.0.5.Final') import io.undertow.Undertow import io.undertow.server.HttpHandler import io.undertow.util.Headers import io.undertow.util.HttpString def safeHandler = { clos -> { exchange -> try { clos.call(exchange) } catch (e) { e.printStackTrace() } } } def acceptor = { exchange -> println("[${new Date()}] Accept: Client[$exchange.sourceAddress], Url[$exchange.requestURI]") exchange } def header = { exchange -> exchange.responseHeaders.with { put(Headers.CONTENT_TYPE, "text/html; charset=UTF-8") put(new HttpString("X-Dummy-Header"), "Value") } exchange } def body = { exchange -> def bodyText = """|<html> |<header><title>タイトル</title></header> |<body>本文</body> |</html> |""".stripMargin() exchange.responseSender.send(bodyText) exchange } def host = "localhost" def port = args.length > 0 ? args[0].toInteger() : 8080 // 左から右に向かって合成する def responseHandlers = [acceptor, header, body] // 合成したClosureを、safeHandlerで包む def handler = safeHandler(responseHandlers.tail().inject(responseHandlers.head()) { acc, c -> acc >> c }) def server = Undertow .builder() .addListener(port, host) .setHandler(handler as HttpHandler) .build() server.start() println("[${new Date()}] SimpleUndertowHttpd Startup[$host:$port]")
この手のスクリプトを書く時は、だいたいテスト用なのでタイムアウトとかを試したくてThread.sleepしたりするのですが、Undertowのハンドラの中でsleepしたら、全体が止まってしまいました…。まあ、NIOだし…ということで今回はあまり気にしないことにしました。
それより、JDK付属のものとは違って処理が分離しやすそうだったので、ちょっとパイプライン化というか、Closureによる関数合成をやってみました。Clojureの定義とパイプラインへの登録で、処理を任意に追加や削除、入れ替えを行えます。
Architecture Overview
http://undertow.io/documentation/core/overview.html
Undertow自体の仕組みではないですが、こういうのをたまにやると面白いです。
Clojure
Clojureでのリライト版。
simple-undertow-httpd.clj
;; 使い方: $ lein exec simple-undertow-httpd.clj [ポート番号(デフォルト8080)] (require '[leiningen.exec :as exec]) (exec/deps '[[io.undertow/undertow-core "1.0.5.Final"]]) (ns simple-undertow-httpd (:import (java.util Date) (io.undertow Undertow) (io.undertow.server HttpHandler) (io.undertow.util Headers HttpString))) (defn safe-handler [func] (fn [exchange] (try (func exchange) (catch Exception e (. e printStackTrace))))) (defn acceptor [exchange] (println (format "[%s] Accept: Client[%s], Url[%s]" (Date.) (. exchange getSourceAddress) (. exchange getRequestURI))) exchange) (defn header [exchange] (doto (. exchange getResponseHeaders) (.put (Headers/CONTENT_TYPE) "text/html; charset=UTF8") (.put (HttpString. "X-Dummy-Header") "Value")) exchange) (defn body [exchange] (let [body-text (reduce #(str %1 (System/lineSeparator) %2) ["<html>" "<header><title>タイトル</title></header>" "<body>本文</body>" "</html>" ""])] (. (.. exchange getResponseSender) send body-text)) exchange) (def response-handlers [acceptor header body]) (def handler (safe-handler (apply comp (reverse response-handlers)))) (let [host "localhost" port (if (> (count *command-line-args*) 1) ;; 最初の引数には、スクリプト名が入っているため (Integer/parseInt (second *command-line-args*)) 8080) server (-> (Undertow/builder) (.addListener port host) (.setHandler (proxy [HttpHandler] [] (handleRequest [exchange] (handler exchange)))) (.build))] (. server start) (println (format "[%s] SimpleUndertowHttpd Startup[%s:%d]" (Date.) host port)) ;; EnterかCtrl-cを入力されるまで、待ち合わせ (read-line))
コンセプトは、Groovy版と同じ。関数合成利用版です。
Perl
最後は、Perl。
simple-fork-httpd.pl
#!/usr/bin/perl ## 使い方: $ perl simple-fork-httpd.pl [ポート番号(デフォルト8080)] use strict; use warnings; use encoding 'utf8'; use Encode; use Socket; # 第1引数が、受付ポート番号 my $port; if ($#ARGV >= 0) { $port = $ARGV[0]; } else { $port = 8080; } my $socket; my $crlf = "\r\n"; socket($socket, PF_INET, SOCK_STREAM, 0) or die "Can't create Socket $!"; setsockopt($socket, SOL_SOCKET, SO_REUSEADDR, 1); bind($socket, pack_sockaddr_in($port, INADDR_ANY)) or die "Can't bind Socket $!"; listen($socket, SOMAXCONN) or die "Can't listen Socket $!"; print '[' . get_time() . "] SimpleForkHttpd Server[$port]\n"; while (1) { my $client_socket; my $paddr = accept($client_socket, $socket); my ($client_port, $client_iaddr) = unpack_sockaddr_in($paddr); # my $client_hostname = gethostbyaddr($client_iaddr, AF_INET); my $client_ip = inet_ntoa($client_iaddr); print '[' . get_time() . "] Accept: Client[$client_ip:$client_port]\n"; my $pid = fork(); if ($pid == 0) { # 子プロセス $| = 1; my $request = ''; while (my $line = <$client_socket>) { $line =~ s/\r?\n$//; $request .= $line . $crlf; if ($line eq '') { last; } } # print '[' . get_time() . "] Client[$client_ip:$client_port] RequestDump\n$request\n"; my $headers = get_headers() . $crlf; print $client_socket $headers; my $body = get_body() . $crlf; print $client_socket encode('utf-8', $body); close($client_socket); exit; } else { # 親プロセス } } close($socket); sub get_headers { my $headers = <<HEADERS; HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 X-Dummy-Header: Value HEADERS return $headers; } sub get_body { my $body = <<BODY; <html> <header><title>タイトル</title><header> <body>本文</body> </html> BODY return $body; } sub get_time { my ($sec, $min, $hour, $day, $mon, $year) = localtime(); return sprintf("%04D/%02d/%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $day, $hour, $min, $sec); }
こちらは、forkを使って実装。起動が、Java系のものと比べるとめちゃくちゃ高速です。ただ、やってることは1番プリミティブですが。