読者です 読者をやめる 読者になる 読者になる

カタカタブログ

SIerで働くITエンジニアがカタカタした記録を残す技術ブログ。Java, Oracle Database, Linuxが中心です。たまにRuby on Railsなども。

Java ComparableとComparatorの違いについて

Java

今回はJavaのオブジェクト同士を比較するための以下の二つのインタフェースの違いについて、勉強したことをまとめる。

  • java.lang.Comparable<T>インタフェース
  • java.util.Comparator<T>インタフェース

どちらもオブジェクトを順序付けし、比較するためのインタフェースだが、
今回は例として、以下のidnameフィールドだけを持つ従業員クラスEmployeeを比較することを考える。

class Employee implements Comparable<Employee> {
       private int id;
       private String name;
       public Employee(int id, String name) {
              this.id = id;
              this.name = name;
       }
       public int getId() {
              return this.id;
       }
       public String getName() {
              return this.name;
       }
       public String toString() {
              return this.id + ":" + this.name;
       }
       @Override
       public int compareTo(Employee emp) {
              return this.id - emp.id;
       }
}

このときに、TreeSetを使って、以下を実行する。
TreeSetは順序性を持つ集合であり、内部的にオブジェクト同士の比較を行っている。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>();
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);
    }
}

実行結果は以下が出力される。

[1:Sato, 2:Yamada, 3:Suzuki]

id順になっていることが分かる。

java.util.Comparator<T>インタフェース

java.util.Comparator<T>インタフェースの特徴は以下。

  • 二つのオブジェクトを比較するためのインタフェース
  • 比較処理を実装した新規クラスとしてインタフェースを実装する(ただし後述のComparator.comparingメソッドで生成可能)
  • 比較したいオブジェクト二つを引数に取るcompareメソッドをオーバーライドする
  • compare(T o1, T o2)は二つのオブジェクトの大小関係に応じて以下の値を返す
  • o1がo2より大きい => 正の値
  • o1がo2より小さい => 負の値
  • o1とo2は同じ大きさ => 0

・原則としてSerializableインタフェースも実装する※以下の実装では省略

今度は、従業員の名前の辞書順で従業員同士を比較するためのComparatorを実装する。
なお、Employeeクラスは先ほど実装したようにComparableインタフェースを実装したクラスのままとする。

class EmployeeNameComparator implements Comparator<Employee> {
       @Override
       public int compare(Employee emp1, Employee emp2) {
              return emp1.getName().compareTo(emp2.getName());
       }
}

このときに、以下を実行するとnameで辞書順にソートされた結果が得られる。
Employeeクラスは上でComparableを実装したため標準ではidでソートするが、
今回はTreeMapのコンストラクタにEmployeeNameComparatorを明示することでソートをnameの辞書順に変更している。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>(new EmployeeNameComparator());
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);
    }
}

実行結果は以下が出力される。

[1:Sato, 3:Suzuki, 2:Yamada]

nameの辞書順にソートされていることが分かる。

Comparator.comparingメソッド

今回、Compartorクラスを実装したクラスを新規に作成したが、
その場限りで順序を指定した場合などに毎回新規クラスを実装するのは煩わしい。
そこで、Java SE 8からはComparatorインタフェースのstaticメソッドとしてcomparingメソッドが追加された。
これはcompareメソッドを適切にオーバーライドしたComparatorクラスを生成するもので、以下のようなシグネチャを持つ。

static <T,U extends Comparable<? super U>>Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)

このままだと非常にわかりづらいが、要するに、
ソートキーとなる値を返すFunctionを引数に与えると、適切にそのキーで比較してくれるComparatorを返してくれるというものである。
これはラムダ式を使って簡単に書けるので、TreeMapコンストラクタの引数に直接書くことでシンプルに比較順を変更できる。

具体的に実装例を見た方が分かりやすいので、先ほど作ったEmployeeNameComparatorと同等の実装をしてみる。
ラムダ式には、比較したいキーとなる値を返す関数を指定すればよいので、getNameをメソッド参照すると簡潔に書ける。

public class Work {
    public static void main(String[] args) {
       Set<Employee> empSet = new TreeSet<>(Comparator.comparing(Employee::getName));
       empSet.add(new Employee(2, "Yamada"));
       empSet.add(new Employee(1, "Sato"));
       empSet.add(new Employee(3, "Suzuki"));
       System.out.println(empSet);  
    }
}

実行結果は以下が出力される。

[1:Sato, 3:Suzuki, 2:Yamada]

nameの辞書順にソートされていることが分かる。
同じ結果が得られるので、こちらの方がより簡潔で見やすい。
何より、キーを指定するのみで、生々しい比較ロジックを書く必要がないのが便利。

まとめ

Javaのオブジェクト比較のための二つのインタフェースの違いについて見た。
Comparableはそのオブジェクト自身が本来持つデフォルトの比較ルールを実装するのに対し、
Comparatorは外から特定クラスのオブジェクト同士の比較ルールの適用を可能とする。
さらに、ComparatorComparator.comparingメソッドを使えば、ラムダ式を使って簡潔に書けるので便利。

参考文献

Effective Java 第2版 項目12「Comparableの実装を検討する」

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

以上

Raspberry Pi 2にUSB-Bluetoothアダプタを接続しWindows・Macとペアリングさせてみた

Raspberry Pi Linux ネットワーク

Raspberry Pi 3ではすでにBluetoothは標準機能で搭載されているが、手元Raspberry Pi 2にはBluetooth機能がないので、
市販のUSB-Bluetoothアダプタを購入して試しにMacbookやWindows7のPCとペアリングさせてみた。

Bluetoothアダプタ選定

まず、Raspberry Pi 2に接続するためのUSB-Bluetoothアダプタを買いに行く。
Bluetoothにもいくつか規格があるが、最新のBluetooth 4.0はBLE(Bluetooth Low Energy)と呼ばれ省電力化され、IoT機器などの領域で注目を集めるようになった。
BLEはそれ以前のバージョンとは互換性がないが、アダプタ製品によってBLEしかサポートしないもの、従来のBluetoothしかサポートしないもの、両方サポートするものの3種類に分類され、それぞれ製品パッケージに以下のように表記されている。

  • Bluetooth: 従来のBluetoothのみサポート
  • Bluetooth Smart: BLEのみサポート
  • Bluetooth Smart Ready: 両方サポート

よって、今回は両方サポートしているBuffaloのBSBT4D09BK というモデルのBluetoothアダプタを購入。
価格も家電量販店で1,680円とお手頃。
確かにBluetooth Smart Readyと書いてある。

iBUFFALO Bluetooth4.0+EDR/LE対応 USBアダプター ブラック BSBT4D09BK

iBUFFALO Bluetooth4.0+EDR/LE対応 USBアダプター ブラック BSBT4D09BK

f:id:osn_th:20170109121508j:plain

Bluetoothアダプタ接続・確認

USBに買ったアダプタを取り付ける。
接続後、lsusbでデバイスを認識していることを確認する。

pi@raspberrypi:~ $ lsusb
Bus 001 Device 004: ID 7392:7811 Edimax Technology Co., Ltd EW-7811Un 802.11n Wireless Adapter [Realtek RTL8188CUS]
Bus 001 Device 005: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode) ★デバイス確認
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Bluetoothアダプタがデバイスとして認識されていることが確認できる。

続いてBluetooth機器を操作するツール(Bluetoothスタックというらしい)で有名なBluezを使って、
Bluetoothアダプタが認識されているか確認する。
Bluezはhcitoolhciconfigといったコマンドを使うのだが、Raspbian標準についているようで、すでにコマンドが実行できた。

pi@raspberrypi:~ $ hcitool | grep ver
hcitool - HCI Tool ver 5.23

もしコマンドが使えなかった場合は以下のコマンドでBluetooth系ツール一式をインストールする。

pi@raspberrypi:~ $ sudo apt-get update
pi@raspberrypi:~ $ sudo apt-get install bluetooth bluez blueman

hichconfigでデバイスがUP RUNNINGであることを確認する。

pi@raspberrypi:~ $ hciconfig
hci0:   Type: BR/EDR  Bus: USB
        BD Address: 00:1B:DC:XX:XX:XX  ACL MTU: 310:10  SCO MTU: 64:8
        UP RUNNING
        RX bytes:676 acl:0 sco:0 events:47 errors:0
        TX bytes:2472 acl:0 sco:0 commands:47 errors:0

Raspberry PiからWindows7-PCとペアリング

手元のWindows 7ノートPCはBluetoothアダプタがついているので、Raspberry Pi側からとりあえずこれとペアリングさせてみる。

まずbluetooth設定ツールを起動する。

pi@raspberrypi:~ $ bluetoothctl
[NEW] Controller 00:1B:DC:XX:XX:XX raspberrypi [default]

対話式のツールになっているので、scan onコマンドでBluetoothをスキャンする。

[bluetooth]# scan on
Discovery started
[CHG] Controller 00:1B:DC:XX:XX:XX Discovering: yes
[NEW] Device 28:C2:DD:XX:XX:XX MY-WINDOWS-PC ★
(・・・略)
[bluetooth]# scan off

scan onコマンドを実行すると、検知したBluetoothデバイスを次々にバックグラウンドで表示する。
目的のWindows-PCが検知できたらscan offコマンドでスキャンを停止する。

続いてペアリングを行う。
pairコマンドで上で確認したWindows-PCのBluetooth MACアドレスを引数に実行する。

[bluetooth]# pair 28:C2:DD:XX:XX:XX
Attempting to pair with 28:C2:DD:XX:XX:XX
[CHG] Device 28:C2:DD:XX:XX:XX Connected: yes

この時点でWindows-PC側でBluetooth接続許可通知が開くので、許可する。
f:id:osn_th:20170109121511p:plain
しばらく待つと、正常にBluetooth接続が完了。
f:id:osn_th:20170109121505p:plain
次回以降に自動接続するようにtrustしておく。

[bluetooth]# trust 28:C2:DD:XX:XX:XX
[CHG] Device 28:C2:DD:XX:XX:XX Trusted: yes
Changing 28:C2:DD:XX:XX:XX trust succeeded

Bluetooth設定ツール終了するにはexitと入力。

[bluetooth]# exit
[DEL] Controller 00:1B:DC:XX:XX:XX raspberrypi [default]

これでWindows7-PCとは正常にペアリングできた!

