Node+WebSocket-Nodeで動画データ送信時にEPIPEが発生する

# はじめに

Node＋[WebSocket-Node][URL-WebSocket-Node]でクライアントにある動画ファイル(.mp4)をサーバーに送信するプログラムを書き動かしている際、とある送信タイミングで(クライアント側で)EPIPEが発生したので、その原因と対応内容について記載します。

https://github.com/theturtle32/WebSocket-Node

[URL-WebSocket-Node]:https://github.com/theturtle32/WebSocket-Node

## 送信していたデータ

クライアントから送信する動画ファイル(.mp4)は以下のようなものでした。

```
-rwxrwxrwx    1 root     root        164514 Apr 16 14:13 /path/data/20210416/14/1311.mp4
-rwxrwxrwx    1 root     root        157882 Apr 16 14:13 /path/data/20210416/14/1313.mp4
-rwxrwxrwx    1 root     root        266566 Apr 16 14:13 /path/data/20210416/14/1315.mp4
-rwxrwxrwx    1 root     root        296955 Apr 16 14:13 /path/data/20210416/14/1317.mp4
-rwxrwxrwx    1 root     root        175276 Apr 16 14:13 /path/data/20210416/14/1319.mp4
```

上記の1311.mp4, 1313.mp4, 1315.mp4の送信は問題なく行えますが、(とある送信タイミング＝＝)1317.mp4を送信するタイミングでEPIPEが発生しています。

## サーバー側のソース

```JavaScript:myServer.js
const WebSocketServer = require('websocket').server;
const http = require('http');

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(port, function() {
    console.log((new Date()) + ' Server is listening on port ' + port);
});

var wsServer = new WebSocketServer({
    httpServer: server,
    autoAcceptConnections: false,
});

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
        request.reject();
        return;
    }
    
    var connection = request.accept('echo-protocol', request.origin);

    connection.on('message', function(message) {
    });

    connection.on('close', function(reasonCode, description) {
    });
});
```

## クライアント側のソース

```JavaScript:myClient.js
    client = new WebSocketClient();
    client.connect(wss_server, 'echo-protocol', origin, wss_headers);
    client.on('connect', function(connection) {
        console.log('Event : Connected');

        connection.on('error', function(error) {
            console.log("Connection Error: " + error.toString());
        });

        connection.on('close', function() {
            console.log('Connection Closed');
        });
```

### エラーになった時のクライアント側のコンソールログ

```Text:クライアント側のコンソールログ
Connection Error: Error: write EPIPE
Connection Closed
```


# 原因

先に書いてしまいますが、原因は送信したデータのサイズが、WebSocket-Node(WebSocketServer)の最大メッセージサイズ(maxReceivedMessageSize)を超えていたためでした。

| コンポーネント | パラメータ | デフォルト値 | 実際のデータサイズ |
|:-:|:-:|:-:|:-:|
| WebSocketServer | maxReceivedMessageSize | 0x100000（1MB） | 1MB以上？？？ |
| WebSocketClient | maxReceivedMessageSize | 0x800000（8MB） | - |

今回問題となったのは送信時(サーバーが受信した時)ですのでWebSocketServerのmaxReceivedMessageSizeのサイズが影響してきますが、参考までにWebSocketClient のmaxReceivedMessageSizeも載せておきます。サーバーからの送信時(クライアント側で受信した時)にEPIPEが発生した場合は、この辺りを疑ってください。

# 解決策

WebSocketServerのmaxReceivedMessageSizeを3MBに変更します。(今回のケースでは2MBでも大丈夫だとは思いますが、ひとまず3MBにしました)

>WebSocketClientの最大受信メッセージサイズ(maxReceivedMessageSize)のデフォルトは8MBです。
>なぜWebSocketServerのmaxReceivedMessageSizeのデフォルトは1MBなのか、、、。
>Webサーバーとブラウザの関係から来ているのでしょうかね。(Webサーバーにある動画ファイルをブラウザで再生するようなケースを想定しているなど)

# 対応コード

WebSocketServerのconfigにて、maxReceivedMessageSizeに3MB（0x100000 * 3）を追加しました。

```JavaScript:myServer.js
var wsServer = new WebSocketServer({
    httpServer: server,
    autoAcceptConnections: false,
    maxReceivedMessageSize: 0x100000 * 3     ・・・  この行を追加
});
```

