HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

CentOS 7環境にMonoを導入してみる

ふと思い立って、C#のプログラムをCentOS 7上で動かせないかと考え、導入からコンパイル、実行までをやってみたメモ。

参考サイト

環境

$ uname -a
Linux proteus-annex-centos7 3.10.0-862.3.3.el7.x86_64 #1 SMP Fri Jun 15 04:15:27 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/redhat-release 
CentOS Linux release 7.5.1804 (Core) 
$ 

導入

自分の環境では、yum-config-managerが足りなかったので、まずはそれをインストール。

$ su -
# yum -y install yum-utils

そして、Monoのインストール。

# rpm --import "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF"
# yum-config-manager --add-repo http://download.mono-project.com/repo/centos/
# yum install -y mono-complete

バージョン確認。これは一般ユーザでOK。

$ mono --version
Mono JIT compiler version 5.12.0.233 (tarball Tue May  8 09:28:02 UTC 2018)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
        TLS:           __thread
        SIGSEGV:       altstack
        Notifications: epoll
        Architecture:  amd64
        Disabled:      none
        Misc:          softdebug 
        Interpreter:   yes
        LLVM:          supported, not enabled.
        GC:            sgen (concurrent by default)
$ 

ソースコード

いたってシンプルなHello World

using System;

public class Program {
    public static void Main(string[] args) {
        Console.WriteLine("Hello World");
    }
}

コンパイル

$ mcs HelloWorld.cs

実行

$ mono HelloWorld.exe
Hello World
$ 

大量の座標値を読む場合の効率的な読み方

ふと、AtCoderなどのコンテストでよくある「大量の座標値を読んで何かを求める」という場合に、どういう処理を書いたら一番効率がいいのかと疑問に思ったので試してみたメモ。

テストケース

実際に試したケースは以下の通り

  • Main01.java - Scanner.nextInt()で読む
    • ひたすらScanner.nextInt()で読んでいく
  • Main02.java - String.split(" ")とInteger.parseInt(String)
    • 単なる1文字から成る文字列で分割させてInteger.parseInt(String)でint化
  • Main03.java - StringTokenizer.nextToken()とInteger.parseInt(String)
    • StringTokenizerで分割させてInteger.parseInt(String)でint化
  • Main04.java - String.split("[ ]")とInteger.parseInt(String)
    • 正規表現"[ ]"で分割させてInteger.parseInt(String)でint化
  • Main05.java - String.indexOf(String)とInteger.parseInt(String)
    • String.indexOf(" ")で分割位置を求めて分割し、Integer.parseInt(String)でint化
  • Main06.java - String.indexOf(int)とInteger.parseInt(String)
    • String.indexOf(' ')で分割位置を求めて分割し、Integer.parseInt(String)でint化
  • Main07.java - StringBuilder.indexOf(" ")とInteger.parseInt(String)
    • StringBuilder.indexOf(" ")で分割位置を求めて分割し、Integer.parseInt(String)でint化

以下、そのソースコード

  • Main01.java - Scanner.nextInt()で読む
import java.util.*;

public class Main01 {
    public static void main(String[] args) {
        try (Scanner sc = new Scanner(System.in)) {
            int N = sc.nextInt();
            for (int i = 0; i < N; ++i) {
                int x = sc.nextInt();
                int y = sc.nextInt();
            }
        }
    }
}
  • Main02.java - String.split(" ")とInteger.parseInt(String)
import java.io.*;

public class Main02 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                String[] ss = in.readLine().split(" ");
                int x = Integer.parseInt(ss[0]);
                int y = Integer.parseInt(ss[1]);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Main03.java - StringTokenizer.nextToken()とInteger.parseInt(String)
import java.io.*;
import java.util.*;

public class Main03 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                StringTokenizer st = new StringTokenizer(in.readLine());
                int x = Integer.parseInt(st.nextToken());
                int y = Integer.parseInt(st.nextToken());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Main04.java - String.split("[ ]")とInteger.parseInt(String)
import java.io.*;

public class Main04 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                String[] ss = in.readLine().split("[ ]");
                int x = Integer.parseInt(ss[0]);
                int y = Integer.parseInt(ss[1]);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Main05.java - String.indexOf(String)とInteger.parseInt(String)
import java.io.*;

public class Main05 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                String s = in.readLine();
                int idx = s.indexOf(" ");
                int x = Integer.parseInt(s.substring(0, idx));
                int y = Integer.parseInt(s.substring(idx + 1));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Main06.java - String.indexOf(int)とInteger.parseInt(String)
import java.io.*;

public class Main06 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                String s = in.readLine();
                int idx = s.indexOf(' ');
                int x = Integer.parseInt(s.substring(0, idx));
                int y = Integer.parseInt(s.substring(idx + 1));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • Main07.java - StringBuilder.indexOf(" ")とInteger.parseInt(String)
