Azure IoT Hub デバイス ストリームを試してみた


# はじめに
こんにちは
1月下旬にパブリックプレビューが開始された、
IoT Hubの機能の一つデバイスストリームをご存知ですか？

今回は、実際に触ってみて、どういった機能なのか、
優れている点はどこか を確認していきます。

# デバイス ストリーム
とりあえず軽くMicrosoft Docsの受け売り説明

## 概要
デバイスストリームは双方向TCPトンネルの作成が簡単になる機能です。

デバイスとサービスの間をIoTHubが仲介することで、
ファイアウォールの後ろにあっても、プライベートネットワーク内にいても、
サービスからデバイスへのTCP接続ができるようになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/c512081b-52dc-2216-26cd-ff8c010721e7.png)
_Microsoft Docs|IoT Hub デバイス ストリーム (プレビュー) [^1]_

## 利点
### ファイアウォールフレンドリ
デバイスがIoTHubへ 443のアウトバウンド接続を作成するだけで、
ファイアウォールに穴をあけることなく、インバウンド(着信)の接続を受け取れます。

### 接続の簡略化
デバイスのIPアドレスを識別せず、デバイスIDのみでデバイスとの通信が開始できます。

### 認証
通常のIoTHubで提供される、デバイスやサービスの認証機能が利用できます

### 暗号化
デバイスストリームを通したい通信が暗号化されていない場合でも、
デバイスとIoTHub、サービスとIoTHubはそれぞれTLS接続なので、
経路はセキュアに保たれます。

### TCP/IPとの互換
いわゆる普通のソケット通信が可能です

## ストリーム作成の流れ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/93d457d5-f6d4-32d9-c2aa-f87c20a8ffbb.png)
_引用元:Microsoft Docs|IoT Hub デバイス ストリーム (プレビュー)[^2]_

1. デバイスがIoTHubに接続して、ストリームの開始要求リクエストの応答を待機するコールバックを登録します
1. サービスがIoTHubに接続して、デバイスIDを指定してストリームの開始要求を送信します
1. IoTHubがデバイスのコールバックを呼び出して通知します。デバイスは、ストリームの開始要求を受け入れるか拒否する事ができます。
1. デバイスはIoTHubのストリーミングエンドポイントにアウトバウンドTCPで接続し、WebSocketにアップグレードします
1. サービスは、ストリームの開始要求の結果通知を受け取り、ストリーミングエンドポイントにWebSocketで接続します。

## 使用するSDK
接続要求を開始する側は、Service用のSDKを使用します。
着信接続を受ける側は、Device用のSDKを使用します。

## 対応リージョン
パブリックプレビュー中は`米国中部`か`米国中部EUAP(?)`にIoTHubを作成すると利用できます。

## SKU
Standard(S1-3)と、Free(F1)で利用できます

## 費用
不明… テレメトリメッセージとかではなく、いろんなメトリック見ても接続開始時に操作分しかカウントされていないような。。
IoTHubの標準費用と、Azureからのアウトバウンド通信分の費用は少なからず発生する(はず)

# サンプルプログラムを試す
.NET Core、Node.js(サービスのみ)、C言語(デバイスのみ)の3種類が提供されています。
今回は.NET Coreで試します。

## 提供されているサンプル(.NET Core)
* [エコーサンプル](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Quickstarts/device-streams-echo)
プログラムでデバイスストリームを使用するサンプル
* [ローカルプロキシーサンプル](https://github.com/Azure-Samples/azure-iot-samples-csharp/tree/master/iot-hub/Quickstarts/device-streams-proxy)
SSHやRDPなどの既存の通信をトンネルするサンプル

今回はローカルプロキシーサンプルでSSHとSQLServerに接続してみます。

## SSH
まずはサンプルのそのまま、[チュートリアル](https://docs.microsoft.com/ja-jp/azure/iot-hub/quickstart-device-streams-proxy-csharp#ssh-to-a-device-via-device-streams)に従って、
AMQPで直接インターネットに出るパターンで試しました。
構成/設定値はこんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/29e3039c-d3ec-6194-df89-010587766577.png)

※SSHDはDeviceLocalProxyを動かすPCとは別で試しています(ラズパイ)

あっさりと接続できました。
スクショではまったく表現できないですが、遠隔地にあるなといった遅延を感じます
<img src="https://qiita-image-store.s3.amazonaws.com/0/260166/547249e0-0289-2fc6-b815-2fec8177e883.png" width=60% />

この時のTCP接続の状態は以下の通り。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/8f2876d4-be78-5f28-04f5-9f681eaa6f6b.png)