Macとペアリング

今度は手元のMacBook Proとペアリングさせてみる。
まずMacでBluetoothを検知可能な状態にするため、「システム環境設定」 > 「Bluetooth」のウィンドウを開く。
このウィンドウを開いた状態にしておくと、外部デバイスからBluetoothが検知可能な状態になる
f:id:osn_th:20170109121518p:plain
さきほどWindows-PCとペアリングしたときと同じようにbluetoothctlでスキャン・ペアリングを行う。

pi@raspberrypi:~ $ bluetoothctl
[NEW] Controller 00:1B:DC:XX:XX:XX raspberrypi [default]
[bluetooth]# scan on
Discovery started
[CHG] Controller 00:1B:DC:XX:XX:XX Discovering: yes
[NEW] Device F4:0F:24:XX:XX:XX MacBook Pro ★
[bluetooth]# scan off
[bluetooth]# pair 28:C2:DD:XX:XX:XX
Attempting to pair with 28:C2:DD:XX:XX:XX
[CHG] Device 28:C2:DD:E5:A2:19 Connected: yes

Mac側にペアリング要求画面が開くので、「ペアリング」を行う。
f:id:osn_th:20170109121529p:plain
f:id:osn_th:20170109121524p:plain
次回以降に自動接続するようにtrustし、終了。

[bluetooth]# trust F4:0F:24:XX:XX:XX
[CHG] Device F4:0F:24:XX:XX:XX Trusted: yes
Changing F4:0F:24:XX:XX:XX trust succeeded
[bluetooth]# exit
[DEL] Controller 00:1B:DC:XX:XX:XX raspberrypi [default]

これでMacともペアリングできた。

Raspberry Pi のペアリング状態確認

最後に、ペアリング状態を確認するには以下のコマンドで見ることができる。

pi@raspberrypi:~ $ bluetoothctl
[NEW] Controller 00:1B:DC:XX:XX:XX raspberrypi [default]
[NEW] Device F4:0F:24:XX:XX:XX MacBook Pro
[NEW] Device 28:C2:DD:XX:XX:XX MY-WINDOWS-PC 

ペアリングした二つの機器がDeviceとして表示されていることが確認できる!

まとめ

Raspberry Pi 2に市販のUSB-Bluetoothアダプタを接続し、WindowsとMacのPCとペアリングできるところまで見た。
まだ疎通しただけでBluetoothを使って何かしたわけではないが、これで同じようにBluetoothデバイスとも連携できるはず。

以上

iBUFFALO Bluetooth4.0+EDR/LE対応 USBアダプター ブラック BSBT4D09BK

iBUFFALO Bluetooth4.0+EDR/LE対応 USBアダプター ブラック BSBT4D09BK

Java ジェネリクスについて勉強したことのまとめ

Java

Javaのジェネリクスについて理解が浅かったため、今回Effective Java第2版を読んで勉強してみたことをまとめる。
ジェネリクスはJava SE 1.5から導入されたものだが、今回はJava SE 8の環境で検証している。

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

ジェネリクスとは

ジェネリクスはあるクラスを作る上で、型の情報を汎用的に持たせるためのJava SE 6からの仕組みである。
特にリストやハッシュ等のコレクションは任意の型のインスタンスをまとめることができるが、異なる型の要素を持ってはならないという制約がある。この場合、ジェネリクスを使って型情報を汎用化することで、コンパイラが型の安全性を保証できるようになっている。

ジェネリクスのクラスを宣言するときは以下の構文を使う。
Tの部分を型パラメータと言う。型パラメータ名は大文字一文字で、要素に使う型であればE、返り値に使う型であればR、それ以外であればT、複数使うのであればU、などを使うのが一般的のよう。

class MyClass<T> {
    // 略
}

ジェネリクスとコレクション

これを自前の簡単なリストMyListクラスを作って確認する。要素は内部で配列を使って管理する。
まずジェネリクスを使わない場合、任意の型の要素を扱うためにはObjectクラスの配列を宣言する必要があるが、これだと異なる型をリストに混在できてしまうのでよくない。

・ジェネリクスを使わないMyListクラス

class MyList {
     private Object[] elements;
     private static final int MAX = 256;
     private int size = 0;
     public MyList() {
          elements = new Object[MAX];
     }
     public void add(Object e) {
          elements[size++] = e;
     }
     public Object get(int index) {
          return elements[index];
     }
     public int size() {
          return this.size;
     }
}

これは以下のようにStringとint(Integer)が混在するリストを許すことになってしまう。この場合、リストから要素を取り出して使うには、Object型から実際の型にキャストしないといけないが、型が混在している可能性があるため、実行時にキャストエラーが発生してしまう恐れがある。このため、このリストは型安全とは言えないリストとなる。

MyList list = new MyList();
list.add("abc");
list.add("def");
list.add(123);
for(int i=0; i<list.size(); i++) {
     System.out.println(list.get(i));
}

そこでジェネリクス(総称型)を使って型情報をパラメータ化する。
リストに格納する要素の型を型パラメータEとして明示して使うことができるようになる。

・ジェネリクスを使ったMyListクラス

class MyList<E> {
     private E[] elements;
     private static final int MAX = 256;
     private int size = 0;
     public MyList() {
          @SuppressWarnings("unchecked")
          E[] es = (E[])new Object[MAX];
          elements = es;
     }
     public void add(E e) {
          elements[size++] = e;
     }
     public E get(int index) {
          return elements[index];
     }
     public int size() {
          return this.size;
     }
}

こうすると、リスト変数宣言時に型パラメータを指定できるので、
それ以外の型の要素をリストに入れようとするとコンパイルエラーとなる。

MyList<String> list = new MyList<>();
list.add("abc");
list.add("def");
list.add(123); //★コンパイルエラー: The method add(String) in the type MyList<String> is not applicable for the arguments (int)
for(int i=0; i<list.size(); i++) {
     System.out.println(list.get(i));
}

ちなみに、MyListのコンストラクタで以下のようにキャストしている箇所がある。

@SuppressWarnings("unchecked")
E[] es = (E[])new Object[MAX];
elements = es;

ジェネリクスはコンパイル時のみに使う情報で、実行時には消えてしまっているので、型変数のクラスや配列をnewすることはできない。elements = new E[MAX];と書きたいところだがコンパイルエラーとなってしまう。
そこで、Object型の配列で宣言したあとでE[]にキャストしている。ただしこれはコンパイラレベルでは型安全であることを保証できないため、以下の警告が出る。

Type safety: Unchecked cast from Object[] to E[]

しかし、今回のケースでは空のObject型配列をnewしてインスタンス変数のE[] elementsに代入するだけなのでキャストエラーが発生することはない。そのため、コンパイラに型安全であることを伝えて警告を非表示にするために、以下のアノテーションを変数宣言時に付与している。

@SuppressWarnings("unchecked”)

このようにジェネリクスを使うことで、型安全なコレクションクラスを使うことができる。

ジェネリクスの不変性

まず、共変(covariant)と不変(invariant)の定義を確認しておく。
共変は、より広義の型から狭義の型へ変換できる・互換性があることを意味している。
一方、不変は広義の型と狭義の型への変換はできず、互換性がないことを意味している。
これは配列は共変で、ジェネリクスは不変である、という意味を理解すると分かりやすい。例えば、Superクラスを継承したクラスをSubクラスとすると、
配列Super[]と配列Sub[]は継承関係があり共変であるが、リストListとリストListは継承関係はなく不変である。

そのことを以下のコードで確認する。

public class Work {
     public static void main(String[] args) {
          Super[] supers = new Super[3];
          Super[] subs = new Sub[3];

          List<Super> superList = new ArrayList<Super>();
          List<Super> subList = new ArrayList<Sub>(); //コンパイルエラー: Type mismatch: cannot convert from ArrayList<Sub> to List<Super>
     }
}
class Super {}
class Sub extends Super {}

Super[]で宣言した配列にはnew Sub[]を代入することができる。
一方で、Listnew ArrayListを代入すると、型不一致によるコンパイルエラーとなる。

ただし注意点として、この不変性はあくまでジェネリクスとしてListクラスを扱うときの問題であり、要素を扱う場合はポリモーフィズムを活用できる。
つまり、以下のようにSuperクラスのリストにSubクラスのインスタンスを要素として持つことは問題なく行える。

・コンパイルも実行も正常にできるコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          superList.add(new Sub());
     }
}

型パラメータのワイルドカード

ジェネリクスは不変であるため、Listの要素の継承関係、つまりポリモーフィズムを有効に使うためには別の仕組みを用いる必要がある。
例えば、さきほどのMyListに別のリストを引数にとって、そのリストの全要素を自身のリストに加えるgetAndAddAllというメソッドを考える。
上で見たように、superListはSuperクラスをのインスタンスを要素として保持するリストなので、Subクラスのインスタンスを格納することは問題なく行える。
しかしgetAndAddAll(MyList list)の引数listには不変性が働くので、MyListを引数に与えるとコンパイルエラーとなる。

・コンパイルエラーとなるコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          superList.getAndAddAll(subList);//コンパイルエラー: The method getAndAddAll(MyList<Super>) in the type MyList<Super> is not applicable for the arguments (MyList<Sub>)
      }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // 以下のメソッドを追加(それ以外はこれまでと同じなので省略)
     public void getAndAddAll(MyList<E> list) {
          for(int i = 0; i < list.size(); i++) {
               add(list.get(i));
          }
     }
}

そこで、このgetAndAddAllメソッドにMyListを与えて実行できるようにするためには、型パラメータにワイルドカードを使ってgetAndAddAll(MyList list)と指定する。
こうすると、引数に型パラメータを継承したジェネリクス・クラスを指定できるようになる。

・コンパイル・実行可能なコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          superList.getAndAddAll(subList);
      }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // メソッドの型パラメータを変更(それ以外はこれまでと同じなので省略)
     public void getAndAddAll(MyList<? extends E> list) {
          for(int i = 0; i < list.size(); i++) {
               add(list.get(i));
          }
     }
}

同様に、今度は引数のlistに自身の要素をすべて足し込むputAllというメソッドを考える。
これはさきほどとは逆に、引数として与える型は型パラメータEもしくはそのスーパークラスという意味で、putAll(MyList) listと書くと意図通りのコードになる。