import java.io.*;

public class Main07 {
    public static void main(String[] args) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) {
            int N = Integer.parseInt(in.readLine());
            for (int i = 0; i < N; ++i) {
                StringBuilder sb = new StringBuilder(in.readLine());
                int idx = sb.indexOf(" ");
                int x = Integer.parseInt(sb.substring(0, idx));
                int y = Integer.parseInt(sb.substring(idx + 1));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

データ生成

データは以下のスクリプトで生成した。

#! /bin/bash

for n in 80000 160000 320000 640000 1280000 ; do
    ( echo ${n} ; for ((i = 1; i <= n; ++i)); do echo "${i} ${i}" ; done ) > in/$(printf "%07d" ${n}).txt
done

実行スクリプト

テストの実行は以下のスクリプトで行った。

#! /bin/bash

for cls in $@ ; do
    javac -d . ${cls}.java || exit 1
done
for f in in/*.txt ; do
    echo "===${f}==="
    for cls in $@ ; do
        printf "%-8s: " ${cls}
        cat ${f} | /usr/bin/time -f "%MKB / %esec" java -Xms1192m -Xmx1192m ${cls}
    done
done

以下のように呼び出す。

$ ./run.sh Main01 Main02 Main03 Main04 Main05 Main06 Main07

実行結果

出力結果は以下の通り。

===in/0080000.txt===
Main01  : 157044KB / 0.63sec
Main02  : 62940KB / 0.28sec
Main03  : 50528KB / 0.20sec
Main04  : 128936KB / 0.45sec
Main05  : 48324KB / 0.18sec
Main06  : 46600KB / 0.18sec
Main07  : 52804KB / 0.18sec
===in/0160000.txt===
Main01  : 265428KB / 0.85sec
Main02  : 88084KB / 0.35sec
Main03  : 67468KB / 0.25sec
Main04  : 217240KB / 0.60sec
Main05  : 59660KB / 0.26sec
Main06  : 60032KB / 0.21sec
Main07  : 76748KB / 0.24sec
===in/0320000.txt===
Main01  : 356544KB / 1.00sec
Main02  : 138256KB / 0.40sec
Main03  : 105100KB / 0.29sec
Main04  : 355560KB / 0.91sec
Main05  : 88500KB / 0.32sec
Main06  : 87328KB / 0.28sec
Main07  : 116480KB / 0.34sec
===in/0640000.txt===
Main01  : 357572KB / 1.35sec
Main02  : 238488KB / 0.50sec
Main03  : 177380KB / 0.43sec
Main04  : 357616KB / 0.92sec
Main05  : 127000KB / 0.36sec
Main06  : 129108KB / 0.33sec
Main07  : 186420KB / 0.37sec
===in/1280000.txt===
Main01  : 356480KB / 2.14sec
Main02  : 354196KB / 0.70sec
Main03  : 321064KB / 0.51sec
Main04  : 356188KB / 1.33sec
Main05  : 216928KB / 0.45sec
Main06  : 212696KB / 0.46sec
Main07  : 315560KB / 0.50sec

このままだと分かりづらいので、メモリと実行時間に分けて一覧表にしてみる。

  • メモリ使用量(KB)
テストケース\行数 80000 160000 320000 640000 1280000
Main01 157044 265428 356544 357572 356480
Main02 62940 88084 138256 238488 354196
Main03 50528 67468 105100 177380 321064
Main04 128936 217240 355560 357616 356188
Main05 48324 59660 88500 127000 216928
Main06 46600 60032 87328 129108 212696
Main07 52804 76748 116480 186420 315560
  • 実行時間(秒)
テストケース\行数 80000 160000 320000 640000 1280000
Main01 0.63 0.85 1.00 1.35 2.14
Main02 0.28 0.35 0.40 0.50 0.70
Main03 0.20 0.25 0.29 0.43 0.51
Main04 0.45 0.60 0.91 0.92 1.33
Main05 0.18 0.26 0.32 0.36 0.45
Main06 0.18 0.21 0.28 0.33 0.46
Main07 0.18 0.24 0.34 0.37 0.50

メモリ使用量、実行時間ともに、以下の2つが処理が単純なので効率が良いが、String.split(String)を使う場合に比べてコードが長くなる。

  • Main05.java - String.indexOf(String)とInteger.parseInt(String)
    • String.indexOf(" ")で分割位置を求めて分割し、Integer.parseInt(String)でint化
  • Main06.java - String.indexOf(int)とInteger.parseInt(String)
    • String.indexOf(' ')で分割位置を求めて分割し、Integer.parseInt(String)でint化

コードの単純さでは以下の2つが分かりやすいが、上の2つに比べると処理効率が悪い。

  • Main02.java - String.split(" ")とInteger.parseInt(String)
    • 単なる1文字から成る文字列で分割させてInteger.parseInt(String)でint化
  • Main04.java - String.split("[ ]")とInteger.parseInt(String)
    • 正規表現"[ ]"で分割させてInteger.parseInt(String)でint化

これは、Stringクラスのソースコードを見ると分かるのだが、Main02のケースでは内部的にArrayListを生成するので、メモリと処理時間を少し余計に食う。また、Main04のケースではPatternクラスに移譲しているので更にメモリと処理時間を食う。

プログラミングコンテストのような処理速度の速さが求められるケースでは、Main01のScannerを使うケースは読み込むデータが少量の場合に限られるだろう。Main03のStringTokenizerを使うケースは、コンテストにおいてはFastScannerという自前実装の中でよく見る。

Main05とMain06が処理効率的には良いが、文字列が3つ以上のトークンから成る場合を考えると処理が面倒になってくるので、その場合は、メモリ等を多少食うが、素直にString.split(" ")を使うのが良いだろう。

mapのoperator[]の罠

mapを使ったあるプログラムを書いていて、やたらとメモリを食うので、何だろうといろいろ試行錯誤しながら調べてみたら、どうもmapの使い方に問題があるのではないかということが分かってきた。

mapのリファレンスを探して読んでみると、以下のようなことが書いてある。

https://cpprefjp.github.io/reference/map/map/op_at.html

戻り値

キーxに対応する値を返す。対応する要素が存在しない場合は、要素をデフォルト構築して参照を返す。

「デフォルト構築」ってなんぞや?・・・

そこで、(いろいろすっ飛ばして)検証プログラムを書いてみた。

#include <iostream>
#include <map>

using namespace std;

int main(int argc, char** argv) {
    map<int, int*> m;

    int a = 1;
    int b = 2;

    // (1)
    {
        map<int, int*>::iterator it = m.find(1);
        cout << "(1) " << (it == m.end()) << endl;
    }

    // (2)
    {
        int* p = m[1];
        cout << "(2) " << (p == NULL) << endl;
    }

    // (3)
    {
        map<int, int*>::iterator it = m.find(1);
        cout << "(3) " << (it == m.end()) << ":" << (it->second == NULL) << endl;
    }

    // (4)
    {
        m[1] = &a;
        map<int, int*>::iterator it = m.find(1);
        cout << "(4) " << (it == m.end()) << ":" << (it->second == NULL) << ":" << (it->second == &a) << endl;
    }

    return EXIT_SUCCESS;
}

出力は以下のようになる。

(1) 1
(2) 1
(3) 0:1
(4) 0:0:1

(1)ではitm.end()に等しいのに、(2)でm[1]をやった後に(3)でもう一度findを使うと、今度はm.end()と等しくならなくなっている、つまり、mapの中に何かしらのエントリが存在する状態になっているようだ。

(4)はついでだが、m[1]に何かを代入すると、当然ながらその値がmapに入ることになる。

これは罠だなぁ‥覚えておこう‥

json_encode関数のJSON_UNESCAPED_UNICODEオプションの指定方法

json_encode関数はデフォルトではUnicode文字列がエスケープされるのだが、PHP 5.4より以前ではエスケープを避ける手段がないので、以下のようなコードを書くと、出力を人がパッと見ても読めない。

<?php

ini_set('display_errors', 'on');
date_default_timezone_set('Asia/Tokyo');

set_include_path('../smarty-2.6.28/libs/');

require_once('Smarty.class.php');

$smarty = new Smarty();

$smarty->template_dir = 'smarty/templates/';
$smarty->compile_dir  = 'smarty/templates_c/';
$smarty->config_dir   = 'smarty/configs/';
$smarty->cache_dir    = 'smarty/cache/';

$ary = array(
    '名前' => 'ボブ',
    'サロゲートペア' => '𠮷',
);
$smarty->assign('strs_json', json_encode($ary));
$smarty->assign('strs_ary', $ary);

$smarty->display('index.tpl');
PHPバージョン:{$smarty.const.PHP_VERSION}
<br />
<pre>
{$strs_json}
</pre>
<pre>
{$strs_ary|@json_encode}
</pre>
  • 出力
PHPバージョン:5.3.3
<br />
<pre>
{"\u540d\u524d":"\u30dc\u30d6","\u30b5\u30ed\u30b2\u30fc\u30c8\u30da\u30a2":"\ud842\udfb7"}
</pre>
<pre>
{"\u540d\u524d":"\u30dc\u30d6","\u30b5\u30ed\u30b2\u30fc\u30c8\u30da\u30a2":"\ud842\udfb7"}
</pre>

一方、PHP 5.4からは、JSON_UNESCAPED_UNICODEというオプションが指定できるようになった。

‥のだが、Smartyではどう指定するのかをちょっと悩んだのでメモ。

答えは、以下のように書けばよい。

<?php

ini_set('display_errors', 'on');
date_default_timezone_set('Asia/Tokyo');

set_include_path('../smarty-2.6.28/libs/');

require_once('Smarty.class.php');

$smarty = new Smarty();

$smarty->template_dir = 'smarty/templates/';
$smarty->compile_dir  = 'smarty/templates_c/';
$smarty->config_dir   = 'smarty/configs/';
$smarty->cache_dir    = 'smarty/cache/';

$ary = array(
    '名前' => 'ボブ',
    'サロゲートペア' => '𠮷',
);
$smarty->assign('strs_json', json_encode($ary, JSON_UNESCAPED_UNICODE));
$smarty->assign('strs_ary', $ary);

$smarty->display('index.tpl');
PHPバージョン:{$smarty.const.PHP_VERSION}
<br />
<pre>
{$strs_json}
</pre>
<pre>
{$strs_ary|@json_encode:$smarty.const.JSON_UNESCAPED_UNICODE}
</pre>
  • 出力
PHPバージョン:5.4.16
<br />
<pre>
{"名前":"ボブ","サロゲートペア":"𠮷"}
</pre>
<pre>
{"名前":"ボブ","サロゲートペア":"𠮷"}
</pre>

Javaでの和暦対応の罠

ふと、Javaで和暦対応してたよなぁと思い出し、検索して見つけた以下のサイトのプログラムを元に、とあることを検証してみた。

それは、「Calendar#setで西暦年をセットして和暦でフォーマットして出力したら、西暦年に対応する元号が分かるじゃん」というもの。

早速書いてみる。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

public class Test1 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance(locale);
        System.out.println(cal.getClass().getName());

        int[] years = { 1900, 1925, 1950, 1975, 2000 };
        for (int year : years) {
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

いざ実行。

$ javac Test1.java && java Test
java.util.JapaneseImperialCalendar
1900: 平成1900年3月11日
1925: 平成1925年3月11日
1950: 平成1950年3月11日
1975: 平成1975年3月11日
2000: 平成2000年3月11日
$ 

・・・えっ!?‥「cal.set(Calendar.YEAR, year)」って「和暦での年数をセットしないといけないの?」‥ どうやらそうらしい。

というわけでプログラムをちょこちょこいじりながら調べてみたら、「Calendar cal = Calendar.getInstance(locale)」の部分が良くないらしい。

書き直したプログラム。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

public class Test2 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance();
        System.out.println(cal.getClass().getName());

        int[] years = { 1900, 1925, 1950, 1975, 2000 };
        for (int year : years) {
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

実行。

$ javac Test2.java && java Test2
java.util.GregorianCalendar
1900: 明治33年3月11日
1925: 大正14年3月11日
1950: 昭和25年3月11日
1975: 昭和50年3月11日
2000: 平成12年3月11日
$ 

おぉ、今度は期待通り。


ということはだよ、例えば「明治33年」の「33」という年数を素直にセットしたい場合はできないってことじゃん‥

ということで、最初のプログラム(Test1.java)の出力で得られた「java.util.JapaneseImperialCalendar」のソースを見てみる。(CentOS 7のjava-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64)

class JapaneseImperialCalendar extends Calendar {
    /*
     * Implementation Notes
     *
     * This implementation uses
     * sun.util.calendar.LocalGregorianCalendar to perform most of the
     * calendar calculations. LocalGregorianCalendar is configurable
     * and reads <JRE_HOME>/lib/calendars.properties at the start-up.
     */

    /**
     * The ERA constant designating the era before Meiji.
     */
    public static final int BEFORE_MEIJI = 0;

    /**
     * The ERA constant designating the Meiji era.
     */
    public static final int MEIJI = 1;

    /**
     * The ERA constant designating the Taisho era.
     */
    public static final int TAISHO = 2;

    /**
     * The ERA constant designating the Showa era.
     */
    public static final int SHOWA = 3;

    /**
     * The ERA constant designating the Heisei era.
     */
    public static final int HEISEI = 4;

パッケージプライベートなクラスなので直接の参照はできないが、明治、大正、昭和、平成に対応するCalendar.ERAの定数らしきが定義されている。というわけで試してみる。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Locale;

public class Test3 {
    public static void main(String[] args) {
        Locale locale = new Locale("ja", "JP", "JP");
        Calendar cal = Calendar.getInstance(locale);
        System.out.println(cal.getClass().getName());

        Map<Integer, Integer> years = new HashMap<>();
        years.put(1, 33); // 明治33年
        years.put(2, 14); // 大正14年
        years.put(3, 25); // 昭和25年
        years.put(4, 12); // 平成12年
        for (Map.Entry<Integer, Integer> entry : years.entrySet()) {
            int era = entry.getKey();
            int year = entry.getValue();
            cal.set(Calendar.ERA, era);
            cal.set(Calendar.YEAR, year);
            DateFormat format = new SimpleDateFormat("GGGGy年M月d日", locale);
            System.out.println(year + ": " + format.format(cal.getTime()));
        }
    }
}

実行してみる。

$ javac Test3.java && java Test3
java.util.JapaneseImperialCalendar
33: 明治33年3月11日
14: 大正14年3月11日
25: 昭和25年3月11日
12: 平成12年3月11日
$ 

おぉ、できた、できた、定数の値の実装が変わったらアウトだがなw‥

サーバー名の末尾にドットを付けたリクエストを送ると予期せぬ応答が返ってくる問題

前置き

以下の2つの環境での挙動が違うことに悩んでいた。

何をしたときの挙動かというと、

$ echo '' | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts

のように、サーバー名の末尾にドットがついたリクエストを送ったときにSSL/TLSハンドシェイクに失敗する場合としない場合があるということ。 つまり、失敗する場合には以下のようなレスポンスが返ってくる。

CONNECTED(00000003)
139918566537120:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 324 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520436816
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

成功する場合には、以下のようにCipherに適切な文字列が返ってくるし、ドットを付けない場合も同様に成功する。また、CentOS 6環境のJDKをOpenJDK 1.7.0に切り替えてみても成功するようになる。

(省略)
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
(省略)
---
DONE

そこで、Java 7とJava 8で何かが変わったのだろうと思い、その部分を特定するためにTomcat 6のソースを眺めてみたりしてものすごく遠回りしたのだが、「もしかして、根本的なところで仕様が変わっているんじゃね?」と思い始め、別の環境(CentOS 7)で簡単なサーバープログラムを動かしてみたら何か分かるかもということで試してみた。

事前準備

まず、使用するJDK環境を2つ用意する。OpenJDK 1.7.0とOpenJDK 1.8.0。インストールの手順は省略する。

で、alternativesで切り替えて挙動を見る。

事前準備としては、コンパイラ(javac)はOpenJDK 1.7.0にしておき、インタプリタは必要に応じて切り替える。最初はOpenJDK 1.7.0にしておく。

$ sudo alternatives --config javac

2 プログラムがあり 'javac' を提供します。

  選択       コマンド
-----------------------------------------------
*+ 1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/bin/javac)
   2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/bin/javac)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*+ 1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
   2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ javac -version