### 実験1 WebProxy経由にしてみる
社畜の足枷であるWebProxy経由で接続できれば素敵なので、試してみます。
今回は、WebProxyに私の愛してやまないFiddlerを使用しています。ハンドシェイクの過程を多少は可視化できますし。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/187701ac-1665-3512-10a3-335a50e8153f.png)


サンプルコードを少し修正します。
修正点
1. IoTHubへ接続する、ServiceClientとDeviceClientのコンストラクタでそれぞれWebProxyを使用するよう指定します
1. プロトコルはAMQPではなくて、AMQP over WebSocketを指定します

差分はこちら[github](https://github.com/HiroyukiSakoh/DeviceStream/compare/7873205ba433f9888c5f513822138807175c0f7c...7de77c0ba6b8e48000eb108e59285e65dbfbab83)

※ServiceClient側のWebProxy対応が不十分(?)で、一部ReflectionでWebProxy無理矢理指定してます


すべての通信がProxy経由になっていることが確認できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/d4375c48-4661-387b-d300-d28f1c736d42.png)

ストリームの開始要求のリクエストとレスポンスです
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/5debc286-a29c-7ada-6a0e-10cc321a5ee8.png)

URLを見ると、`twins`の中の`stream`なんですね。
また、ストリームエンドポイントのURLが返っていることが確認できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/4726e139-c080-3f04-be7d-b4c159222d46.png)

## SQLServer
### 実験2 構成そのまま
実験1と同様の構成で
クライアントにSSMS、サーバーをローカルに立てたSQLServerで試してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/c8eb2e2e-cb1c-d863-112f-ca630656e4e3.png)


SSMS上はタイムアウトしました。
<img src="https://qiita-image-store.s3.amazonaws.com/0/260166/209d9eaf-ac84-0eef-edf4-b887b334346b.png" width=50% />

SSMS,SQLServerのプロトコルの理解が不足しているようなので、
デバイスストリーム使用しない通信をWiresharkで観察してみましたが、
どうやらSSMSはソースポートの異なる複数のコネクションを張っているようでした。

Fiddlerからも、複数回デバイスストリーム要求を出していて、
2回目のデバイスストリーム開始要求が504応答となっていました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/9cfceb40-7257-b61b-2822-db8c3d457db7.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/3bdd134b-010e-7384-c435-a8cf6b9b1403.png)

### 実験3 複数のコネクションに対応する
デバイス側のサンプルプログラムは1回分のデバイスストリーム開始要求を受け取ったら、
次を待たないようになっていたため、繰り返し待ち受けるように変更しました。

差分はこちら[github](https://github.com/HiroyukiSakoh/DeviceStream/commit/15f507492f437567299ef2663f6e9e6cda44be7f)

うまくいきました。
<img src="https://qiita-image-store.s3.amazonaws.com/0/260166/fd2682c0-fa5e-a620-e59e-ba7579145a5f.png" width=50% />

## 実験4 通信速度を計測してみる
さて次は、通信速度がどの程度出るのか、遅延はどの程度となるか 試してみます。
＃米国中部-日本を往復するので遅延は当然ですが
以下の構成です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/260166/a83c00ae-c7bf-2dad-12dd-d9a2e57c67b1.png)

結果