・コンパイル・実行可能なコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          subList.putAll(superList);
     }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // 以下のメソッドを追加(それ以外はこれまでと同じなので省略)
     public void putAll(MyList<? super E> list) {
          for(int i = 0; i < list.size(); i++) {
               list.add(this.get(i));
          }
     }
}

Get&Put原則

? extends E? super Eのどちらを使うべきかを迷った場合はGet&Put原則という考え方がある。
値を取得するだけの場合、つまりgetのときは? extends Eを使い、
値を更新するだけの場合、つまりputのときは? super Eを使い、
その両方を行う場合は、?(つまり任意の型を扱える)を使う、というものである。

まとめ

Javaのコレクションを使う上で便利なジェネリクスについて、扱い方をまとめた。
Listを使う程度であれば普段はあまり意識することなく便利に利用できるジェネリクスだが、ライブラリ開発などでジェネリクスを活用するクラスを作りたい場合は、ちゃんと使い方を知っておく必要がある。
コレクションだけでなく、Java SE 8からのラムダ式や関数型インタフェースでもジェネリクスはよく見かけるので、型パラメータの扱い方を理解しておくこと、扱う上でも正しく使えるようになると思う。

参考文献

Effective Java 第2版 第5章「ジェネリクス」

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

以上

Raspberry Pi 2 でLチカ

Raspberry Pi 電子工作

Raspberry Pi 2はこれまで簡易サーバ的な使い方しかしてこなかったが、今回初めて電子工作的な使い方をしてみる。
電子工作の領域ではLEDを点灯されることを"Lチカ"と呼び、Hello World的な入門するときに最初に作るべきものという位置づけらしいので、
今回はまずこれをやってみる。
なお、すでにRaspberry Pi 3が出ているが、ピンの仕様等は同じらしいので同じやり方で大丈夫なはず。

準備

動くRaspberry Piはある前提で、電子工作用に下記の部品を調達した。

ブレッドボード
f:id:osn_th:20161215092555j:plain
ジャンパーピン オス/メス・オス/オス
f:id:osn_th:20161215092549j:plain
330オーム抵抗
f:id:osn_th:20161215092552j:plain
赤色LED
f:id:osn_th:20161201150155j:plain

配線

いよいよブレッドボードにこれらのパーツを組み合わせていく。
つなぎ方はいろんなところに情報があるので割愛するが、GPIOの3.3vピンとGPIO25をブレッドボードにつなぎ、
抵抗、LEDでそれぞれ回路が1周するように接続すればOK。

接続できたら、以下のコードを実行してLEDを点滅させてみる。
switch_gpio25.py

import RPi.GPIO as GPIO
from time import sleep

GPIO.setmode(GPIO.BCM)
GPIO.setup(25, GPIO.OUT)

try:
 while True:
  GPIO.output(25, GPIO.HIGH)
  sleep(0.5)
  GPIO.output(25, GPIO.LOW)
  sleep(0.5)

except KeyboardInterrupt:
 GPIO.output(25, GPIO.LOW)
 pass

GPIO.cleanup()

このPythonスクリプトを実行すると0.5秒間隔でLEDが点滅する。
Ctlr-Cで消灯して停止する。
f:id:osn_th:20161202104748j:plain

まとめ

以上、非常に簡易的な電子工作入門としてLEDを点灯させてみた。
初めて回路に触るときは少し緊張したが、うまく動くことがわかると他のものもいろいろつなげてみたくなったので、
今度はセンサーやカメラ等もつなげてみたい。

以上!

買ったもの

全ての部品は秋葉原の秋月電子で買いましたが、ブレッドボードは同じものはAmazonで買えるようです。

普通のブレッドボード

普通のブレッドボード

参考書籍

Raspberry Pi 2にWi-Fi USBアダプタで無線LAN接続したときのメモ

Raspberry Pi Linux ネットワーク

Wi-Fiが標準搭載されたRaspberry Pi 3も出ているこのご時世に今更感は否めないが、
今回自宅のRaspberry Pi 2にWi-Fi USBアダプタを使って無線LAN接続設定を行ったので、内容をメモ。

Wi-Fi USBアダプタ準備

今回はWi-Fi USBアダプタはRaspberry Piと相性が良さそうなこちらを購入した。
Wireless 802.11n USB Adapter (Edimax)

Wireless 802.11n USB Adapter

Wireless 802.11n USB Adapter

「Ideal for Raspberry Pi」と書かれており、理想的とのこと。
f:id:osn_th:20161201150114j:plain

デバイス疎通確認

まず、有線LANは接続したまま、このWi-Fi USBアダプタをRaspberry Piに取り付ける。
デバイスが認識されていることを確認する。

pi@raspberrypi:~ $ lsusb
Bus 001 Device 004: ID 7392:7811 Edimax Technology Co., Ltd EW-7811Un 802.11n Wireless Adapter [Realtek RTL8188CUS] ★認識されている
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

一番上の行から、認識されていることが分かる。
さらにワイヤレスデバイスとしても認識されていることを確認する。

pi@raspberrypi:~ $ iwconfig
wlan0  unassociated Nickname:"<WIFI@REALTEK>"
     Mode:Managed Frequency=2.412 GHz Access Point: Not-Associated
     Sensitivity:0/0
     Retry:off RTS thr:off Fragment thr:off
     Power Management:off
     Link Quality:0 Signal level:0 Noise level:0
     Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0
     Tx excessive retries:0 Invalid misc:0 Missed beacon:0

lo    no wireless extensions.

eth0   no wireless extensions.

wlan0という名前のネットワークデバイスが追加されており、正しく認識されている。

ネットワーク設定

続いて無線LAN接続するための設定を行う。
まず/etc/network/interfacesを編集する。

pi@raspberrypi:~ $ sudo vi /etc/network/interfaces

以下の内容で設定する。なお、"myssid"の部分は接続したいアクセスポイントのSSIDに適宜変更すること。

auto wlan0
allow-hotplug wlan0
iface wlan0 inet manual
wireless-essid myssid # ★SSIDステルスの場合必要
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

ここでwireless-essidという設定があるが、これはSSIDがブロードキャストしておらずステルスモードの場合に必要。
今回の自宅環境では必要となるため、この設定を入れている。

続いて、アクセスポイントに接続するための認証情報を登録する。
まず、アクセスポイントのSSIDとパスワードを引数にwpa_passphraseコマンドを実行する。
※下の"myssid"と"password"は適宜設定

pi@raspberrypi:~ $ wpa_passphrase myssid password
network={
    ssid="myssid"
    #psk="password"
    psk=d71c7bac9102d571ee553fe886cb3302e25e2282f238e4fdf0658ee56b7a23cf
}

するとパスワードが暗号化された上のようなレスポンスが表示されるので、これをコピーしておく。

この情報を/etc/wpa_supplicant/wpa_supplicant.confに無線LAN接続のための設定として記載する。

pi@raspberrypi:~ $ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

上でコピーした情報をベースにkey_mgmt以下の行を追記する。アクセスポイントの認証方法に応じた値を設定する。

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="myssid"
    psk=d71c7bac9102d571ee553fe886cb3302e25e2282f238e4fdf0658ee56b7a23cf
    key_mgmt=WPA-PSK
    proto=WPA2
    pairwise=CCMP TKIP
    group=CCMP TKIP
    priority=2
}

これで設定は完了。
あとは設定を反映させるために、ネットワークデバイスを再起動する。

pi@raspberrypi:~ $ sudo ifdown wlan0
pi@raspberrypi:~ $ sudo ifup wlan0

これでネットワークデバイスが起動し、IPアドレスがルータから割り当てられれば設定は完了。

ネットワーク接続確認

無線LANが使えるようになっているか確認する。

pi@raspberrypi:~ $ iwconfig
wlan0  IEEE 802.11bgn ESSID:"myssid" Nickname:"<WIFI@REALTEK>"
     Mode:Managed Frequency:2.452 GHz Access Point: AA:AA:AA:AA:AA:AA
     Bit Rate:72.2 Mb/s Sensitivity:0/0
     Retry:off RTS thr:off Fragment thr:off
     Power Management:off
     Link Quality=72/100 Signal level=96/100 Noise level=0/100
     Rx invalid nwid:0 Rx invalid crypt:0 Rx invalid frag:0
     Tx excessive retries:0 Invalid misc:0 Missed beacon:0

lo    no wireless extensions.

eth0   no wireless extensions.

ESSIDがmyssidになっているので、接続しているよう。
IPアドレスも確認する。

pi@raspberrypi:~ $ ifconfig wlan0
wlan0  Link encap:イーサネット ハードウェアアドレス aa:aa:aa:aa:aa:aa
     inetアドレス:192.168.11.12 ブロードキャスト:192.168.11.255 マスク:255.255.255.0
     inet6アドレス: fe80::c9ed:fbcc:6606:3e7a/64 範囲:リンク
     UP BROADCAST RUNNING MULTICAST MTU:1500 メトリック:1
     RXパケット:485 エラー:0 損失:182 オーバラン:0 フレーム:0
     TXパケット:314 エラー:0 損失:2 オーバラン:0 キャリア:0
   衝突(Collisions):0 TXキュー長:1000
     RXバイト:133078 (129.9 KiB) TXバイト:73727 (71.9 KiB)

正常にIPアドレスが割り当てられている!

あとは最後にルータの方から付与するIPアドレスを固定化しておけばOK。
これで有線LANケーブルを外し、再起動しても同じIPアドレスでRaspberry Piにアクセスできる。

まとめ

Rasperry Pi 2にWi-Fi USBアダプタを接続して、無線LANを使えるようにした!
これで持ち運びにも便利になった!

以上

Wireless 802.11n USB Adapter

Wireless 802.11n USB Adapter

AWSにCloudera ManagerでCDH(Hadoop)クラスタを構築してみた

Cloudera Manager Hadoop

AWSにEC2インスタンスを複数立てて、Cloudera ManagerをインストールしてCDHクラスタを構築してみた。
Hadoopのクラスタ構成を自前で組むと結構面倒らしいが、Cloudera Managerを入れるとかなり簡単に組むことができた。

環境情報は以下。

  • Redhat Enterprise Linux 6.5
  • Cloudera Manager 5.9.0

AWSインスタンス構築

Cloudera Managerの機能でEC2インスタンスを作ってクラスタ構成を構築してくれる機能もあるらしいが、
今回はそれは使わずにEC2インスタンスはあらかじめ作っておき、OSがインストールされたノードが4ノードある状態にCDHを導入する。