javac 1.7.0_171
$ java -version
java version "1.7.0_171"
OpenJDK Runtime Environment (rhel-2.6.13.0.el7_4-x86_64 u171-b01)
OpenJDK 64-Bit Server VM (build 24.171-b01, mixed mode)
$ 

次に、サーバー証明書を適当に作っておく。

$ openssl genrsa -aes256 2048 -out server.key
Generating RSA private key, 2048 bit long modulus
....................................+++
......................+++
e is 65537 (0x10001)
Enter pass phrase: test
Verifying - Enter pass phrase: test
-----BEGIN RSA PRIVATE KEY-----
(省略)
-----END RSA PRIVATE KEY-----
$ openssl rsa -in server.key -out server.key 
writing RSA key
$ openssl req -new -sha256 -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Hokkaido
Locality Name (eg, city) [Default City]:Sapporo
Organization Name (eg, company) [Default Company Ltd]:HHeLiBeX Ltd.
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:hhelibex.local
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ openssl x509 -in server.csr -out server.crt -req -signkey server.key -days 365
Signature ok
subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
Getting Private key
$ openssl pkcs12 -export -inkey server.key -in server.crt -out server.p12
Enter Export Password: changeit
Verifying - Enter Export Password: changeit
$ 

サーバープログラム

以下のような簡単なプログラムを作って起動する。テストだから、必要なパラメータはすべてべた書きだがご容赦を。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;

