[cocoa][swift]Web API通信の符号化について（ASN.1, JSON, MessagePack, ProtocolBuffers, FlatBuffers, Avro）

<p>本発表では、Web API 通信の基本と符号化について説明する。</p>

<p>HTTP通信<br />
Web API通信とは、Webブラウザでホームページを閲覧する際のHTTP通信と同じ物だ。サーバから得られた結果を文書としてWebブラウザで表示するか、プログラムで利用する為のデータを取得するかの違いでしかない。<br />
Webブラウザのアドレス欄に、「http://www.bitz.co.jp/index.html」と入力すると、Webブラウザはサーバに対して以下の要求を送る。</p>

<pre><code>GET /index.html HTTP/1.1
Host: www.bitz.co.jp
Accept: image/pict, image/postscript, */*
Accept-Language: en
Connection: Keep-Alive
Uset-Agent: ave-front/1.0
</code></pre>

<p>すると、サーバは、以下の応答を返す。</p>

<pre><code>HTTP/1.1 200 OK
Date: Sat, 25 Aug 2018 23:40:00 GMT
Server: Apache/1.3.9 (Unix)
Last-Modified: Fri, 24 Aug 2018 09:30:00 GMT
ETag: “12345-123-12345678”
Content-Length: 141
Content-Type: text/html
 
&gt;html&lt;
	&gt;head&lt;&gt;title&lt;INDEX&gt;/title&lt;&gt;/head&lt;
	&gt;body&lt;
		本文
	&gt;/body&lt;
&gt;/html&lt;
</code></pre>

<p>途中の空行より上がヘッダー部、下がボディ部となる。<br />
Webブラウザは、応答のボディ部のデータを表示している。<br />
Web API通信では、ボディ部に符号化されたデータが格納されることになる。
</p>

<p>ASN.1<br />
通信のデータ構造、エンコード、デコードを記述する記法で、たとえば、SNMPで利用されている。RSA公開鍵を生成する必要があって、ASN.1の知識が必要になったことがあったが、その際、調べた事を説明する。<br />
RSA公開鍵は、modulusとpublicExponentの二つのパラメータで構成されている。他のプラットフォームでは、この二つのパラメータからRSA公開鍵を生成するAPIが用意されていたのだが、iOSでは用意されていなかったので、独自に対応した。<br />
RSA公開鍵のASN.1での定義は以下のとおり。</p>

<pre><code>RSAPublicKey ::= SEQUENCE {
    modulus INTEGER, -- n
    publicExponent INTEGER -- e
}
</code></pre>

<p>ASN.1のエンコード方式は複数あるのだが、RSA公開鍵はDERということなので、これについて説明する。<br />
SEQUENCEやINTEGER等のオブジェクトの形式は以下のとおり。</p>

<table border="1">
<tr>
        <td>タグ</td>
        <td>長さ</td>
        <td>値</td>
</tr>
</table>

<p>タグの番号は以下のとおり。</p>

<table border="1">
<tr>
        <th>タグ</th>
        <th>タグ番号</th>
        <th>説明</th>
</tr>
<tr>
        <td>INTEGER</td>
        <td>0x02</td>
        <td>整数型</td>
</tr>
<tr>
        <td>SEQUENCE</td>
        <td>0x10</td>
        <td>構造体 / 配列</td>
</tr>
</table>

<p>オブジェクトのタグ欄には、タグ番号がそのまま格納されるのではなくて以下の形式となっている。</p>

<table border="1">
<tr>
        <th>8</th>
        <th>7</th>
        <th>6</th>
        <th>5</th>
        <th>4</th>
        <th>3</th>
        <th>2</th>
        <th>1</th>
</tr>
<tr>
        <td colspan="2">クラス<br />00: 汎用</td>
        <td>構造化フラグ<br /0: 単一型<br /1: 構造型</td>
        <td colspan="5">タグ番号</td>
</tr>
</table>

<p>7〜8bitのクラスは、汎用型のSEQUENCEとINTEGERのみなので、RSA公開鍵では00となる。<br />
6bitの構造化は、INTEGERは単一型(0)、SEQUENCEは構造型(1)となる。なので、タグ欄にはINTEGERにはタグ番号そのものが設定され、SEQUENCEは、00+1+10000＝0x30となる。<br />
長さ欄は、以下のとおり。</p>

<table border="1">
<tr>
        <th>8</th>
        <th>7</th>
        <th>6</th>
        <th>5</th>
        <th>4</th>
        <th>3</th>
        <th>2</th>
        <th>1</th>
</tr>
<tr>
        <td>0</td>
        <td colspan="7">長さ（127オクテット以下）</td>
</tr>
</table>

<p>8bitに収まらない場合は以下となる。</p>

<table border="1">
<tr>
        <th>8</th>
        <th>7</th>
        <th>6</th>
        <th>5</th>
        <th>4</th>
        <th>3</th>
        <th>2</th>
        <th>1</th>
</tr>
<tr>
        <td>1</td>
        <td colspan="7">長さ部の長さn</td>
</tr>
</table>

<table border="1">
<tr>
        <th>8</th>
        <th>7</th>
        <th>6</th>
        <th>5</th>
        <th>4</th>
        <th>3</th>
        <th>2</th>
        <th>1</th>
</tr>
<tr>
        <td colspan="8">トークン1</td>
</tr>
</table>

<p>・・・</p>

<table border="1">
<tr>
        <th>8</th>
        <th>7</th>
        <th>6</th>
        <th>5</th>
        <th>4</th>
        <th>3</th>
        <th>2</th>
        <th>1</th>
</tr>
<tr>
        <td colspan="8">トークンn</td>
</tr>
</table>

<p>JSON<br />
JSONは、JavaScriptのオブジェクトの表記法をベースとした軽量なデータ記述書式で、人にとっての読み書きしやすいのが特徴だ。</p>

<pre><code>{
    "name" : "Pear",
    "points" : 250,
    "description" : "A ripe pear."
}
</code></pre>

<p>CocoaでJSONを扱うために以前からJSONSerializationというクラスが用意されていたが、その後追加されたJSONEncoderとJSONDecoderを利用すれば、JSONの扱いもより一層簡単になる。<br />
以下は、構造体からJSONデータを生成するサンプルだ。</p>

<pre><code>struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}
 
let pear = GroceryProduct(name: "Pear", points: 250, description: "A ripe pear.")
 
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
 
let data = try encoder.encode(pear)
print(String(data: data, encoding: .utf8)!)
</code></pre>

<p>以下は、逆にJSONデータから構造体のインスタンスを得るサンプルだ。</p>

<pre><code>let json = """
{
    "name": "Durian",
    "points": 600,
    "description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!
 
let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)
 
print(product.name) // Prints "Durian"
print(product)
</code></pre>

<p>MessagePack<br />
MessagePackはJSON感覚で利用できる符号化の仕組みで、公開されて仕様をもとに本家以外からも各種環境のライブラリが提供されている。どの符号化方式を選ぶから、クライアント側の都合のみではダメで、サーバ側の環境向けのライブラリが用意されているかが重要だ。この条件に合致するMessagePackはJSONの次に採用される符号化の資格が十分あるが、我々Cocoaプログラマにとっての不満は、Objective-C / Swift のライブラリが充実していないということだ。<br />
なので、軽く触れる説明するにとどめる。</p>

<pre><code>{ “compact”:true, “schema@:0 }
</code></pre>

<p>これをMessagePackで符号化すると以下のとおり。</p>

<pre><code>[82][A7]compact [C3][A6]schema [00]</code></pre>

<dl>
  <dt>[82]</dt>
    <dd>2-element map</dd>
  <dt>[A7]compact</dt>
    <dd>7-byte string</dd>
  <dt>[C3]</dt>
    <dd>true</dd>
  <dt>[A6]schema</dt>
    <dd>6-byte string</dd>
  <dt>[00]</dt>
    <dd>integer 0</dd>
</dl>

<p>バイナリ書式なので、27bytesから18bytesに短縮。</p>

<p>ProtocolBuffers<br />
インタフェース定義言語（IDL）で構造を定義する符号化方式で、大規模なプロジェクトの場合、仕様書からIDLを出力し、IDLで定義した内容でクライアントとサーバが通信するという利点がある。<br />
Googleが開発したものだが、本家からObjective-C / Swiftのライブラリが提供されていないという欠点があったのだが、なんと、Appleから提供されるようになった！</p>

<a href="https://github.com/apple/swift-protobuf">Swift Protobuf</a>

<p>例。定義ファイル (.proto  ファイル) を用意する。</p>

<pre><code>syntax = "proto3";
 
message BookInfo {
   int64 id = 1;
   string title = 2;
   string author = 3;
}
</code></pre>

<p>Swiftコードを生成する。</p>

<pre><code>$ protoc --swift_out=. DataModel.proto
</code></pre>

<p>これを利用してエンコード / デコードする。</p>

<pre><code>// Create a BookInfo object and populate it:
var info = BookInfo()
info.id = 1734
info.title = "Really Interesting Book"
info.author = "Jane Smith"
 
// As above, but generating a read-only value:
let info2 = BookInfo.with {
    $0.id = 1735
    $0.title = "Even More Interesting"
    $0.author = "Jane Q. Smith"
  }
 
// Serialize to binary protobuf format:
let binaryData: Data = try info.serializedData()
 
// Deserialize a received Data object from `binaryData`
let decodedInfo = try BookInfo(serializedData: binaryData)
 
// Serialize to JSON format as a Data object
let jsonData: Data = try info.jsonUTF8Data()
 
// Deserialize from JSON format from `jsonData`
let receivedFromJSON = try BookInfo(jsonUTF8Data: jsonData)
</code></pre>

<p>FlatBuffers<br />
これもGoogle製の符号化方式で高パフォーマンスが特徴だ。<br />
ただ、本家からObjective-C / Swiftライブラリが提供されていないので紹介するにとどめる。</p>

<p>Apache Avro<br />
これも本家からObjective-C / Swiftライブラリが提供されていないということで、簡単な紹介にとどめる。<br />
ProtocolBuffersと比較される符号化方式となるが、特徴なのはC#がサポートされていて、Unityで利用できるということ。これで採用されているプロジェクトがあるようだ。
</p>

【関連情報】
<a href="https://cocoa-kanto.connpass.com/event/97193/">Cocoa.swift 2018-09</a><br />
<a href="https://cocoa-kanto.connpass.com/event/101034/">Cocoa.swift 2018-10</a><br />
<a href="https://cocoa-kanto.connpass.com/">Cocoa.swift</a><br />
<a href="https://www.facebook.com/groups/cocoa.kanto/">Cocoa勉強会 関東</a><br />
<a href="http://www.bitz.co.jp/weblog/">Cocoa練習帳</a><br />
<a href="https://qiita.com/m_yukio">Qiita</a><br />