JavaScriptで学ぶHTTP通信〜HTTPStreaming編〜

# はじめに

本記事は、WebSocketを使わないPUSH通信の一方式である、Chunked transfer encodingによるHTTPStreamingのサンプルプログラムを示す

* サーバ側はRubyで書いたCGIプログラム
* クライアント側はバニラJavaScriptによるfetchAPIを使用したプログラムである
* HTTPStreamingにより、サーバ側で時間のかかる処理をしていても、<font color="red">実行終了を待たずに処理結果を<strong>その都度</strong>、クライアントに通知可能になる</font>

---
# Chunked transfer encodingによるHTTPStreaming

WebSocketを使わないPUSH通信の一方式。百聞は一見に如かずということで、[HTTP Streaming パターン テスト](http://tsujimotter.info/js_hack/http-streaming/)というデモサイトを紹介。このサイトの場合は、PHP/jQueryでの実装である。


## サーバからのレスポンス通信をどうすればいいのか

* レスポンスヘッダとしてTransfer-encoding: chunkedを送る
* サーバからPUSH通知する塊(chunk)毎に、chunkのサイズとchunkの中身を送る
* 具体的には次の通り。

>```
>Content-Type: text/plain
>Transfer-Encoding: chunked
>
>1
>a
>2
>aa
>3
>aaa
>4
>aaaa
>5
>aaaaa
>6
>aaaaaa
>7
>aaaaaaa
>8
>aaaaaaaa
>9
>aaa
aaaaaa
>a
>aaaaaaaaaa
>b
>aaaaaaaaaaa
>c
>aaaaaaaaaaaa
>d
>aaaaaaaaaaaaa
>e
>aaaaaaaaaaaaaa
>0
>[CRLF]
```

* chunkのサイズは<strong><font color="red">16進数</font></strong>で送る。
* 通信の終わりは<font color="red">0と空行</font>を送る。
* chunkのサイズとchunkが一致しない場合、クライアントはエラーになる
* このChunked transfer encodingは[HTTP/1.1の4.1節](https://tools.ietf.org/html/rfc7230#section-4.1)
で定義されている。次の通りである。

>```
chunked-body   = *chunk
                 last-chunk
                 trailer-part
                 CRLF
chunk          = chunk-size [ chunk-ext ] CRLF
                 chunk-data CRLF
chunk-size     = 1*HEXDIG
last-chunk     = 1*("0") [ chunk-ext ] CRLF
chunk-data     = 1*OCTET ; a sequence of chunk-size octets
```

## クライアントではどう受け取ればいいのか

* サーバ側ではRubyによるCGIプログラム
* クライアントはJavaScriptでfetchAPIを使用する実装を示す

* 一秒おきに、chunkを送るプログラム

```streaming.cgi
#!/usr/bin/ruby                                                                                                         

puts "Content-Type: text/plain"
puts "Transfer-encoding: chunked"
puts "\n"

15.times{|n|
  if(n > 0) then
    puts n.to_s(16) # 16進数
    puts 'a' * n
    STDOUT.flush
  end
  sleep 1
}

puts "0"
puts               # 空行
``` 

* クライアントではレスポンス通信の結果をReadStreamから得る
* 毎秒送られてくるchunkを、送られた都度で画面上に出力する

```index.js
function startStream(){
    const url = "./streaming.cgi"
    fetch(url)
        .then((res)=>{
            if(res.ok){
                const reader = res.body.getReader();
                let received = 0;
                const txt = new TextDecoder();

                reader.read().then(function processText({done, value}){
                    if(done){
                        console.log("Stream Complete")
                        return;
                    }
                    received += value.length;
                    const chunk = txt.decode(value);

                    let listItem = document.createElement('li');
                    listItem.textContent = 'Read ' + received + ' bytes. Current chunk = ' + chunk;
                    document.getElementById("result").appendChild(listItem);

                    return reader.read().then(processText);
                })

            }else{
                console.log(res);
            }
        })
        .catch((err)=>{
            console.log(err);
        })
}
```

```HTML
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title>HTTP streaming Test</title>
        <script src="./index.js"></script>
        <style></style>
    </head>
    <body>
        <h1>HTTP streaming Test</h1>

        <button onclick="startStream()">Streaming開始</button>
        <ul id="result"></ul>
    </body>
</html>
```

## 実行結果

![chunkedTransferEncoding.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192607/66ee2fdc-d52f-a765-bd5e-fd768a857383.gif)


---
# 参考情報まとめ

* tsujimotterのノートブック
    * [HTTP Streaming パターン ～ HTTPサーバーからデータをプッシュする方法](https://tsujimotter.hatenablog.com/entry/2014/03/05/181712)
    * [HTTP Streaming パターン テスト](http://tsujimotter.info/js_hack/http-streaming/)
* [WEBRick で HTTP streaming (Chunked transfer encoding)](https://qiita.com/ledsun/items/0c8936b7f34e3ee4d052)
* [HTTP/1.1 の Transfer-Encoding: chunked をビジュアライズするツール書いてみた](http://blog.nomadscafe.jp/2013/05/http11-transfer-encoding-chunked.html)