```
Connecting to host 127.0.0.1, port 50001
[  4] local 127.0.0.1 port 53082 connected to 127.0.0.1 port 50001
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-1.00   sec  4.25 MBytes  35.6 Mbits/sec
[  4]   1.00-2.00   sec  7.00 MBytes  58.7 Mbits/sec
[  4]   2.00-3.00   sec  1.50 MBytes  12.6 Mbits/sec
[  4]   3.00-4.00   sec  0.00 Bytes  0.00 bits/sec
[  4]   4.00-5.00   sec  1.00 MBytes  8.39 Mbits/sec
[  4]   5.00-6.00   sec  1.50 MBytes  12.6 Mbits/sec
[  4]   6.00-7.00   sec  1.00 MBytes  8.39 Mbits/sec
[  4]   7.00-8.00   sec  1.00 MBytes  8.39 Mbits/sec
[  4]   8.00-9.01   sec  1.50 MBytes  12.5 Mbits/sec
[  4]   9.01-10.01  sec  1.00 MBytes  8.37 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bandwidth
[  4]   0.00-10.01  sec  19.8 MBytes  16.6 Mbits/sec                  sender
[  4]   0.00-10.01  sec  10.1 MBytes  8.49 Mbits/sec                  receiver

iperf Done.
```
比較対象がなく比較はできませんが、WebSocketに乗せる分のオーバーヘッドがあると思いますが、
普通に実用レベルの速度は出せそうです。

# まとめ
* Azure IoT Hub デバイス ストリームはTCP互換の接続が可能な機能
* SDKは双方向のWebSocket用Endpointを提供する所まで実施する
* WebSocketをプログラムで扱うなり、別のソース/宛先へ転送することもできる

## 何が優れているか
* デバイスもサービスも アウトバウンド443のみでTCPの相互通信が可能となる

## どういった使い方が想定できるか
* Edgeデバイスのリモートメンテナンスなどのサービスに活用

* アプリケーションに組み込んで、どんな環境でも通信できるライブラリとして使う
TeamViewerとかSkypeとか、StackOverflowのCEO Joel Spolskyが昔作ってた [Copilot](http://local.joelonsoftware.com/wiki/Project_Aardvark_Functional_Specification_(Jp))とか
考え方は昔からあるけど、PaaSとしてはないと思うので、
IoTHubの機能の一つではなく、独立したサービスとしてあっていいかもしれない。

* 社畜の足枷であるWebProxy対策としてつかう
SSHポートフォワーディングの中継サーバーの代わりとして、
IoTHubの認証+WebScoketで、SSH構えるより狙われにくそうなイメージがある。
Proxyを簡単に超えられるので、マネージド中継サーバー的な用途として考えると、強力な社畜の味方となりうる(か?)
→現状のSDKだけではなくて、Proxyのサンプルプログラムの異常系とかしっかり実装して、バイナリで提供してほしい所

# おわりに
仕組みを見て、
社会人の初めごろに読んだ[Joel on Software](https://www.amazon.co.jp/Joel-Software-Spolsky/dp/4274066304)を思い出した。
エンジニアとして必要なことの大半はこの本で学んだ 気がする。
テンポよく面白いので、飽きずに読めます。良書。
MS系技術が好きになったきっかけも、この本だったかもしれない。

Azure IoT Hub デバイス ストリーム
PublicPreview中なので、使ってみてフィードバックしましょう。
いろんな使い道がありそうな機能なので、今後の動向を要チェックです。

# 参考
[Microsoft Docs | IoT Hub デバイス ストリーム (プレビュー)](https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-device-streams-overview)
[Microsoft Docs | クイック スタート:C# プロキシ アプリケーションを使用した IoT Hub デバイス ストリーム経由の SSH または RDP (プレビュー)](https://docs.microsoft.com/ja-jp/azure/iot-hub/quickstart-device-streams-proxy-csharp)

[^1]: https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-device-streams-overview#overview
[^2]: https://docs.microsoft.com/ja-jp/azure/iot-hub/iot-hub-device-streams-overview#device-stream-creation-flow