Help us understand the problem. What is going on with this article?

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)

組込みの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)

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":"ふーばー"}

参考資料

niwasawa
迷子になりがちな地図・位置情報系プログラマ。
http://niwasawa.hatenablog.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした