WebSocketを用いたバイナリデータ送受信における、ブラウザ固有のバイトオーダー（Endianness a.k.a Byte ordering）の扱い方

# 背景
先日とある機会があり、WebSocketを用いて、バイナリデータをブラウザからサーバーに転送するコードを書きました。その際に、バイトオーダー（Endianness a.k.a Byte ordering）でつまずきました。

僕はウェブ上にある記事を色々漁って、「ブラウザ上にて、バイトオーダーに気をつけながらバイナリデータを扱う方法」を自分の中で理解しました。

ちょっくら、まとめてみようと思います。

# 僕が直面した問題

「Aブラウザ上で、Uint32Array型の変数を生成し、それをWebSocketを介してBブラウザ上で処理する」
といったプログラムを書きました。図にするとこんな感じ。

```bash
Aブラウザ
||
|| websocket
||
\/
サーバー
||
|| websocket
||
\/
Bブラウザ
```

その時、「バイトオーダー」に関する知識が抜けていた僕は、ざっくりと以下のようなコードを書きました。
とりあえず送信側のコードだけ・・・

```javascript
let ui32arr = new Uint32Array(2048);
...省略...
ui32arr[0] = 12345;
...省略...
socket.send(ui32arr.buffer); // socketはwebsocket
```

このコードは正常に動作する時もありますが、意図しない動作になる時もあります。
バグが含まれているコードです。

# バイトオーダー（Endianness a.k.a Byte ordering）とは？

バイトオーダーとは、コンピュータのメモリにマルチバイト数値を格納する際の、各バイトの格納順序です。主に以下の2つの格納順序があります。

- ビッグエンディアン（Big endian）
  - マルチバイト数値を最上位バイトから順番に格納します。
- リトルエンディアン（Little endian）
  - マルチバイト数値を最下位バイトから順番に格納します。

どういうこと？例で説明します。

### 例

```javascript
let a = new Uint32Array(2);
a[0] = 99998891;
a[1] = 14892758;
```
上記の配列の要素は、マルチバイト数値です。
（Uint32は、32ビットの符号なし正数、という意味です。）
従って、数値は4バイトで表現されます。
（1バイトは8ビットです。よって、4＝32/8で、4バイト。）

コンピュータのメモリ上では、99998891という数値は、4バイトの数値によって表現されています。14892758も同様です。

ビッグエンディアンでは、4バイトは、最上位バイトから順番にメモリに格納されます。
リトルエンディアンでは、4バイトは、最下位バイトから順番にメモリに格納されます。

<table>
  <tr>
    <td></td><td colspan=4 align=center>a[0]</td><td colspan=4 align=center>a[1]</td>
  </tr>
  <tr>
    <td align=right>格納されている数値</td>
    <td colspan=4 align=center border=1>99998891</td><td colspan=4 align=center border=1>14892758</td>
  </tr>
  <tr>
    <td></td>
    <td align=center colspan=1>上位</td>
    <td border=0></td>
    <td border=0></td>
    <td align=center colspan=1>下位</td>
    <td align=center colspan=1>上位</td>
    <td></td>
    <td></td>
    <td align=center colspan=1>下位</td>
  </tr>
  <tr>
    <td align=right>各バイトの値<br>（16進数表記）</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td align=right>ビッグエンディアン</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td align=right>リトルエンディアン</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
</table>

コンピュータのメモリ上に格納される全てのデータは、ビッグエンディアン、リトルエンディアン、どちらかのバイトオーダーによって、メモリ上に格納されます。

# このデータのバイトオーダーは何だ？と思ったら

このデータのバイトオーダーが、ビッグエンディアン、リトルエンディアン、どっちだろう？と思った時に、
「そのデータのバイトオーダーを判別する方法」を知っていることが大切です。

バイトオーダーは、様々な「仕様」により決められています。
（だから厄介なんだよなぁ・・・。どっちかに統一してくれよ・・・。）

### ファイルの仕様

ファイルフォーマットのエンコードの仕様として、バイトオーダーが決められている場合があります。
（場合があります、というか殆どの場合、決められているはずです。バイトオーダーが明確に定義されていないと、プログラムが書けないからです。）

