Java 標準ライブラリの com.sun.net.httpserver で簡易的な Web サーバを作る

## 概要

- Java 標準ライブラリの com.sun.net.httpserver パッケージを使用して簡易的な Web サーバのサンプルを作る

## com.sun.net.httpserver とは

com.sun.net.httpserver は HTTP サーバを構築可能な Java パッケージ。

[com\.sun\.net\.httpserver \(Java SE 14 & JDK 14\)](https://docs.oracle.com/javase/jp/14/docs/api/jdk.httpserver/com/sun/net/httpserver/package-summary.html)

> 組込みのHTTPサーバーの構築に使用できる、単純で高度なHTTPサーバーAPIを提供します。 「HTTP」と「HTTPS」の両方がサポートされています。 APIは、RFC 2616 (HTTP 1.1)およびRFC 2818 (HTTP over TLS)の実装の一部を提供します。 このAPIで提供されないHTTP機能は、APIを使用してアプリケーション・コードで実装できます。
> 
> プログラマは、HttpHandlerインタフェースを実装する必要があります。 このインタフェースは、クライアントからの着信要求を処理するために呼び出されるコールバックを提供します。 HTTP要求とその応答を交換といいます。 HTTP交換は、HttpExchangeクラスによって表されます。 HttpServerクラスは、着信TCP接続の待機に使用され、これらの接続での要求をサーバーに登録されているハンドラにディスパッチします。

com.sun.net.httpserver パッケージは jdk.httpserver モジュール に属しているので注意。

[概要 \(Java SE 14 & JDK 14\)](https://docs.oracle.com/javase/jp/14/docs/api/index.html)

> Java Development Kit (JDK) APIはJDK固有のものであり、必ずしもJava SEプラットフォームのすべての実装で使用できるとは限りません。 これらのAPIは、名前がjdkで始まるモジュール内にあります。

## 今回の環境

- macOS 10.15.5 Catalina
- Java 14 (AdoptOpenJDK 14.0.1)

```
$ javac --version
javac 14.0.1

$ java --version
openjdk 14.0.1 2020-04-14
OpenJDK Runtime Environment AdoptOpenJDK (build 14.0.1+7)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 14.0.1+7, mixed mode, sharing)
```

## Web サーバのサンプルコード

以下のソースコードを MyServer.java というファイル名で保存する。

```MyServer.java
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class MyServer {

  public static void main(String args[]) throws IOException {
    // HTTP サーバを起動
    int port = 8000;
    HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
    server.createContext("/", new MyHandler());
    System.out.println("MyServer wakes up: port=" + port);
    server.start();
  }

  // HTTP リクエストを処理するために呼び出されるハンドラ 
  private static class MyHandler implements HttpHandler {

    // HTTP リクエストを処理する
    public void handle(HttpExchange t) throws IOException {

      System.out.println("**************************************************");

      // 開始行を取得
      String startLine =
        t.getRequestMethod() + " " +
          t.getRequestURI().toString() + " " +
          t.getProtocol();
      System.out.println(startLine);

      // リクエストヘッダを取得
      Headers reqHeaders = t.getRequestHeaders();
      for (String name : reqHeaders.keySet()) {
        System.out.println(name + ": " + reqHeaders.getFirst(name));
      }

      // リクエストボディを取得
      InputStream is = t.getRequestBody();
      byte[] b = is.readAllBytes();
      is.close();
      if (b.length != 0) {
        System.out.println(); // 空行
        System.out.println(new String(b, StandardCharsets.UTF_8));
      }

      // レスポンスボディを構築
      // (ここでは Java 14 から正式導入された Switch Expressions と
      //  Java 14 でプレビュー機能として使えるヒアドキュメント的な Text Blocks 機能を使ってみる)
      String resBody = switch (t.getRequestURI().toString()) {
        case "/hello" -> "{\"message\": \"Hello, World!\"}";
        case "/foobar" -> """
          {
            "foo": "bar",
            "ふー": "ばー"
          }""";
        default -> "{}";
      };

      // Content-Length 以外のレスポンスヘッダを設定
      Headers resHeaders = t.getResponseHeaders();
      resHeaders.set("Content-Type", "application/json");
      resHeaders.set("Last-Modified",
        ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME));
      resHeaders.set("Server",
        "MyServer (" +
          System.getProperty("java.vm.name") + " " +
          System.getProperty("java.vm.vendor") + " " +
          System.getProperty("java.vm.version") + ")");

      // レスポンスヘッダを送信
      int statusCode = 200;
      long contentLength = resBody.getBytes(StandardCharsets.UTF_8).length;
      t.sendResponseHeaders(statusCode, contentLength);

      // レスポンスボディを送信
      OutputStream os = t.getResponseBody();
      os.write(resBody.getBytes());
      os.close();
    }
  }
}
```

## コンパイル

javac コマンドでコンパイルする。
Java 14 でプレビュー機能として使える Text Blocks を使っているのでオプションで --enable-preview --release 14 を指定する必要がある。

```
$ javac --enable-preview --release 14 MyServer.java
注意:MyServer.javaはプレビュー言語機能を使用します。
注意:詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
```

コンパイルすると MyServer.class ファイルが作られる。

```
$ ls
MyServer.class  MyServer.java
```

## Web サーバを起動

java コマンドで MyServer を指定すると Web サーバが起動する。

```
$ java --enable-preview MyServer
MyServer wakes up: port=8000
```

## Web サーバに GET リクエスト

curl コマンドで /foobar に HTTP GET リクエストする。
レスポンスとして JSON が返ってくるのを確認できる。

```
$ curl -v http://localhost:8000/foobar
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> GET /foobar HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.70.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: MyServer (OpenJDK 64-Bit Server VM AdoptOpenJDK 14.0.1+7)
< Date: Tue, 23 Jun 2020 23:48:57 GMT
< Last-modified: Tue, 23 Jun 2020 23:48:57 GMT
< Content-type: application/json
< Content-length: 40
< 
{
  "foo": "bar",
  "ふー": "ばー"
* Connection #0 to host localhost left intact
}
```

MyServer プログラム側では HTTP GET リクエスト情報を出力している。

```
**************************************************
GET /foobar HTTP/1.1
Accept: */*
Host: localhost:8000
User-agent: curl/7.70.0
```

## Web サーバに POST リクエスト

curl コマンドで /hello に JSON データを HTTP POST リクエストする。
レスポンスとして JSON が返ってくるのを確認できる。

```
$ curl -v -H "Content-Type: application/json" -d '{"foobar":"ふーばー"}' http://localhost:8000/hello
*   Trying ::1:8000...
* Connected to localhost (::1) port 8000 (#0)
> POST /hello HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.70.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 25
> 
* upload completely sent off: 25 out of 25 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: MyServer (OpenJDK 64-Bit Server VM AdoptOpenJDK 14.0.1+7)
< Date: Wed, 24 Jun 2020 20:31:18 GMT
< Last-modified: Wed, 24 Jun 2020 20:31:18 GMT
< Content-type: application/json
< Content-length: 28
< 
* Connection #0 to host localhost left intact
{"message": "Hello, World!"}
```

MyServer プログラム側では HTTP POST リクエスト情報を出力している。
リクエストボディに指定された JSON が出力されている。

```
**************************************************
POST /hello HTTP/1.1
Accept: */*
Host: localhost:8000
User-agent: curl/7.70.0
Content-type: application/json
Content-length: 25

{"foobar":"ふーばー"}
```

## 参考資料

- [com\.sun\.net\.httpserver \(Java SE 14 & JDK 14\)](https://docs.oracle.com/javase/jp/14/docs/api/jdk.httpserver/com/sun/net/httpserver/package-summary.html)
- [HTTP のメッセージ \- HTTP \| MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Messages)
- [JEP 361: Switch Expressions \(Standard\)](https://openjdk.java.net/jeps/361)
- [JEP 368: Text Blocks \(Second Preview\)](https://openjdk.java.net/jeps/368)
- [プレビュー機能 (JDK 14ドキュメント)](https://docs.oracle.com/javase/jp/14/language/preview-language-and-vm-features.html)