maxReceivedMessageSizeに設定する値は10進で 3145728 でもよいですが、WebSocketServerのソースを見ると以下のようになっていましたので、踏襲した形で 0x100000 * 3 としました。

```JavaScript:WebSocket-Node：WebSocketServer.js
        // 1MiB max message size, only applicable if
        // assembleFragments is true
        maxReceivedMessageSize: 0x100000,
```

# 解決するまでの経緯

参考までに解決するまでの経緯を書きたいと思います。

## [WebSocket-Node][URL-WebSocket-Node] のソースを取得しチェック

[WebSocket-Node][URL-WebSocket-Node]のGithubにあるREADMEだけでは情報が不足していますのでソースをgit cloneし眺めてみます。EPIPEがヒットしないか検索したり、最大受信メッセージサイズをチェックしたり。

>実は最初っから最大受信メッセージサイズ辺りを疑っていました。
>ただ最初にも記載しましたが、送信していた動画ファイルのサイズが300KB弱であることと、maxReceivedMessageSizeのデフォルト値が1MBだったので問題ないと判断し、ここではスルーしてしまいました。(最終的にはここにたどり着くのですが。詳細はまた後記)

## デバッグログの追記

[WebSocket-Node][URL-WebSocket-Node]のソースを眺めても問題なさそうだったので、もう少し挙動を確認するため、ログを埋め込むことにしました。

クライアント側のソース(myClient.js)ではエラーの内容を出力していたのと、コンソールログに"Connection Closed"とも出ていたので、そうなるとクライアント↔サーバー間の接続が切れている＝＝サーバー側でも何かしらログが取れるかも、ということでサーバー側のロジック(myServer.js)を見てみます。

すると、、、

```JavaScript:myServer.js
    connection.on('close', function(reasonCode, description) {
    });
```

reasonCode, descriptionでもう少し詳細なログが取れそうなことが分かりました。そこで、、、

```JavaScript:myServer.js
    connection.on('close', function(reasonCode, description) {
        console.log('disconnected : reasonCode =', reasonCode, ", description =", description);
    });
```

として動作確認を行います。すると、、、

```Text:サーバー側のコンソールログ
disconnected : reasonCode = 1009 , description = Maximum message size exceeded.
```

が出ました。”WebSocket 1009”で検索してもヒットしますが、description にある通りメッセージのサイズオーバーが原因のようです。(あれあれ？やはり動画ファイルのサイズが影響している？？？)

改めて送信側(myClient.js)のロジックを見直してみると、、、

```JavaScript:myClient.jsの送信ロジックあたり
        fs.readFile(movieFilePath, function(err, data) {
            if (err) {
                console.error("Failed to publish : " + err);
                if (callback) callback();
                return;
            }

            var content = {
                "from": myId(),
                "to": getRequesterId(),
                "clientType": "client",
                "request": "send_movie_file",
                "fileKey": fileKey,
                "movieIndex": movieIndex,
                "data": msgpack.encode(data)
            };

            // Websocket通知
            wsAgent.sendContent(JSON.stringify(content));
```

！！

データのサイズが1MBを超えたのは msgpack.encode(data) を行っているためですね。

実は動画ファイルのデータだけではなく、その動画ファイルに対する付随情報も送信する必要があったため上記のロジックのようにJSON形式で送信していました。JSONでは動画データ(バイナリ)をそのまま送れないのでmsgpackを利用しています。

BASE64などでもそうですが、msgpackでもエンコードの過程でエスケープ文字などが付加されるため、最終的なデータのサイズは元のサイズより増えます。元は300KB弱の動画ファイルのデータがmsgpack化により1MBを超えてしまったのでしょう。

実際にJSON.stringify(content)のサイズをログに出してみると、、、

```Text:JSON.stringify(content)のサイズ
data size = 1070254
```

のようにmaxReceivedMessageSizeのデフォルト値である1MB(1,048,576Byte)に対して20KBオーバーになっていました。

そこで改めて[WebSocket-Node][URL-WebSocket-Node]のソースを再確認し、"対応コード"で記載した設定を追記しました。
するとEPIPEで送信できなかった1317.mp4も無事に送信できるようになりました。