import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;

public class SSLServer {
    public static void main(String[] args) {
        try {
            String keyStoreFile = "server.p12";
            char[] keyStorePassword = "changeit".toCharArray();

            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);

            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            kmf.init(keyStore, keyStorePassword);

            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers() , null , null);
            ServerSocketFactory ssf = sslContext.getServerSocketFactory();
            ServerSocket serverSocket  = ssf.createServerSocket(8443);

            while (true) {
                System.out.println("--------" + System.getProperty("java.version") + "--------");
                System.out.println("Waiting for SSL connection");

                Socket socket = null;
                BufferedReader in = null;
                BufferedWriter out = null;
                try {
                    socket = serverSocket.accept();
                    if (socket == null) {
                        System.out.println("Client socket is null, ignored.");
                        continue;
                    }
                    System.out.println("Accepted.");

                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

                    String msg = in.readLine();
                    System.out.println("Message from client: " + msg);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (in != null) {
                        try { in.close(); } catch (IOException e) { e.printStackTrace(); }
                    }
                    if (out != null) {
                        try { out.close(); } catch (IOException e) { e.printStackTrace(); }
                    }
                    if (socket != null) {
                        socket.close();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接続テスト(サーバー:OpenJDK 1.7.0)

  • 起動
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*  1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
 + 2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:2
$ java SSLServer
--------1.7.0_171--------
Waiting for SSL connection
  • クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts
CONNECTED(00000003)
depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local
verify error:num=18:self signed certificate
verify return:1
depth=0 C = JP, ST = Hokkaido, L = Sapporo, O = HHeLiBeX Ltd., CN = hhelibex.local
verify return:1
---
Certificate chain
 0 s:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
   i:/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
-----BEGIN CERTIFICATE-----
MIIDQjCCAioCCQC3i2D7IVfOsTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJK
UDERMA8GA1UECAwISG9ra2FpZG8xEDAOBgNVBAcMB1NhcHBvcm8xFjAUBgNVBAoM
DUhIZUxpQmVYIEx0ZC4xFzAVBgNVBAMMDmhoZWxpYmV4LmxvY2FsMB4XDTE4MDMw
NzE2MjU1NloXDTE5MDMwNzE2MjU1NlowYzELMAkGA1UEBhMCSlAxETAPBgNVBAgM
CEhva2thaWRvMRAwDgYDVQQHDAdTYXBwb3JvMRYwFAYDVQQKDA1ISGVMaUJlWCBM
dGQuMRcwFQYDVQQDDA5oaGVsaWJleC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAM2I/AxPvGX4Pu9BqwmP0XoAAXpCYIrRNNa+bw1irLHQ82o+
J1bEGev9x6/jYhA+L7AYHMGnE6gbg1azrxyc06OdzN5X6OTC9xia6S7+LP3Ar8P6
c3BURqoU7TWOZdT7/KmPfgPC/uNty0lgA1U74PsdSMDE6VPU/MVyRoezsDLiXPrV
p9eve9bXHiuRF1G+Y0lO3Ym3fGilIZa7HEEqeVLTrKWmS2odOlIT/t8VAfCKBds9
nd38hwPThB0k9F6fhkUgwDeEZZdXXtMh+UNKEHdkI/VGji6uL72sAnwTc1H2VoAc
srKcdmzAWh8Kj9ynVenoAjWUTD4LV3L/JF76PLcCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAgpx32ml5YAacemf63fk9fNS7czjUZqvHBtfR4B6Vl+nmwHnIOazjSgRz
WRv86ZnP9t2bh5myhFZtg47BLI6gW8Ca4a92lehz1Cl6tB5sqbBk0vm3Jd78SzIV
T1Kxx9CgOEEgT54WuLyRpuaQwICQrKZgWWysCojRtiK/7tZ0amCW8CMfakLdvAaW
NF3814ZPS5AiIWrxaQ4XVEw2kB8yGapHO9dLMO81Jr9xpzjG2ENMR7aAxzPcdFvE
HJmIAyNYN8e6J/KcYAlAGz3Jo+ppcqn3De2GlFWwvPKiuVrDTeuM9r68u+rRFrmA
wTxbkeP/rGxXRzG3G2VQmgY7rtBI6A==
-----END CERTIFICATE-----
---
Server certificate
subject=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
issuer=/C=JP/ST=Hokkaido/L=Sapporo/O=HHeLiBeX Ltd./CN=hhelibex.local
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 1378 bytes and written 495 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-SHA384
    Session-ID: 5AA01ABEC478A8640AF9BFA21DB39BD751CC7BD651F29EFCEA4A14F91B2D7FC8
    Session-ID-ctx: 
    Master-Key: 47C8AF2C33540069495BD8372059306B8411D19F684007DFF8E88BEE5653690CCE5C010145C58D54962A14C29418F89D
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520442046
    Timeout   : 300 (sec)
    Verify return code: 18 (self signed certificate)
---
DONE
$
  • サーバー側でのメッセージ出力

メッセージは、起動時からの続き。

Accepted.
Message from client: Hello World
--------1.7.0_171--------
Waiting for SSL connection
^C
$ 

サーバー証明書が問題なく取得できた。クライアントからのメッセージ「Hello World」もちゃんと送られている。

接続テスト(サーバー:OpenJDK 1.8.0)

  • 起動
$ sudo alternatives --config java

2 プログラムがあり 'java' を提供します。

  選択       コマンド
-----------------------------------------------
*  1           java-1.8.0-openjdk.x86_64 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/bin/java)
 + 2           java-1.7.0-openjdk.x86_64 (/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/bin/java)

Enter を押して現在の選択 [+] を保持するか、選択番号を入力します:1
$ java SSLServer
--------1.8.0_151--------
Waiting for SSL connection
  • クライアントから接続
$ echo "Hello World" | openssl s_client -connect localhost:8443 -servername hhelibex.local. -showcerts
CONNECTED(00000003)
140566511749024:error:140773F2:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert unexpected message:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 313 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID: 
    Session-ID-ctx: 
    Master-Key: 
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1520441888
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---
$ 
  • サーバー側でのメッセージ出力

メッセージは、起動時からの続き。

Accepted.
javax.net.ssl.SSLProtocolException: Illegal server name, type=host_name(0), name=hhelibex.local., value=68:68:65:6c:69:62:65:78:2e:6c:6f:63:61:6c:2e
        at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:143)
        at sun.security.ssl.HelloExtensions.<init>(HelloExtensions.java:78)
        at sun.security.ssl.HandshakeMessage$ClientHello.<init>(HandshakeMessage.java:245)
        at sun.security.ssl.ServerHandshaker.processMessage(ServerHandshaker.java:220)
        at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026)
        at sun.security.ssl.Handshaker.process_record(Handshaker.java:961)
        at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1072)
        at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1385)
        at sun.security.ssl.SSLSocketImpl.readDataRecord(SSLSocketImpl.java:938)
        at sun.security.ssl.AppInputStream.read(AppInputStream.java:105)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at SSLServer.main(SSLServer.java:50)