今回作ったAWSインスタンスは4ノードとし、一つを管理系、残りをデータノードとして使う。
OSはRHEL 6.5とし、t2.large(vCPU:2, Memory:8GB)を選択。
ディスクはルートデバイスを100GBに増やし、さらにデータ用に300GBのデバイスを一つ追加した構成にした。
あとはネットワーク(セキュリティグループ)の設定だが、ノード間通信は基本的に全て許可するようにしておいた。

事前準備

インストールをスムーズに進めるためにいくつか事前準備をしておく。

  • ホスト名解決

各ノードにホスト名を設定し、解決できるようにしておく。
今回は4ノードのホスト名をそれぞれ、cdh01cdh02cdh03cdh04とする。以下を設定。

  • /etc/hosts
  • /etc/hostname
  • /etc/sysconfig/network
  • /etc/cloud/cloud.cfg

SELinux無効

  • $ setenforce 0
  • /etc/selinux/config

公開鍵交換

  • 各ノードのrootユーザでパスワードなしsshログインができるように、ssh-keygenをして秘密鍵を~/.ssh/authorized_keysに登録。

NTP同期

  • ntpdが起動していなかったり時刻同期がされていないとあとでチェックに引っかかるので確認しておく。

yum最新化

  • $ yum update

その他

その他にもおそらく必要な条件はあるが、インストール時に各種チェックをしてくれるので、後はそこで拾うことにする。

CDHインストール

chd01にログインし、rootユーザでCloudera Managerをインストールする。

[root@cdh01 ~]# wget http://archive.cloudera.com/cm5/installer/latest/cloudera-manager-installer.bin
[root@cdh01 ~]# chmod +x cloudera-manager-installer.bin
[root@cdh01 ~]# ./cloudera-manager-installer.bin

対話式のインストーラが開始される。

Next
f:id:osn_th:20161130112410p:plain
Next
f:id:osn_th:20161130112421p:plain
Yes
f:id:osn_th:20161130112353p:plain
Next
f:id:osn_th:20161130112414p:plain
Yes
f:id:osn_th:20161130112418p:plain

f:id:osn_th:20161130112358p:plain
Cloudera Manager管理コンソールにログインするためのURLとユーザ名/パスワードが表示されているのでメモしておく。
OK
f:id:osn_th:20161130112355p:plain
OK
f:id:osn_th:20161130112401p:plain
これでCloudera Managerのインストールが完了。
あとはCloudera Managerにログインし、画面からCDHクラスタを構成する。

CDHクラスタ構成

インストール画面の最後にCloudera Managerのログイン情報をもとにログインする。
http://cdh01:7180/
admin / admin でログインできる。
f:id:osn_th:20161130112433p:plain
ライセンス条項を確認し問題なければチェックを入れ、続行
f:id:osn_th:20161130112535p:plain
エディション選択画面になる。今回はトライアルエディションとする。
続行
f:id:osn_th:20161130112528p:plain
続行
f:id:osn_th:20161130112531p:plain
ホスト名(例: cdh0[1-4]) で検索し、インストール対象のノードを検出する。
続行
f:id:osn_th:20161130112451p:plain
追加したいParcelコンポーネントがあれば追加し、続行
f:id:osn_th:20161130112439p:plain
チェックを入れ、続行
f:id:osn_th:20161130112430p:plain
シングルユーザモードにはチェックを入れず、続行
f:id:osn_th:20161130112503p:plain
cdh01ノードのrootユーザの秘密鍵(.ssh/id_pub)を指定し、続行
f:id:osn_th:20161130112444p:plain
ここで各ノードにクラスタソフトウェアがインストールされる。
問題なく完了したら、続行
f:id:osn_th:20161130112436p:plain
Parcelのインストールが開始されるので、追加したものがある場合はそれもインストールされる。
完了後、続行
f:id:osn_th:20161130112509p:plain
ホストが正常かどうかの検査が開始される。
ここで引っかかった検証項目ごとに対処法もかなり具体的に記載されているので、その指示に従って解消する。
解消後、再実行ボタンを押して全ての検証が通ったら、完了
f:id:osn_th:20161130112459p:plain
クラスタ・サービスを選択する。
今回はSparkも使いたいので「Sparkがあるコア」にし、続行
f:id:osn_th:20161130112447p:plain
ロール割り当てをカスタマイズする
今回はSecondaryNameNodeをcdh02に変えた以外は、デフォルトのままとした。
「ホスト別に表示」すると内容を確認できるので、問題なければ続行
f:id:osn_th:20161130112523p:plain
データベースセットアップでは、データベースホスト名などの各種情報は自動で入力されている。
テスト接続し、問題なければ続行
f:id:osn_th:20161130112454p:plain
各種パラメータ値を設定する。
今回はデフォルト値のまま、続行
なお、今回EC2に300GBのデバイスを追加し、/u01としてマウントしているのだが、
ちゃんとその領域をDataNodeのデータディレクトリとして使ってくれていることが分かる。
f:id:osn_th:20161130112539p:plain
各種サービスのセットアップが実行されるので、
問題なく完了したら、続行
f:id:osn_th:20161130112425p:plain
以上で完了
f:id:osn_th:20161130112520p:plain
Cloudera Managerのホーム画面で以下のように各種コンポーネントがグリーンになっていれば正常に起動している。
f:id:osn_th:20161130112513p:plain
以上でCDHクラスタ構築作業は完了。

動作確認

CDHクラスタが構築できたので、いくつかのコンポーネントを動作させてみる。

hiveを動かしてみる

まずは試しにhiveを動かしてみる。

まずテストファイル作成。

$ cat test.csv
id,value
1,aaa
2,bbb
3,ccc

hiveでテーブル作成し、テストファイルを取り込んで見る。

$ hive
2016-11-29 02:37:56,953 WARN [main] mapreduce.TableMapReduceUtil: The hbase-prefix-tree module jar containing PrefixTreeCodec is not present. Continuing without it.

Logging initialized using configuration in jar:file:/opt/cloudera/parcels/CDH-5.9.0-1.cdh5.9.0.p0.23/jars/hive-common-1.1.0-cdh5.9.0.jar!/hive-log4j.properties
WARNING: Hive CLI is deprecated and migration to Beeline is recommended.
hive> create database db1;
OK
Time taken: 0.259 seconds
hive> use db1;
OK
Time taken: 0.017 seconds
hive> create table table1 (id int, value string) row format delimited fields terminated by ',' lines terminated by '\n'
  > ;
OK
Time taken: 0.181 seconds
hive> load data local inpath 'test.csv' overwrite into table table1;
Loading data to table db1.table1
Table db1.table1 stats: [numFiles=1, numRows=0, totalSize=27, rawDataSize=0]
OK
Time taken: 0.971 seconds
hive> select * from table1;
OK
NULL  value
1   aaa
2   bbb
3   ccc
Time taken: 0.315 seconds, Fetched: 4 row(s)

問題なく動作しているよう。
HDFSでも確認する。

$ hadoop fs -ls /user/hive/warehouse/db1.db
Found 1 items
drwxrwxrwt - ec2-user hive     0 2016-11-29 02:44 /user/hive/warehouse/db1.db/table1

正しく認識しているよう。

Sparkを動かしてみる

hdfsユーザがインストール時に作成されているのでスイッチし、spark-shellコマンドを実行する。
※他のユーザだとHDFSの権限エラーになった

