CLOVER🍀

That was when it all began.

JDK付属、Undertowを使ったGroovy&Clojure、Perlでの簡単なHTTPサーバ

なんとなく、時々ちょっとしたテストとかで使っているダミーのHTTPサーバ用のスクリプトをまとめたくなりまして。

で、せっかくなのでプラスアルファも入れて。

ホントに簡単なものなので、以下のスペックで。

  • リクエストの内容を解析することは、必須としない
  • 起動時にはポート番号を指定できる。しなかった場合は、8080で起動
  • ヘッダ、ボディに何か出力する
  • サーバの終了は、Ctrl-cなどで
  • できれば並行処理可能に

ターゲットは、GroovyとClojureJDK付属のものを使った版、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番プリミティブですが。

実際に使う時は、実はPerlの方が多かったり…Linuxなら、どこでも入ってますからね〜。