倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

『Real World HTTP』のための Java 簡易 HTTP サーバ

ちょっと流行ってるようなので、オライリーの『Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術』を買って読んでるのですが(買って2週間くらいだけどまだ2章までしかいってねぇ)、1章・2章に載っている Go によって書かれた簡易サーバを Java で書いてみます。

ここの HTTP 簡易サーバは、「JDK付属、Undertowを使ったGroovy&Clojure、Perlでの簡単なHTTPサーバ」で紹介されている、JDK 付属の HTTP サーバの箇所を参考にしました(ほとんど Groovy コードを Java コードに直しただけですが)。

簡易 HTTP サーバ

Go には簡易 HTTP サーバが付属しているようですが、JDK にも com.sun.net.httpserver パッケージに簡易 HTTP サーバ用のクラスがあります。 サーバのインスタンスを生成するのに InetSocketAddress オブジェクトを作ったりしてますが、基本的には使い方はそんなに変わらないかと思います。 主な違いは

  • リクエストとレスポンスが1つのクラス HttpExchange にまとめられている
  • レスポンスを返す際にステータスコードとボディのバイト数を明示的に返す必要がある

というあたりでしょうか。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public class MyServer {

    public static void main(String... args)throws IOException{

        HttpServer server = HttpServer.create(new InetSocketAddress(18888), 0);
        server.setExecutor(Executors.newCachedThreadPool());  // Executor の設定
        server.createContext("/", exchange -> {
            // リクエストとレスポンスの情報が exchange にまとめられている

            // リクエストのヘッダなどを表示
            logRequest(exchange);

            // レスポンスを返す
            String responseText = "<html><body>hello</body></html>\n";
            respond(exchange, responseText);
        });
        server.start();
        System.out.println("start http listening :18888");
    }

    /** リクエストのヘッダなどを表示 */
    private static void logRequest(HttpExchange exchange){
        // 「GET / HTTP1.0」などと表示
        System.out.println(exchange.getRequestMethod() + " / " + exchange.getProtocol());
        // リクエストのヘッダを表示
        exchange.getRequestHeaders().forEach((k,v) -> System.out.println(k + ": "+v));
        System.out.println();
    }

    /** レスポンスを返す */
    private static void respond(HttpExchange exchange, String responseText){
        byte[] responseBody = responseText.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");
        try {
            exchange.sendResponseHeaders(200, responseBody.length);  // 明示的に返す必要あり
            exchange.getResponseBody().write(responseBody);

        }catch(IOException ex){
            ex.printStackTrace();
        }
    }
}
  • Executor の設定はここで扱うくらいのコードでは別に必要ないかと思いますが、一応設定しています。
  • 通常、リクエストから情報を引き出してレスポンスを返す処理は HttpHandler インターフェースの実装クラスを作って行いますが、ここではラムダ式で書いています(createContext() メソッドを呼び出しているあたり参照)。
  • レスポンスのヘッダでは、一応 Content-Type でエンコーディングも指定しています。

まぁ、Go のコードより少々長くなっていますが、許容範囲でしょう。

上記のコードをコンパイル & 実行して、Bash などから curl コマンドを以下のように実行すると

$ curl --http1.0 -v http://localhost:18888/greeting
> GET /greeting HTTP/1.0
> Host: localhost:18888
> User-Agent: curl/7.54.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: close
< Date: Fri, 07 Jul 2017 00:47:50 GMT
< Content-type: text/html; charset=UTF-8
< Content-length: 32
<
...
<html><body>hello</body></html>

* Closing connection 0

のようなレスポンスが得られます(一部省略しています)。 また、サーバ側では、リクエストを受け付けると

GET / HTTP/1.0
Accept: [*/*]
Host: [localhost:18888]
User-agent: [curl/7.54.1]

のようにヘッダ情報などが表示されます。

Cookie を付与する

『Real World HTTP』の2章に、クッキーを使ってクライアント情報の管理をするコードも書かれていたので、同じことを Java でも書いてみましょう。 まぁ、骨組みはいっしで、レスポンスのヘッダにクッキーを設定するのと、リクエストのヘッダに設定したクッキーがあるかどうかで処理を切り替える部分が新しいところです。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public class CookieServer {

    public static void main(String... args)throws Exception{

        HttpServer server = HttpServer.create(new InetSocketAddress(18888), 0);
        server.setExecutor(Executors.newCachedThreadPool());
        server.createContext("/", exchange -> {
            logRequest(exchange);

            // クッキーの有無で処理を変える
            if(exchange.getRequestHeaders().containsKey("Cookie")){
                respond(exchange, "<html><body>2回目以降</body></html>\n");
            }else{
                respond(exchange, "<html><body>初訪問</body></html>\n");
            }
        });
        server.start();
        System.out.println("start http listening :18888");
    }

    private static void logRequest(HttpExchange exchange){
        System.out.println(exchange.getRequestMethod() + " / " + exchange.getProtocol());
        exchange.getRequestHeaders().forEach((k,v) -> System.out.println(k + ": "+v));
        System.out.println();
    }

    private static void respond(HttpExchange exchange, String responseText){
        byte[] responseBody = responseText.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().add("Content-Type", "text/html; charset=UTF-8");

        // クッキーを設定
        exchange.getResponseHeaders().add("Set-Cookie", "VISIT=TRUE");
        try {
            exchange.sendResponseHeaders(200, responseBody.length);
            exchange.getResponseBody().write(responseBody);

        }catch(IOException ex){
            ex.printStackTrace();
        }
    }
}

curl でクッキーを扱うために -c/-b オプションをつけて実行すると(cookie.txt ファイルにクッキーを保存)、1度目はクッキーがないので

$ curl --http1.0 -c cookie.txt -b cookie.txt http://localhost:18888/greeting
<html><body>初訪問</body></html>

となり、2度目は

$ curl --http1.0 -c cookie.txt -b cookie.txt http://localhost:18888/greeting
<html><body>2回目以降</body></html>

と、別の表示がされます。 サーバ側でリクエストのヘッダを見ると、2度目以降は以下のようにクッキーがあることが分かります:

GET / HTTP/1.0
Cookie: [VISIT=TRUE]
Accept: [*/*]
Host: [localhost:18888]
User-agent: [curl/7.54.1]

まぁ、だいたいこんな感じです。

『Real World HTTP』の2章には上記のコードの他に Digest 認証を行うコードも載っていますが、Java でもサードパーティー製のライブラリが必要そうなのでここでは扱いません(「go get」のように簡単にライブラリをダウンロードできなさそうだし)。 Basic 認証に関しては、com.sun.net.httpserver パッケージにある BasicAuthenticator クラスを使って行えるようです。

3章以降の Go コードを Java で書き直すのも、やれば勉強になりそうだけど読書が全く進まなさそうなので今のところ未定。 おそらくやり出すと Go に気移りしそうな予感がしますが(笑)

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術