Caused by: java.lang.IllegalArgumentException: Server name value of host_name cannot have the trailing dot
        at javax.net.ssl.SNIHostName.checkHostName(SNIHostName.java:319)
        at javax.net.ssl.SNIHostName.<init>(SNIHostName.java:183)
        at sun.security.ssl.ServerNameExtension.<init>(ServerNameExtension.java:137)
        ... 17 more
--------1.8.0_151--------
Waiting for SSL connection
^C
$ 

今度は例外が発生し、接続が切られている。もちろん、クライアント側では証明書の取得はできず。

検証

失敗したOpenJDK 1.8.0の場合の例外メッセージを見てみると、以下のようなメッセージが読み取れる。

  • 「Illegal server name, ・・・」
  • 「Server name value of host_name cannot have the trailing dot」

そこで、例外メッセージにあるjavax.net.ssl.SNIHostNameを見てみる。

すると、こんなことが書いてある。

TLS拡張(RFC 6066)のセクション3「Server Name Indication」で説明されているように、「HostName」には、クライアントが理解できるサーバーの完全修飾DNSホスト名が含まれます。ホスト名のエンコードされたサーバー名の値は、ASCIIエンコーディングを使用したドットで終わらないバイト文字列として表現されます。

また、

導入されたバージョン
1.8