$ su - hdfs
$ spark-shell
Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel).
Welcome to
   ____       __
  / __/__ ___ _____/ /__
  _\ \/ _ \/ _ `/ __/ '_/
 /___/ .__/\_,_/_/ /_/\_\ version 1.6.0
   /_/

Using Scala version 2.10.5 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_67)
Type in expressions to have them evaluated.
Type :help for more information.
Spark context available as sc (master = yarn-client, app id = application_1480399559471_0003).
SQL context available as sqlContext.

scala> var lines = sc.textFile("/user/hive/warehouse/db1.db/table1")
lines: org.apache.spark.rdd.RDD[String] = /user/hive/warehouse/db1.db/table1 MapPartitionsRDD[1] at textFile at <console>:27

scala> lines.count
res1: Long = 4

先ほど取り込んだHDFS上のファイルの行数を集計するスクリプトが実行できた!

まとめ

以上でAWS上に構築したEC2インスタンスにCloudera Managerをインストールし、CDHクラスタを構築できた。
また、実際にhiveとSparkが動作することも確認できた。
インストール作業はCloudera Managerが全ノードに対して一元的に実施してくれるため、ノードの数が多くてもあまり手間は変わらなく、とても簡単に実施できる。
一方で、ノード数が増えるとインストールの事前作業であるホスト名やカーネルパラメータ設定などが手間になるため、
実際に運用する場合はChefやAnsibleのようなプロビジョニングツールが必須だと感じた

以上!

Hadoop関連書籍

Hadoop 第3版

Hadoop 第3版

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

Hadoop徹底入門 第2版 オープンソース分散処理環境の構築

SQL テーブル参照先の条件句 JOIN vs EXISTS

Oracle DB SQL

昨日の記事に続き、今日もSQLネタ。

さて、あるテーブルにある行を検索する際に検索条件が外部キーで参照した別テーブルの列にあり、かつそのテーブルの値をselectしない場合、
そのようなSQLは結合もしくはexists句を使う2パターンで表現できる。

今回はこの2パターンを検証する。

問題設定

環境は例によってOracle DB 12cのSCOTTスキーマのemp表とdept表を用いる。
データやテーブル構成は前記と同じなのでこちらを参照。
totech.hateblo.jp


今回は、「給与が3000以上である従業員が所属している部署の部署番号と部署名を一覧化する」SQLを考える。
ここで、「最終的にに取得したいデータは全てdept表にあり、emp表は絞込みのためにしか使わない」という点が重要。

以下のようなSQLを実行する。

select EMPNO, ENAME, SAL, DEPTNO
from SCOTT.EMP e
where sal >= 3000
order by e.deptno

このような結果が得られるので、部署番号10と20の部署番号、部署名が正解の結果となる。

EMPNO ENAME SAL DEPTNO
7839 KING 5000 10
7902 FORD 3000 20
7788 SCOTT 3000 20

パターン1: 結合を使う方法

比較的分かりやすいのはこのパターン。あまり考えずに書く場合はだいたいこうなる気がする。

select distinct
 d.deptno
, d.dname
from
 scott.emp e
, scott.dept d
where e.deptno = d.deptno
and e.sal >= 3000

JOIN句は使っていないが、emp表とdept表を結合してwhere句で絞り込んでいる。
見た目には分かりやすい。
emp表と結合してしまうと重複が発生してしまうため、最後にDISTINCTしているところがネックとなる。
実行結果は以下のようになり、正しい結果となっているよう。

DEPTNO DNAME
10 ACCOUNTING
20 RESEARCH

実行計画を見るとHASH UNIQUEによって重複削除を行っている。

----------------------------------------------------------------------------
| Id | Operation     | Name | Rows | Bytes | Cost (%CPU)| Time  |
----------------------------------------------------------------------------
| 0 | SELECT STATEMENT  |   |  3 | 144 |  6 (34)| 00:00:01 |
| 1 | HASH UNIQUE    |   |  3 | 144 |  6 (34)| 00:00:01 |
|* 2 | HASH JOIN    |   |  3 | 144 |  5 (20)| 00:00:01 |
|* 3 |  TABLE ACCESS FULL| EMP |  3 |  78 |  2 (0)| 00:00:01 |
| 4 |  TABLE ACCESS FULL| DEPT |  4 |  88 |  2 (0)| 00:00:01 |
----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 2 - access("E"."DEPTNO"="D"."DEPTNO")
 3 - filter("E"."SAL">=3000)

パターン2: exists句を使う方法

結合を使わずにexsits句で表現することもできる。
その場合のSQLはこのようになる。

select
 d.deptno
, d.dname
from
scott.dept d
where exists
 (select 1
 from scott.emp e
 where e.deptno = d.deptno
  and e.sal >= 3000)

from句には実際に値を取得したいdept表だけが残り、
where句の中のサブクエリとしてemp表を使っている。
見た目はサブクエリを使うせいでやや複雑になっているが、exsits句だと重複は発生しないのでDISTINCTをする必要はなくなった。
実行結果は以下のようになり、先ほどと同じ結果となった(順番は変わったが)。

DEPTNO DNAME
20 RESEARCH
10 ACCOUNTING

実行計画を見ておく。HASH JOIN SEMIによる結合が行われ、代わりに重複削除処理はなくなった。

---------------------------------------------------------------------------
| Id | Operation     | Name | Rows | Bytes | Cost (%CPU)| Time  |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT |   |  3 | 144 |  5 (20)| 00:00:01 |
|* 1 | HASH JOIN SEMI  |   |  3 | 144 |  5 (20)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPT |  4 |  88 |  2 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| EMP |  3 |  78 |  2 (0)| 00:00:01 |
---------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 1 - access("E"."DEPTNO"="D"."DEPTNO")
 3 - filter("E"."SAL">=3000)

まとめ(どちらがよいか)

個人的にはパターン2のexistsを使うほうが、
余計な結合と重複削除がないため、パターン1の結合を使うケースより早くなるケースが多いような気がする。
ただし前回の記事ほど一般的な結果とは言えないように思うので、
このような取得列がないテーブルを条件句で参照する場合は、結合ではなくexistsによる選択肢があることを忘れないようにしたい。

以上。

SQL グループごとの最大値を持つ行を取得するときは分析関数MAXを使う

Oracle DB SQL

SQLを書いていて、あるグループごとにある項目を集計して最大値を持つ行だけを検索したいときがある。
これまではあまり気にせずにgroup byと集計関数MAXを使ったサブクエリを検索条件に使うことで求めていたが、
パフォーマンスがよくないことがあり、今回改めて検証してみた。

結果、自分の中では分析関数MAXを使って検索条件とする方法がパフォーマンス的にもっともよいという結論に達した。

以下、Oracle DB 12cの環境で行った検証についてまとめてみる。
なおこの検証は自分の中では一般性を持つ結果だと考えているが、
データの特性やハードウェアスペックによって異なる結果となる可能性はあるため、必ずその環境で検証を行う必要がある。

問題設定

Oracle DB標準のサンプルスキーマであるSCOTTスキーマの従業員表(emp), 部署表(dept)を用いる。
部署ごとに、それぞれ給与が最大の従業員を求めるクエリを書き、パフォーマンスの観点で比較してみる。
なお、実行速度はデータ数が少なくほとんど差がないため、実行計画を見て比較する。

データの構成を示す。
・emp表

select
 e.EMPNO
, e.ENAME
, e.SAL
, e.DEPTNO
from SCOTT.EMP e
EMPNO ENAME SAL DEPTNO
7369 SMITH 800 20
7499 ALLEN 1600 30
7521 WARD 1250 30
7566 JONES 2975 20
7654 MARTIN 1250 30
7698 BLAKE 2850 30
7782 CLARK 2450 10
7788 SCOTT 3000 20
7839 KING 5000 10
7844 TURNER 1500 30
7876 ADAMS 1100 20
7900 JAMES 950 30
7902 FORD 3000 20
7934 MILLER 1300 10

・dept表

select
  d.DEPTNO
, d.DNAME
from SCOTT.DEPT d
DEPTNO DNAME
10 ACCOUNTING
20 RESEARCH
30 SALES
40 OPERATIONS

・emp表とdept表をDEPTNOで結合

select e.EMPNO, e.ENAME, e.SAL, d.DEPTNO, d.DNAME
from SCOTT.EMP e, SCOTT.DEPT d
where e.DEPTNO = d.DEPTNO
order by d.DEPTNO, e.SAL desc
EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7782 CLARK 2450 10 ACCOUNTING
7934 MILLER 1300 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7566 JONES 2975 20 RESEARCH
7876 ADAMS 1100 20 RESEARCH
7369 SMITH 800 20 RESEARCH
7698 BLAKE 2850 30 SALES
7499 ALLEN 1600 30 SALES
7844 TURNER 1500 30 SALES
7654 MARTIN 1250 30 SALES
7521 WARD 1250 30 SALES
7900 JAMES 950 30 SALES

改めてクエリの条件を書くと、「部署ごとに、それぞれ給与が最大の従業員を求める」SQLを書きたい。
つまり以下のような結果が得られるはずである。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

このような結果を取得するためのSQLについて考えてみる。

パターン1: GROUP BY + MAX集計関数を使う方法

これまで私が何も考えずに書いていたパターン。
サブクエリで部署番号(dept.DEPTNO)ごとに最大の給与(emp.SAL)を持つ従業員番号(emp.EMPNO)を求め、
その結果の部署番号と給与で従業員表を検索する。

select
 e1.EMPNO
, e1.ENAME
, e1.SAL
, d.DEPTNO
, d.DNAME
from
 SCOTT.EMP e1
, (select
     ee.DEPTNO
   , MAX(ee.SAL) MAX_SAL
  from SCOTT.EMP ee
  group by ee.DEPTNO) e2
, SCOTT.DEPT d
where e1.DEPTNO = d.DEPTNO
  and e1.DEPTNO = e2.DEPTNO
  and e1.SAL = e2.MAX_SAL
order by d.DEPTNO

結果は以下の通り。意図通りの検索結果になっている。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7788 SCOTT 3000 20 RESEARCH
7902 FORD 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

ここで実行計画を見ておく。

Plan hash value: 2079720973

------------------------------------------------------------------------------
| Id | Operation      | Name | Rows | Bytes | Cost (%CPU)| Time  |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT   |   |  65 | 7670 |  8 (25)| 00:00:01 |
|* 1 | FILTER       |   |   |   |      |     |
| 2 | SORT GROUP BY   |   |  65 | 7670 |  8 (25)| 00:00:01 |
|* 3 |  HASH JOIN     |   |  65 | 7670 |  7 (15)| 00:00:01 |
|* 4 |  HASH JOIN    |   |  14 | 1288 |  5 (20)| 00:00:01 |
| 5 |   TABLE ACCESS FULL| DEPT |  4 | 136 |  2 (0)| 00:00:01 |
| 6 |   TABLE ACCESS FULL| EMP |  14 | 812 |  2 (0)| 00:00:01 |
| 7 |  TABLE ACCESS FULL | EMP |  14 | 364 |  2 (0)| 00:00:01 |
------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 1 - filter("E1"."SAL"=MAX("EE"."SAL"))
 3 - access("E1"."DEPTNO"="EE"."DEPTNO")
 4 - access("E1"."DEPTNO"="D"."DEPTNO")

6,7行目よりEMP表のフルスキャンが2回走っていることが分かる。
これはサブクエリと本体でそれぞれEMP表を検索し、結合しているためである。

パターン2: MAX分析関数を使う方法

上のパターンはEMP表のフルスキャンが2回走っていたが、
なんとかこれを1回で済ませられないだろうか、という観点で考えた結果、
分析関数MAXで部署番号ごとの最大給与を算出した上で直接where句でその行の給与列と最大給与を比較して絞ってしまえばよい、という発想に至ったので、
さっそくSQLクエリを書いてみた。

select
 e1.EMPNO
, e1.ENAME
, e1.SAL
, d.DEPTNO
, d.DNAME
from
(select
  ee.EMPNO
 , ee.ENAME
 , ee.SAL
 , MAX(ee.SAL) OVER(partition by ee.DEPTNO) MAX_SAL
 , ee.DEPTNO
 from SCOTT.EMP ee) e1
, SCOTT.DEPT d
where e1.DEPTNO = d.DEPTNO
 and e1.SAL = e1.MAX_SAL
order by d.DEPTNO

MAX(ee.SAL) OVER(partition by ee.DEPTNO) MAX_SALのところがいわゆる分析関数で
GROUP BYと同じようにee.DEPTNOの値ごとにee.SALの値を集計し、最大値を求める。
これはOracle DB特有の書き方だが、他のMySQLなどのRDBMSでも似たようなことはできるらしい。

このSQLでも(FORTとSCOTTの順番が逆になったが) 同じ結果が得られる。

EMPNO ENAME SAL DEPTNO DNAME
7839 KING 5000 10 ACCOUNTING
7902 FORD 3000 20 RESEARCH
7788 SCOTT 3000 20 RESEARCH
7698 BLAKE 2850 30 SALES

さらに実行計画を見てみる。

Plan hash value: 2845603103

-----------------------------------------------------------------------------
| Id | Operation      | Name | Rows | Bytes | Cost (%CPU)| Time  |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT  |   |  14 | 1134 |  6 (34)| 00:00:01 |
| 1 | MERGE JOIN     |   |  14 | 1134 |  6 (34)| 00:00:01 |
|* 2 | VIEW       |   |  14 | 826 |  3 (34)| 00:00:01 |
| 3 |  WINDOW SORT   |   |  14 | 644 |  3 (34)| 00:00:01 |
| 4 |  TABLE ACCESS FULL| EMP |  14 | 644 |  2 (0)| 00:00:01 |
|* 5 | SORT JOIN     |   |  4 |  88 |  3 (34)| 00:00:01 |
| 6 |  TABLE ACCESS FULL | DEPT |  4 |  88 |  2 (0)| 00:00:01 |
-----------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

 2 - filter("E1"."SAL"="E1"."MAX_SAL")
 5 - access("E1"."DEPTNO"="D"."DEPTNO")
   filter("E1"."DEPTNO"="D"."DEPTNO")

EMP表のフルスキャンが1回(4行目)になり、COSTも8から6に減少している。
EMP表は従業員表なのでフルスキャンの回数が増えると劇的にパフォーマンスが悪くなるはずであり、
分析関数を用いた方法はフルスキャンの回数が減らせるので、明らかにパフォーマンスがよくなるはず。

まとめ

「グループごとに最大値を含む行のみを取得する」クエリを書く場合は、
GROUP BYとサブクエリで書くよりも、分析関数を利用したほうがパフォーマンス的によさそう。

以上!

Raspberry Pi 2とSoftEtherを使って自宅VPNを構築してみた

Linux ネットワーク Raspberry Pi

これまでヤマハやBUFFALOの市販のルータを使って自宅VPNを構築してきた。
totech.hateblo.jp
totech.hateblo.jp
しかし、先日BUFFALOのルータを買い換えたところ、PPTPサーバ機能がなくてVPNサーバとして使えなかったため、
Raspberry Pi 2を使ってソフトウェアVPNサーバを構築してみようと思い立った。

そこで、今回はSoftEther(ソフトイーサ)というソフトウェアVPNを使って自宅VPNを構築する。
SoftEtherクラウド上でVPNの経路を中継する仕組みのため、VPNサーバとVPNクライアント間で直接セッションを貼るのではなく、間にVPN Azureというクラウドサービスを使う。これは筑波大学が研究用途で公開している無料のサービスで、カタログスペック上はopenvpnよりも帯域や使いやすさで上回っているらしい。
また、クラウドサービスを利用することで、VPN機能のルータはもちろんグローバルIPアドレスすら不要となる。

よって、今回はRaspberry Pi 2上にSoftEtherのVPNサーバをインストールし、VPN Azureを使って中継し、Windows 7からSoftEtherのVPNクライアントを使って自宅LANにVPNに接続する。

基本的には以下の公式マニュアルを参照して構築した。
https://ja.softether.org/4-docs/1-manual/7/7.3

Raspberry PiにSoftEther VPN Serverインストール

まずはRaspberry Pi にVPNサーバを構築する。
SoftEtherのVPNサーバインストーラは以下からダウンロードできる。
http://www.softether.org/5-download
現時点の最新版である softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz をダウンロードした。

f:id:osn_th:20160806181937p:plain

以下、Raspberry Pi上でインストールする作業をrootユーザで行う。
まず、ダウンロードしたインストーラを展開する。

root@raspberrypi:~# pwd
/root
root@raspberrypi:~# ls -l softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz
-rw-r--r-- 1 root root 5646405  8月  4  2016 softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz

root@raspberrypi:~# tar xzvf softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz
vpnserver/
vpnserver/Makefile
vpnserver/.install.sh
vpnserver/ReadMeFirst_License.txt
vpnserver/Authors.txt
vpnserver/ReadMeFirst_Important_Notices_ja.txt
vpnserver/ReadMeFirst_Important_Notices_en.txt
vpnserver/ReadMeFirst_Important_Notices_cn.txt
vpnserver/code/
vpnserver/code/vpnserver.a
vpnserver/code/vpncmd.a
vpnserver/lib/
vpnserver/lib/libcharset.a
vpnserver/lib/libcrypto.a
vpnserver/lib/libedit.a
vpnserver/lib/libiconv.a
vpnserver/lib/libncurses.a
vpnserver/lib/libssl.a
vpnserver/lib/libz.a
vpnserver/lib/License.txt
vpnserver/hamcore.se2

実行可能ファイルを作成するため、コンパイルする。
途中でライセンスに関する確認が行われるので、回答する。

root@raspberrypi:~# cd vpnserver/
root@raspberrypi:~/vpnserver# make
--------------------------------------------------------------------

SoftEther VPN Server (Ver 4.21, Build 9613, ARM EABI) for Linux Install Utility
Copyright (c) SoftEther Project at University of Tsukuba, Japan. All Rights Reserved.

--------------------------------------------------------------------


Do you want to read the License Agreement for this software ?

 1. Yes
 2. No

Please choose one of above number:
1

(・・・略)
Did you read and understand the License Agreement ?
(If you couldn't read above text, Please read 'ReadMeFirst_License.txt'
 file with any text editor.)

 1. Yes
 2. No

Please choose one of above number:
1


Did you agree the License Agreement ?

1. Agree
2. Do Not Agree

Please choose one of above number:
1

make[1]: Entering directory '/root/vpnserver'
Preparing SoftEther VPN Server...
ranlib lib/libcharset.a
ranlib lib/libcrypto.a
ranlib lib/libedit.a
ranlib lib/libiconv.a
ranlib lib/libncurses.a
ranlib lib/libssl.a
ranlib lib/libz.a
ranlib code/vpnserver.a
gcc code/vpnserver.a -O2 -fsigned-char -lm -ldl -lrt -Wl,--no-warn-mismatch -lpthread -L./ lib/libssl.a lib/libcrypto.a lib/libiconv.a lib/libcharset.a lib/libedit.a lib/libncurses.a lib/libz.a -o vpnserver
ranlib code/vpncmd.a
gcc code/vpncmd.a -O2 -fsigned-char -lm -ldl -lrt -Wl,--no-warn-mismatch -lpthread -L./ lib/libssl.a lib/libcrypto.a lib/libiconv.a lib/libcharset.a lib/libedit.a lib/libncurses.a lib/libz.a -o vpncmd
./vpncmd /tool /cmd:Check
vpncmd コマンド - SoftEther VPN コマンドライン管理ユーティリティ
SoftEther VPN コマンドライン管理ユーティリティ (vpncmd コマンド)
Version 4.21 Build 9613   (Japanese)
Compiled 2016/04/24 16:39:47 by yagi at pc30
Copyright (c) SoftEther VPN Project. All Rights Reserved.

VPN Tools を起動しました。HELP と入力すると、使用できるコマンド一覧が表示できます。

VPN Tools>Check
Check コマンド - SoftEther VPN の動作が可能かどうかチェックする
---------------------------------------------------
SoftEther VPN 動作環境チェックツール

Copyright (c) SoftEther VPN Project.
All Rights Reserved.

この動作環境チェックツールを実行したシステムがテストに合格した場合は、SoftEther VPN ソフトウェアが動作する可能性が 高いです。チェックにはしばらく時間がかかる場合があります。そのままお待ちください...

'カーネル系' のチェック中...
              [合格] ○
'メモリ操作系' のチェック中...
              [合格] ○
'ANSI / Unicode 文字列処理系' のチェック中...
              [合格] ○
'ファイルシステム' のチェック中...
              [合格] ○
'スレッド処理システム' のチェック中...
              [合格] ○
'ネットワークシステム' のチェック中...
              [合格] ○

すべてのチェックに合格しました。このシステム上で SoftEther VPN Server / Bridge が正しく動作する可能性が高いと思われます。

コマンドは正常に終了しました。


--------------------------------------------------------------------
The preparation of SoftEther VPN Server is completed !


*** How to switch the display language of the SoftEther VPN Server Service ***
SoftEther VPN Server supports the following languages:
  - Japanese
  - English
  - Simplified Chinese

You can choose your prefered language of SoftEther VPN Server at any time.
To switch the current language, open and edit the 'lang.config' file.


*** How to start the SoftEther VPN Server Service ***

Please execute './vpnserver start' to run the SoftEther VPN Server Background Service.
And please execute './vpncmd' to run the SoftEther VPN Command-Line Utility to configure SoftEther VPN Server.

Of course, you can use the VPN Server Manager GUI Application for Windows / Mac OS X on the other Windows / Mac OS X computers in order to configure the SoftEther VPN Server remotely.


*** For Windows users ***
You can download the SoftEther VPN Server Manager for Windows
from the http://www.softether-download.com/ web site.
This manager application helps you to completely and easily manage the VPN server services running in remote hosts.


*** For Mac OS X users ***
In April 2016 we released the SoftEther VPN Server Manager for Mac OS X.
You can download it from the http://www.softether-download.com/ web site.
VPN Server Manager for Mac OS X works perfectly as same as the traditional Windows versions. It helps you to completely and easily manage the VPN server services running in remote hosts.

--------------------------------------------------------------------

make[1]: Leaving directory '/root/vpnserver'

正常に終了したら、VPN Serverのプログラムが生成されるので、それをvpnserverディレクトリごと/user/localに移動する。

root@raspberrypi:~/vpnserver# cd ..
root@raspberrypi:~# ls
softether-vpnserver-v4.21-9613-beta-2016.04.24-linux-arm_eabi-32bit.tar.gz  vpnserver
root@raspberrypi:~# mv vpnserver/ /usr/local/
root@raspberrypi:~# ls -l /usr/local/vpnserver/
合計 8784
-rwxrwxrwx 1 root root    1881  4月 24 08:11 Authors.txt
-rwxrwxrwx 1 root root    2859  4月 24 08:11 Makefile
-rwxrwxrwx 1 root root   30801  4月 24 08:11 ReadMeFirst_Important_Notices_cn.txt
-rwxrwxrwx 1 root root   36297  4月 24 08:11 ReadMeFirst_Important_Notices_en.txt
-rwxrwxrwx 1 root root   50695  4月 24 08:11 ReadMeFirst_Important_Notices_ja.txt
-rwxrwxrwx 1 root root   58932  4月 24 08:11 ReadMeFirst_License.txt
drwx------ 2 root root    4096  8月  4 07:39 chain_certs
drwxrwxrwx 2 root root    4096  8月  4 07:38 code
-rwxrwxrwx 1 root root 1295363  4月 24 08:11 hamcore.se2
-rw------- 1 root root     867  8月  4 07:38 lang.config
drwxrwxrwx 2 root root    4096  8月  4 07:38 lib
-rwxr-xr-x 1 root root 3742580  8月  4 07:38 vpncmd
-rwxr-xr-x 1 root root 3742656  8月  4 07:38 vpnserver

パーミッションを変えておく。

root@raspberrypi:~# cd /usr/local/vpnserver/
root@raspberrypi:/usr/local/vpnserver# chmod 600 *
root@raspberrypi:/usr/local/vpnserver# chmod 700 vpncmd
root@raspberrypi:/usr/local/vpnserver# chmod 700 vpnserver
root@raspberrypi:/usr/local/vpnserver# ls -l
合計 8784
-rw------- 1 root root    1881  4月 24 08:11 Authors.txt
-rw------- 1 root root    2859  4月 24 08:11 Makefile
-rw------- 1 root root   30801  4月 24 08:11 ReadMeFirst_Important_Notices_cn.txt
-rw------- 1 root root   36297  4月 24 08:11 ReadMeFirst_Important_Notices_en.txt
-rw------- 1 root root   50695  4月 24 08:11 ReadMeFirst_Important_Notices_ja.txt
-rw------- 1 root root   58932  4月 24 08:11 ReadMeFirst_License.txt
drw------- 2 root root    4096  8月  4 07:39 chain_certs
drw------- 2 root root    4096  8月  4 07:38 code
-rw------- 1 root root 1295363  4月 24 08:11 hamcore.se2
-rw------- 1 root root     867  8月  4 07:38 lang.config
drw------- 2 root root    4096  8月  4 07:38 lib
-rwx------ 1 root root 3742580  8月  4 07:38 vpncmd
-rwx------ 1 root root 3742656  8月  4 07:38 vpnserver

vpncmdというコマンドツールで、VPNサーバの動作チェックが行えるので、念のため行っておく。ちなみに上で実行可能ファイルを生成した時によく見ると同様のチェックがすでに行われている。

root@raspberrypi:/usr/local/vpnserver# ./vpncmd
vpncmd コマンド - SoftEther VPN コマンドライン管理ユーティリティ
SoftEther VPN コマンドライン管理ユーティリティ (vpncmd コマンド)
Version 4.21 Build 9613   (Japanese)
Compiled 2016/04/24 16:39:47 by yagi at pc30
Copyright (c) SoftEther VPN Project. All Rights Reserved.

vpncmd プログラムを使って以下のことができます。

1. VPN Server または VPN Bridge の管理
2. VPN Client の管理
3. VPN Tools コマンドの使用 (証明書作成や通信速度測定)

1 - 3 を選択: 3

VPN Tools を起動しました。HELP と入力すると、使用できるコマンド一覧が表示できます。

VPN Tools>help
下記の 6 個のコマンドが使用できます:
 About         - バージョン情報の表示
 Check         - SoftEther VPN の動作が可能かどうかチェックする
 MakeCert      - 新しい X.509 証明書と秘密鍵の作成 (1024 bit)
 MakeCert2048  - 新しい X.509 証明書と秘密鍵の作成 (2048 bit)
 TrafficClient - 通信スループット測定ツールクライアントの実行
 TrafficServer - 通信スループット測定ツールサーバーの実行

それぞれのコマンドの使用方法については、"コマンド名 ?" と入力するとヘルプが表示されます。
コマンドは正常に終了しました。

VPN Tools>check
Check コマンド - SoftEther VPN の動作が可能かどうかチェックする
---------------------------------------------------
SoftEther VPN 動作環境チェックツール

Copyright (c) SoftEther VPN Project.
All Rights Reserved.

この動作環境チェックツールを実行したシステムがテストに合格した場合は、SoftEther VPN ソフトウェアが動作する可能性が 高いです。チェックにはしばらく時間がかかる場合があります。そのままお待ちください...

'カーネル系' のチェック中...
              [合格] ○
'メモリ操作系' のチェック中...
              [合格] ○
'ANSI / Unicode 文字列処理系' のチェック中...
              [合格] ○
'ファイルシステム' のチェック中...
              [合格] ○
'スレッド処理システム' のチェック中...
              [合格] ○
'ネットワークシステム' のチェック中...
              [合格] ○

すべてのチェックに合格しました。このシステム上で SoftEther VPN Server / Bridge が正しく動作する可能性が高いと思われます。

コマンドは正常に終了しました。

VPN Tools>exit

すべてのチェックに合格していることを確認する。

ここから、VPNサーバを運用しやすくするため、スタートアップスクリプトに登録する。

root@raspberrypi:/usr/local/vpnserver# touch /etc/init.d/vpnserver
root@raspberrypi:/usr/local/vpnserver# vi /etc/init.d/vpnserver

公式マニュアル通り、以下の内容を記載する。

#!/bin/sh
# chkconfig: 2345 99 01
# description: SoftEther VPN Server
DAEMON=/usr/local/vpnserver/vpnserver
LOCK=/var/lock/subsys/vpnserver
test -x $DAEMON || exit 0
case "$1" in
start)
$DAEMON start
touch $LOCK
;;
stop)
$DAEMON stop
rm $LOCK
;;
restart)
$DAEMON stop
sleep 3
$DAEMON start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit 0

パーミッションも適切に設定する。

root@raspberrypi:/usr/local/vpnserver# chmod 755 /etc/init.d/vpnserver

chkconfigに登録する。当環境ではchkconfigが入っていなかったのでapt-getでインストールから行った。

root@raspberrypi:~# apt-get install chkconfig
root@raspberrypi:~# chkconfig vpnserver on
insserv: warning: script 'vpnserver' missing LSB tags and overrides

警告が出るが、以下を見る限り大した問題ではなさそうなので、ここでは無視する。
http://www.sssg.org/blogs/hiro345/archives/16237.html

root@raspberrypi:~# chkconfig vpnserver --list
vpnserver                 0:off  1:off  2:on   3:on   4:on   5:on   6:off

これで以下のコマンドでVPNサーバの起動・停止・ステータス確認が正常に行えるようになった。

# service vpnserver start
# service vpnserver stop
# service vpnserver status

vpnserverを起動させておく。

root@raspberrypi:~# service vpnserver start

これでRaspberry PiにSoftEther VPN Serverのインストールが完了した。

Windows 7マシンからSoftEther VPN Serverの設定

Raspberry Pi にインストールしたVPN Serverの設定を行う。
サーバ上で直接vpncmdを使ってもできるのだが、Win/Mac用のGUIの管理用ツールがあるので、各種設定はこちらから行う。
そのため、ここからはWindowsマシンを使って作業する。今回は後にVPNクライアントととしても使うことになるWindows 7端末を利用する。

VPNサーバと同じダウンロードページから softether-vpnserver_vpnbridge-v4.21-9613-beta-2016.04.24-windows-x86_x64-intel.exe をダウンロードする。
http://www.softether.org/5-download
f:id:osn_th:20160806181923p:plain

インストーラを展開し、実行する。
SoftEther VPN サーバー管理マネージャを選択し、次へ
f:id:osn_th:20160806182010p:plain
あとは、ライセンスやインストール先を指定するだけで、基本的に指示通りに「次へ」を選んでいくだけでインストールが進む。
完了し、管理マネージャを起動すると以下のようなGUIのツールが起動する。
「新しい接続設定」をクリックし、さきほどRaspberry Pi にインストールしたSoftEther VPN Serverの設定を行っていく。
f:id:osn_th:20160806181950p:plain
接続設定名はなんでもよいが、今回はraspi-vpnとした。なお、この先いろいろなところで各種設定の名前を入力するが、今回は全てraspi-vpnとしている。
ホスト名はVPN ServerをインストールしたサーバのIPアドレスもしくはホスト名を指定する。
f:id:osn_th:20160806181947p:plain
設定が作成されたので、「接続」する。
f:id:osn_th:20160806181955p:plain
初回ログイン時に管理パスワードの設定をする。
以降は管理ツールにアクセス時にそのパスワードを入力する(ちなみにこれはVPNユーザのパスワードとはまた別で、それはあとで別に設定する)。
f:id:osn_th:20160806181941p:plain
リモートアクセス VPNサーバーにチェックを入れ、次へ
f:id:osn_th:20160806181933p:plain
はい
f:id:osn_th:20160806182012p:plain
仮想HUB名もraspi-vpnにしておく
f:id:osn_th:20160806181958p:plain
固有のダイナミックDNSが割り当てられる。変更も可能なようだが、今回は特に変更せずにそのまま利用する。
f:id:osn_th:20160806182018p:plain
今回はVPN Azureを使ってクラウド経由のVPNを作るため、「VPN Azureを有効にする」にチェックを入れる。
ここで、上のダイナミックDNSに基づいた「現在のVPN Azureホスト名」が実際にVPNクライアントから接続するときのホスト名となるので、控えておいた上で、OK。
f:id:osn_th:20160806182021p:plain
続いてVPNユーザーを作成する。
f:id:osn_th:20160806182004p:plain
f:id:osn_th:20160806182014p:plain
最後に、VPN経由でLANにアクセスするために、ローカルブリッジ設定をする。
この設定を入れないとVPNでつないだときにリンクローカルアドレスが割り当てられてしまい、LAN内のサーバと通信ができない。
この設定を入れることで、VPNセッションをつないだときにLAN内のDHCPサーバ(ルータ)からIPアドレスが割り当てられる。
f:id:osn_th:20160806182034p:plain
仮想HUBに「物理的な既存のLANカードとのブリッジ接続」を選んで、「ローカルブリッジを追加」する。
f:id:osn_th:20160806181930p:plain
追加されたローカルブリッジが動作中になる

f:id:osn_th:20160806181921p:plain
以上で、VPN Server側の設定が完了した!

SoftEther VPN Clientセットアップ

VPNサーバ側のセットアップが完了したので、あとはVPNクライアントをセットアップして実際に接続してみる。
SoftEther VPN Clientもこれまでと同じようにダウンロードする。
f:id:osn_th:20160806182006p:plain
SoftEther VPN Clientをインストールする。
f:id:osn_th:20160806181952p:plain
クライアント側も指示どおりに「次へ」を選んでいくだけで、特に迷うことなくインストールできる。
VPNクライアントが起動したら、「新しい接続設定の作成」を選択する。
f:id:osn_th:20160806182000p:plain
はい
f:id:osn_th:20160806181943p:plain
ここでもraspi-vpnとする。
f:id:osn_th:20160806181927p:plain
つくられたraspi-vpn仮想LANカードをクリック
f:id:osn_th:20160806182024p:plain
さきほど作ったVPNサーバ側のホスト名やVPNユーザ情報など各種設定を入力し、OK。
f:id:osn_th:20160806182027p:plain

ここまででVPNサーバ側およびクライアント側の全設定が完了した。

VPN接続テスト

セットアップしたVPNが正しく動作するか検証する。
ここまで作業は自宅LAN内のルータの無線LANを使ってインターネットに出ていたが、正しくVPN経由で自宅LANにアクセスできることを確認するため、ここからはモバイルルータにネットワークを切り替えて作業する。

作成したVPNクライアントの接続設定を右クリックし、「接続」を選択する。
f:id:osn_th:20160806182031p:plain
VPN上のDHCPサーバからIPアドレスが取得される。
f:id:osn_th:20160806181945p:plain
IPアドレスが割り当てられ、接続完了となればOK。
f:id:osn_th:20160806182037p:plain

ipconfigを打つと、raspi-vpn仮想ネットワークカードに自宅LANのIPアドレスが割り当てられていることが確認できる。

>ipconfig

Windows IP 構成


イーサネット アダプター raspi-vpn - VPN Client:

   接続固有の DNS サフィックス . . . :
   リンクローカル IPv6 アドレス. . . . : xxxx::xxxx:xxxx:xxxx:xxxx%60
   IPv4 アドレス . . . . . . . . . . : 192.168.11.17
   サブネット マスク . . . . . . . . : 255.255.255.0
   デフォルト ゲートウェイ . . . . . : 192.168.11.1

Raspberry Pi 2 (192.168.11.11)にもアクセスできる。

>ping 192.168.11.11

192.168.11.11 に ping を送信しています 32 バイトのデータ:
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64
192.168.11.11 からの応答: バイト数 =32 時間 =1ms TTL=64

192.168.11.11 の ping 統計:
    パケット数: 送信 = 4、受信 = 4、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 1ms、最大 = 1ms、平均 = 1ms

sshもOK。

>ssh pi@192.168.11.11
pi@192.168.11.11's password:

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Aug  6 01:20:30 2016 from 192.168.11.4
pi@raspberrypi:~ $

sshでRaspberry Piにログインもできた。
正しくVPNが構築できているよう!

まとめ

今回はSoftEtherのVPN Azureを使って自宅VPNを構築した。
Raspberry PiをSoftEther VPN Server、Windows 7マシンをSoftEther VPN Clientとして環境構築、設定、疎通まで確認できた。
それほど難しい手順はなく(VPNやネットワークの基礎知識がないと辛いが)、管理ツールも使いやすいのでかなり便利だと思う。クラウドVPNを使うことでグローバルIPアドレスが不要になるなので、自宅ルータにVPN機能がなくても普通のマシンされあればVPN Server化できるので、構築のための敷居はかなり低いと感じた。

以上!

apt-get install時に「 パッケージ 'wireless-tools' のファイル一覧ファイルに最後の改行がありません」エラー

Linux Raspberry Pi

久しぶりにRaspberry Piを触った。
Raspberry Pi 2にapt-getでchkconfigを入れようとしたときによくわからないエラーが出たので、対策をメモ。

エラーと対応方法

apt-getでchkconfigを入れようとしたときに、wireless-toolsパッケージ周りでエラーが発生し、インストールに失敗するという問題が発生。

# apt-get install chkconfig
(・・・略)
dpkg: 復旧不可能な致命的なエラーです。中止します:
パッケージ 'wireless-tools' のファイル一覧ファイルに最後の改行がありません
E: Sub-process /usr/bin/dpkg returned an error code (2)

apt-get update, upgradeをやってから再実行するも変わらず。

wireless-toolsのファイル一覧ファイルがおかしいということなのでファイルの中身を確認してみる。

# file /var/lib/dpkg/info/wireless-tools.list
/var/lib/dpkg/info/wireless-tools.list: PNG image data, 38 x 25, 8-bit/color RGBA, non-interlaced

なぜかPNG画像ファイルになっている。。
普通は以下のようにテキストファイルのはず。つまり、wireless-tools.listが破損してしまっているよう。

# file /var/lib/dpkg/info/*.list | head
/var/lib/dpkg/info/acl.list:                 ASCII text
/var/lib/dpkg/info/adduser.list:               ASCII text
/var/lib/dpkg/info/adwaita-icon-theme.list:          ASCII text
/var/lib/dpkg/info/alacarte.list:               ASCII text
/var/lib/dpkg/info/alsa-base.list:              ASCII text
/var/lib/dpkg/info/alsa-utils.list:              ASCII text
/var/lib/dpkg/info/apt-utils.list:              ASCII text
/var/lib/dpkg/info/apt.list:                 ASCII text
/var/lib/dpkg/info/aptitude-common.list:           ASCII text
/var/lib/dpkg/info/aptitude.list:               ASCII text
# head /var/lib/dpkg/info/acl.list
/.
/usr
/usr/bin
/usr/share
/usr/share/man
/usr/share/man/man1
/usr/share/man/man1/chacl.1.gz
/usr/share/man/man1/getfacl.1.gz
/usr/share/man/man1/setfacl.1.gz
/usr/share/man/man5

正しいwireless-tools.listを探す。適当にググると以下にそれっぽいものを発見。
信頼性はまったくもって怪しいが、どうせ自宅利用のRaspberry Piなのでこれで試してみる。
https://github.com/volumio/RootFS-RaspberryPI/blob/master/var/lib/dpkg/info/wireless-tools.list

# cd/var/lib/dpkg/info
# mv wireless-tools.list wireless-tools.list.org
# viwireless-tools.list

wireless-tools.listの中身は以下のようになった。

/.
/sbin
/sbin/iwgetid
/sbin/iwlist
/sbin/iwevent
/sbin/iwspy
/sbin/iwconfig
/sbin/iwpriv
/etc
/etc/network
/etc/network/if-pre-up.d
/etc/network/if-pre-up.d/wireless-tools
/etc/network/if-post-down.d
/etc/network/if-post-down.d/wireless-tools
/usr
/usr/share
/usr/share/doc/wireless-tools
/usr/share/doc/wireless-tools/changelog.Debian.gz
/usr/share/doc/wireless-tools/HOTPLUG-UDEV.txt.gz
/usr/share/doc/wireless-tools/PCMCIA.txt.gz
/usr/share/doc/wireless-tools/README.Debian
/usr/share/doc/wireless-tools/copyright
/usr/share/doc/wireless-tools/DISTRIBUTIONS.txt.gz
/usr/share/doc/wireless-tools/changelog.gz
/usr/share/doc/wireless-tools/README.gz
/usr/share/man
/usr/share/man/cs
/usr/share/man/cs/man8
/usr/share/man/cs/man8/iwspy.8.gz
/usr/share/man/cs/man8/iwpriv.8.gz
/usr/share/man/cs/man8/iwconfig.8.gz
/usr/share/man/cs/man8/iwevent.8.gz
/usr/share/man/cs/man8/iwgetid.8.gz
/usr/share/man/cs/man8/iwlist.8.gz
/usr/share/man/cs/man7
/usr/share/man/cs/man7/wireless.7.gz
/usr/share/man/fr.UTF-8
/usr/share/man/fr.UTF-8/man8
/usr/share/man/fr.UTF-8/man8/iwspy.8.gz
/usr/share/man/fr.UTF-8/man8/iwpriv.8.gz
/usr/share/man/fr.UTF-8/man8/iwconfig.8.gz
/usr/share/man/fr.UTF-8/man8/iwevent.8.gz
/usr/share/man/fr.UTF-8/man8/iwgetid.8.gz
/usr/share/man/fr.UTF-8/man8/iwlist.8.gz
/usr/share/man/fr.UTF-8/man7
/usr/share/man/fr.UTF-8/man7/wireless.7.gz
/usr/share/man/man8
/usr/share/man/man8/iwspy.8.gz
/usr/share/man/man8/iwpriv.8.gz
/usr/share/man/man8/iwconfig.8.gz
/usr/share/man/man8/iwevent.8.gz
/usr/share/man/man8/iwgetid.8.gz
/usr/share/man/man8/iwlist.8.gz
/usr/share/man/fr.ISO8859-1
/usr/share/man/fr.ISO8859-1/man8
/usr/share/man/fr.ISO8859-1/man8/iwspy.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwpriv.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwconfig.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwevent.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwgetid.8.gz
/usr/share/man/fr.ISO8859-1/man8/iwlist.8.gz
/usr/share/man/fr.ISO8859-1/man7
/usr/share/man/fr.ISO8859-1/man7/wireless.7.gz
/usr/share/man/man7
/usr/share/man/man7/wireless.7.gz

これでapt-get installを再実行する。

# apt-get install chkconfig
E: dpkg は中断されました。問題を修正するには 'dpkg --configure -a' を手動で実行する必要があります。

ファイルを手で直接いじったせいか、実行できなかったので、指示されたとおりのコマンドを実行する。

# dpkg --configure -a

再実行

# apt-get install chkconfig
(・・・略)
(データベースを読み込んでいます ... 現在 118819 個のファイルとディレクトリがインストールされています。)
.../chkconfig_11.4.54.60.1debian1_all.deb を展開する準備をしています ...
chkconfig (11.4.54.60.1debian1) を展開しています...
man-db (2.7.0.2-5) のトリガを処理しています ...
chkconfig (11.4.54.60.1debian1) を設定しています ...

今度は正常に完了した!

# which chkconfig
/sbin/chkconfig

ちゃんとchkconfigがインストールされた。

まとめ

なぜかapt-getであるパッケージのファイル一覧ファイルが壊れてしまったため、
そのパッケージに依存するパッケージのインストールに失敗してしまう、という問題が発生した。
ファイル一覧ファイル(xxx.list)を適当に拾ってきたものだが、差し替えたところ、正常にインストールできるようになった。

以上!