|ファイル形式|バイトオーダー|
|----------|------------|
|wav|RIFFなら、そのwavファイルはリトルエンディアン。RIFXなら、そのwavファイルはビッグエンディアン。詳しくは[こちら](http://soundfile.sapp.org/doc/WaveFormat/)|
|png|ビッグエンディアン。詳しくは[こちら](https://www.w3.org/TR/2003/REC-PNG-20031110/#7Integers-and-byte-order)|
|ユニコードのテキスト|BOMにより判断。詳しくは[こちら](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E3%82%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%83%9E%E3%83%BC%E3%82%AF)|

### プログラムの仕様

そのプログラムの実行環境により決められている場合があります。

例えば、Javaのプログラムの実行環境であるJVMでは、基本的には、マルチバイト数値はビッグエンディアンとしてメモリ上に格納されます。

### CPUの仕様

上記にあげた仕様によりバイトオーダーが決められていない場合、或いは、「バイトオーダーはネイティヴのものに従う」といった記述がある場合、
そのコンピュータのCPUの仕様によりバイトオーダーが決まります。

僕はMacbook proを利用しているのですが、Macbook proのネイティヴバイトオーダーはリトルエンディアンです。なぜならIntelのCPUを利用しているからです。

# WebSocketを介したバイナリデータの送受信で気をつけること

JavaScriptには、バイナリデータを扱うために、[TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays)ビューと呼ばれる、マルチバイト数値を要素とする配列があります。

気をつけることべきことは、

> Typed array views are in the native byte-order (see Endianness) of your platform.

**TypedArrayビューは、プラットフォーム固有のネイティヴのバイトオーダーです。**

要するに、ブラウザがインストールされているCPUの仕様に依存するので、リトルエンディアン、ビッグエンディアン、どっちか分からないってことです。

# バイトオーダーを考慮せずに書かれたプログラムは、なぜ問題なのか？

「Aブラウザ上で、Uint32Array型の変数を生成し、それをWebSocketを介してBブラウザ上で処理する」
といったプログラムを、僕はこんな風に書きました。

```bash
Aブラウザ
||
|| websocket
||
\/
サーバー
||
|| websocket
||
\/
Bブラウザ
```

Aブラウザ

```javascript
let ui32arr = new Uint32Array(2048);
...省略...
ui32arr[0] = 12345;
...省略...
socket.send(ui32arr.buffer);
// socketはwebsocket
// ui32arr.bufferは、
// 実際にネイティヴメモリ上のに格納されている
// ui32arrデータ表わすバイト配列
```

Bブラウザ

```javascript
socket.onmessage = (event) => {
  let ui32arr = new Uint32Array(
    event.data, 0, event.data.length / Uint32Array.BYTES_PER_ELEMENT
  );
};
```

このコードは、AブラウザとBブラウザのバイトオーダーが同じ時には正常に動作します。
しかし、AブラウザとBブラウザのバイトオーダーが異なる時には意図しない動作になります。

なぜか？

すごい簡単な例で説明します。

### 例

Aブラウザ（リトルエンディアン）

```javascript
let ui32arr = new Uint32Array(2);
ui32arr[0] = 99998891;
ui32arr[1] = 14892758;
socket.send(ui32arr.buffer);
```

Bブラウザ（ビッグエンディアン）

```javascript
socket.onmessage = (event) => {
  let ui32arr = new Uint32Array(
    event.data, 0, event.data.length / Uint32Array.BYTES_PER_ELEMENT
  );
  console.log(ui32arr[0]);
  console.log(ui32arr[1]);
};
```

<table>
  <tr>
    <td></td><td colspan=4 align=center>ui32arr[0]</td><td colspan=4 align=center>ui32arr[1]</td>
  </tr>
  <tr>
    <td align=right>Aブラウザ<br>ui32arr</td>
    <td colspan=4 align=center border=1>99998891</td><td colspan=4 align=center border=1>14892758</td>
  </tr>
  <tr>
    <td align=right>各バイトの値<br>（16進数表記）</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td align=right>ui32arr.buffer<br>の中身<br>リトルエンディアン</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td align=right>websocketで送られているデータ</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td>Bブラウザ<br>ui32arr.bufferの中身<br>ビッグエンディアンで</br>読み出しちゃう<br>こりゃあかん！</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td align=right>各バイトの値<br>（16進数表記）</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td align=right>Bブラウザ<br>ui32arr</td>
    <td colspan=4 align=center border=1>2883384581</td><td colspan=4 align=center border=1>3594445568</td>
  </tr>
</table>

送信したデータと、受信したデータが異なる！バグです。

# 解決方法) DataViewを使う

問題の解決方法の1つは、[DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)を利用することです。

DataViewを利用することで、ネイティヴバイトオーダーがビッグエンディアンかリトルエンディアンかに関わらず、プログラマが意図したバイトオーダーでマルチバイト数値をメモリ上に書き出すことができます。

Aブラウザ（リトルエンディアン）

```javascript
let ui32arr = new Uint32Array(2);
ui32arr[0] = 99998891;
ui32arr[1] = 14892758;
// リトルエンディアンでマルチバイト数値をメモリ上に書き出す
let buf = new ArrayBuffer(ui32arr * Uint32Array.BYTES_PER_ELEMENT);
let dv = new DataView(buf);
dv.setInt32(0, ui32arr[0], true);
dv.setInt32(4, ui32arr[1], true);
// DataViewのsetInt32メソッド（第3引数true）を用いることで、
// bufは、必ずリトルエンディアンになる。
socket.send(buf);
```

Bブラウザ（ビッグエンディアン）

```javascript
socket.onmessage = (event) => {
  let dv = new DataView(event.data);
  let ui32arr = new Uint32Array(
    event.data.length / Uint32Array.BYTES_PER_ELEMENT
  );
  // リトルエンディアンでマルチバイト数値をメモリ上から読み出す
  ui32arr[0] = dv.getInt32(0, true);
  ui32arr[1] = dv.getInt32(4, true);
  // DataViewのgetInt32メソッド（第2引数true）を用いることで、
  // 必ずリトルエンディアンでデータが読み出せる。
  console.log(ui32arr[0]);
  console.log(ui32arr[1]);
};
```

<table>
  <tr>
    <td></td><td colspan=4 align=center>ui32arr[0]</td><td colspan=4 align=center>ui32arr[1]</td>
  </tr>
  <tr>
    <td align=right>Aブラウザ<br>ui32arr</td>
    <td colspan=4 align=center border=1>99998891</td><td colspan=4 align=center border=1>14892758</td>
  </tr>
  <tr>
    <td align=right>各バイトの値<br>（16進数表記）</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td align=right>buf<br>の中身<br>リトルエンディアン</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td align=right>websocketで送られているデータ<br>リトルエンディアン</td>
    <td>ab</td><td>dc</td><td>f5</td><td>05</td>
    <td>d6</td><td>3e</td><td>e3</td><td>00</td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td colspan=9></td>
  </tr>
  <tr>
    <td>Bブラウザ<br>ui32arr.bufferの中身<br>リトルエンディアンとして<br>読み出せた！</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td align=right>各バイトの値<br>（16進数表記）</td>
    <td>05</td><td>f5</td><td>dc</td><td>ab</td>
    <td>00</td><td>e3</td><td>3e</td><td>d6</td>
  </tr>
  <tr>
    <td align=right>Bブラウザ<br>ui32arr</td>
    <td colspan=4 align=center border=1>99998891</td><td colspan=4 align=center border=1>14892758</td>
  </tr>
</table>

# まとめ

得られた教訓は以下。

1. WebSocketでバイナリデータを送受信する場合、**必ず**WebSocketを介して送受信されるデータのバイトオーダーを、明確に定義し、実装すること。実装する際にはDataViewを使うこと。


# 参考文献

- [Typed Arrays: Binary Data in the Browser](https://www.html5rocks.com/en/tutorials/webgl/typed_arrays/)
  - JavaScriptでバイナリデータを扱うなら必読。過不足なく、初学者にとっても素晴らしくわかりやすい。