「ドットで終わらないバイト文字列」!!

なるほど、RFC 6066がJava 8から実装され、ドットで終わるホスト名が許されなくなったということらしい。

これで冒頭の疑問が解ける。成功する方はJava 7なのでドットで終わるホスト名がまだ許されていたが、失敗する方はJava 8からのRFC 6066の実装によってドットで終わるホスト名が許されなくなった。

通常はドットで終わるホスト名でアクセスすることはないだろうから問題になることは少ないのだろうが、これは罠だなぁ‥ともあれ、疑問が解消されてよかった‥

参考

mb_encode_mimeheader/mb_decode_mimeheaderする際には内部文字エンコーディングに注意

マニュアルをちゃんと読むと書いてあるのだが。

前者のmb_encode_mimeheader()は、以下のように書いてある。

パラメータ

str
 エンコードする文字列。 mb_internal_encoding() と同じエンコーディングにしなければいけません。

つまり、マルチバイト文字を扱う限り、mb_internal_encoding()による内部文字エンコーディングの設定が必須なのだ。

ついでに、後者のmb_decode_mimeheader()のマニュアルも見てみる。

返り値

内部文字エンコーディングでデコードされた文字列を返します。

「内部文字エンコーディング」と明記してある。

というわけで、ちょっと試してみるために、以下のようなシナリオに沿ったプログラムを書いてみる。

共通部品/処理

<?php

/*
 * コマンド「od -c」を模したダンプを出力する関数。
 */
function od($str) {
    for ($i = 0; $i < strlen($str); ++$i) {
        if ($i % 16 === 0) {
            if ($i > 0) {
                echo PHP_EOL;
            }
            printf("%08o", $i);
        }
        $s = substr($str, $i, 1);
        if (ctype_graph($s) || $s === " ") {
            printf(" %3s", $s);
        } else {
            printf(" %03o", ord($s));
        }
    }
    if ($i % 16 !== 15) {
        echo PHP_EOL;
    }
    printf("%08o\n", $i);
}
<?php

// ** このファイルはもちろん「UTF-8」で保存する **

// 初期設定
mb_internal_encoding("UTF-8");

// メールで送る文字列の件名
$str = "あいうえおかきくけこさしすせそたちつてとなにぬねの";

シナリオ1

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

echo PHP_EOL;

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

実行結果。

// エンコードされた文字列
string(115) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREJEYk?=
 =?ISO-2022-JP?B?SCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(68) "あいうえおかきくけこさしすせそたちつてH$J$K$L$M$N"
00000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
00000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
00000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
00000060 343 201 241 343 201 244 343 201 246   H   $   J   $   K   $   L
00000100   $   M   $   N
00000104

あれ、文字化けした。

多分、mb_decode_mimeheader()でデコードするときに以下のような処理をしているのだろうと思われる。

<?php
mb_internal_encoding("UTF-8");
var_dump(
    mb_convert_encoding(base64_decode("GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREJEYk"), mb_internal_encoding(), "ISO-2022-JP")
    . mb_convert_encoding(base64_decode("SCRKJEskTCRNJE4bKEI="), mb_internal_encoding(), "ISO-2022-JP"));
string(68) "あいうえおかきくけこさしすせそたちつてH$J$K$L$M$N"

同じ結果になった。

というわけで、これではダメ。

シナリオ2

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// 内部文字エンコーディングを変更
$origInternalEncoding = mb_internal_encoding();
mb_internal_encoding("ISO-2022-JP");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

echo PHP_EOL;

// 内部文字エンコーディングを変更
mb_internal_encoding("ISO-2022-JP");

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

実行結果。

// エンコードされた文字列
string(123) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREGyhC?=
 =?ISO-2022-JP?B?GyRCJEYkSCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(56) "あいうえおかきくけこさしすせそたちつてとなにぬねの"
00000000 033   $   B   $   "   $   $   $   &   $   (   $   *   $   +   $
00000020   -   $   /   $   1   $   3   $   5   $   7   $   9   $   ;   $
00000040   =   $   ?   $   A   $   D   $   F   $   H   $   J   $   K   $
00000060   L   $   M   $   N 033   (   B
00000070

一見よさそうだが、ダンプを見ると、「ISO-2022-JP」になっている。システムの内部文字エンコーディングは「UTF-8」だったはずだ。

これもダメ。

シナリオ3

ソースコードは以下の通り。

<?php

include("od.php");
include("init.php");

// ** このファイルはもちろん「UTF-8」で保存する **

// 文字列の文字エンコーディング変換
$convStr = mb_convert_encoding($str, "ISO-2022-JP", "UTF-8");

// 内部文字エンコーディングを変更
$origInternalEncoding = mb_internal_encoding();
mb_internal_encoding("ISO-2022-JP");

// エンコード処理
$encStr = mb_encode_mimeheader($convStr, "ISO-2022-JP");
echo "// エンコードされた文字列" . PHP_EOL;
var_dump($encStr);

// 内部文字エンコーディングを戻す
mb_internal_encoding($origInternalEncoding);

echo PHP_EOL;

// デコード処理
$decStr = mb_decode_mimeheader($encStr);
echo "// デコードされた文字列" . PHP_EOL;
var_dump($decStr);
od($decStr);

実行結果。

// エンコードされた文字列
string(123) "=?ISO-2022-JP?B?GyRCJCIkJCQmJCgkKiQrJC0kLyQxJDMkNSQ3JDkkOyQ9JD8kQSREGyhC?=
 =?ISO-2022-JP?B?GyRCJEYkSCRKJEskTCRNJE4bKEI=?="

// デコードされた文字列
string(75) "あいうえおかきくけこさしすせそたちつてとなにぬねの"
00000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
00000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
00000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
00000060 343 201 241 343 201 244 343 201 246 343 201 250 343 201 252 343
00000100 201 253 343 201 254 343 201 255 343 201 256
00000113

よさそうである。文字化けしていないし、ダンプを見ても「UTF-8」になっている。念のため確認で以下を実行してみた(コンソールの文字エンコーディングUTF-8)。

$ echo -n 'あいうえおかきくけこさしすせそたちつてとなにぬねの' | od -c
0000000 343 201 202 343 201 204 343 201 206 343 201 210 343 201 212 343
0000020 201 213 343 201 215 343 201 217 343 201 221 343 201 223 343 201
0000040 225 343 201 227 343 201 231 343 201 233 343 201 235 343 201 237
0000060 343 201 241 343 201 244 343 201 246 343 201 250 343 201 252 343
0000100 201 253 343 201 254 343 201 255 343 201 256
0000113
$ 

同じ結果だ。

まとめ

というわけで、まとめると、以下のようにして各関数を呼び出さないといけない。