Real World HTTPの輪読会をした part2

yagisuke.hatenadiary.com

で、「HTTP REAL WORLD」の7章のメモを記載したが、 2章のメモもあったので記載します。

HTTP/1.0のセマンティクス:ブラウザの基本機能の裏側

1章ではHTTPの基本機能が紹介された - メソッドとパス - ヘッダー - ボディー - ステータスコード

Webの高度化にともない多くの機能が追加されているが、 特にヘッダーで多くの機能が実現されている. ブラウザがこれらの基本要素をどのように応用して 基本機能を実現しているのかを2章では紹介しています.

フォーム送信について

  • シンプルなフォームの送信(x-www-form-urlencoded)
  • フォームを使ったファイルの送信(multipart/form-data)

シンプルなフォームの送信(x-www-form-urlencoded)

フォームを使ったPOST送信にはいくつかあるが、一番シンプルなものから紹介.

<form method="post">
  <input type="text" name="title">
  <input type="text" name="atuhor">
  <input type="submit">
</form>
$ curl -v --http1.0 -d title="Books Title" -d author="Books Author" http://localhost:18888

例: https://beauty.hotpepper.jp/catalog/ladys/

検索結果の並び順を変えるとフォームを使ってPOST送信され、 Content-Type:application/x-www-form-urlencoded が設定される.

urlエンコードについて

ブラウザは RFC1866 で定める変換フォーマットに従って変換をおこなうため、 以下の項目以外はエスケープが必要になる. - アルファベット - 数値 - アスタリスク - ハイフン - ピリオド - アンダースコア

GET送信の場合

POSTと違い、GETの場合はボディではなく、クエリーとしてURLに付与される.

フォームを使ったファイルの送信(multipart/form-data)

フォームを使ったPOST送信にはいくつかあるが、一番シンプルなものから紹介.

<form method="post" enctype="multipart/type">
  <input type="text" name="title">
  <input type="text" name="atuhor">
  <input type="file" name="sample">
  <input type="submit">
</form>
$ curl -v --http1.0 -F title="TITLE" -F auhtor="AUTHOR" -F attachment-file@test.txt  http://localhost:18888

参考:
フォームによるファイルアップロードの仕様
https://www.javadrive.jp/servlet/fileupload_tutorial/index2.html

300番台のリダイレクトの懸念事項

リダイレクトには懸念事項もある.

  1. URLは2000文字を目安にするべき(GETのクエリーには送信データ量に制限がある)
  2. データがURLに入るため送信した内容がアクセスログに残りセキュリティ的に懸念がある

上記回避策としてフォームを利用したリダイレクトがある.

自動リダイレクトするフォーム

<hrml>
<body action="next" onload="document.forms[0].submit()">
  <form method="post">
    <input type="hidden" name="id">
    <input type="hidden" name="password">
    ....
    <input type="submit">
  </form>
</body>
</hrml>

コンテントネゴシエーション

サーバーとクライアントは別々に開発されているため、両者が期待する形式や設定が常に一致しているとは限らない. そのため、1リクエストの中で両者がベストの設定を共有する仕組みが必要となり、それをコンテントネゴシエーションという. コンテントネゴシエーションはヘッダーを利用する.ネゴシエーションする対象とヘッダーは以下の通りとなっている.

リクエス レスポンス ネゴシエーション対象
Accept Content-Type MIMEタイプ
Accept-Language Content-Language 表示言語
Accept-Charset Content-Type 文字のキャラクターセット
Accept-Encoding Content-Encoding ボディーの圧縮

Accept: ファイル種類の決定

ChromeのAcceptは以下の通り. accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

わかりやすい画像を例に

  • image/webp
  • image/apng
  • /;q=0.8

q は品質係数と呼ばれ、0〜1で設定し、デフォルトは1となっている. これは優先度の高さを表し、サーバーは優先度が高く、対応しているフォーマットから返す. もしお互いに一致しているものがなかったら、406 Not Acceptableエラーとなる.

補足:

  • webp -> Googleが推奨するpngよりも2割程ファイルサイズが小さい画像フォーマット.
  • apng -> アニメーションするpng. gifに取って代わる次世代の新しい画像フォーマット.

Accept-Language: 表示言語の決定

GitHubのAccept-Languageは以下の通り.
Accept-Language:ja,en-US;q=0.8,en;q=0.6

ja,en-US,enの順でリクエストを送る. 言語情報を収める箱としてContent-Languageがあるが定義されているが 多くのWebサイトではそれを使っていなく、次のようにhtmlタグの中で返しているページが多いらしい. GitHubjaの日本語設定高くしているのって、時期に日本語対応してくれるのかなと思った.

<html lang="en">

Accept-Charset: キャラクターセットの決定

ヘッダーはご想像どおり以下のような感じ.
Accept-Charset: UTF-8,SHIFT_JIS;q=0.7,*;q=0.3

全キャラクターセットのエンコーダをブラウザが内包しているため、 モダンブラウザはAccept-Charsetを送信していない. なので、事前にネゴシエーションする必要がなくなったみたい.

HTMLのはHTTPヘッダーと同じ指示をドキュメントの内部に埋め込んで返すための箱.

<meta charset="UTF-8">

使用できるキャラクターセットはIANAで管理されている.
著者が一番言いたげなのはUTF-8, SHIFT_JISなど区切り文字を なぜ統一しなかったんだ...ということのように思えてきた.

Accept-Encoding: ボディーの圧縮

ボディーの圧縮は圧縮による通信速度の向上のためにも積極的にやってください. 圧縮してそれを展開してっていう処理を入れても、圧縮しない時よりも、Webページ表示までにかかるトータルの処理速度は早くなる.

  • ブラウザ利用者にとって転送速度がはやくなる
  • データ量が減ることで通信料金も安くなる

リクエスaccept-encoding:gzip, deflate, br

レスポンス content-encoding:gzip

$ curl --http1.0 --compressed http://localhost:18888

補足: - Brotli(br) -> gzipよりも効率が良い新しい圧縮フォーマット

クッキー

Webサイトの情報をブラウザ側に保存する仕組み.
クッキーもHTTPのヘッダーをインフラとして実装されている.

$ curl -v --http1.0 -c cookie.txt -b cookie.txt http://localhost:18888

-c: 指定したファイルにcookieを保存
-b: 指定したファイルと指定したkey:valueをcookieとして送信

クッキーの間違った使い方

永続性の問題

シークレットモード、改ざん、etc..、確実に保持されるものではないので、 なくなっても困らないもの、サーバーの情報から復元できるもの以外は保存しないこと.

容量の問題

容量制限がある(4Kバイトほど)、リクエスト/レスポンスのヘッダーに入るため速度が劣化するなどの問題もある.

セキュリティの問題

secureオプションを付与すればHTTPSでしか送受信されないが、HTTPの場合は平文で送受信される. パスワードや個人情報など見えて困るものを入れると情報漏洩の問題となるし、書き換えも容易なので誤動作につながるような情報は入れないようにすること.

クッキーに制約をあたえる

クッキーは特定のサービスを利用するためのトークンとして利用することが多いため、 クッキーを本来必要としないサーバに送信することはセキュリティリスクを高めることにつながる. そのため、送信先の制御や寿命を設定するような属性が存在し、HTTPクライアントはこれらの属性を解釈し、クッキーの送信を制御する責務がある.

Expires, Max-Age属性

クッキーの寿命を設定する属性.

Domain属性

クライアントからクッキーを送信する対象のサーバー. 省略時はクッキーを発行したサーバー.

Path属性

クライアントからクッキーを送信する対象のサーバーのパス. 省略時はクッキーを発行したサーバーのパス.

Secure属性

HTTPS接続での安全な接続以外は、クライアントからサーバーへのクッキー送信をしない.

HttpOnly属性

CookieJavaScriptから触ることができないようにする属性. クロスサイトスクリプティングなど、悪意のあるJavaScriptが実行されるリスクを守る.

SameSite属性

RFCには存在しない属性. リクエストの起因となったオリジンと、リクエスト先のオリジンが異なるようなリクエストには送信しないように設定する属性.
参考: http://qiita.com/flano_yuki/items/b87b2c28db0b056665ef#same-site-cookies

認証とセッション

ユーザ名とパスワードを入力してログインすることを認証といい、 サービス側はその認証情報をもとに、誰がアクセスしているかを特定する.

  • Basic認証
  • Digest認証
  • クッキーを使ったセッション管理

Basic認証

ユーザ名とパスワードをbase64エンコーディングしたもの. SSL/TLSを使っていないと、通信を傍受されたら簡単にユーザ名とパスワードが漏洩する.

$ curl -v --http1.0 --basic -u user:pass http://localhost:18888

ヘッダーにはAuthorization: Basic dXNlcjpwYXNzが付与される.

Digest認証

Basic認証より強固なものが、ハッシュ関数(A->Bは簡単だが、B->Aは簡単には計算できない)を利用したDigest認証. 認証対象画面へアクセスすると401認証エラーを返し、承認ダイアログを表示する.

www-Authenticate: Digest username="ユーザ名" realm="エリア名", nonce="0123456789", algorithm=MD5, qop="auth"

クッキーを使ったセッション管理

Basic・Digest認証は現在はあまり使われていない理由

  • 特定フォルダ配下を認証しないと見せないという使い方しかできない.
    ログインしてもしなくても良い画面とかできない
  • ログイン画面がカスタムできない.
    画像表示: https://point.recruit.co.jp/member/login/
  • 明示的にログアウトできない.
  • ログインした端末の識別ができない.
    Googleとかだと新しい端末からアクセスしたら警告メールがきますよね

クッキーとセッションを使った認証管理が主流

認証の流れとしては以下の通り 1. クライアントは、フォームでユーザ名とパスワードを送信
ユーザ名とパスワードは直接送信するのでSSL/TLSでないと簡単に漏洩するので注意 2. サーバ側ではユーザ名とパスワードで認証し、問題がなければセッショントークンを発行し、DBに保存 3. トークンはクッキーとしてクライアントに返却 こっちの方がわかりやすい:
http://qiita.com/hththt/items/07136ad74127999df271

プロキシ

プロキシはHTTPなどの通信を中継する仕組み.

プロキシサーバ導入のメリット

  1. 送信元を隠す
    Proxy を介して通信する事で、WEBページにとってはProxy が送信元として捉えられる. この為、送信元を隠す目的で使用される. 例えば、アメリカのProxyを使えば、アメリカからアクセスしているように思わせる事も可能.
  2. セキュリティ
    大企業などでは、社内からInternet を閲覧する際に、必ずと言って良いほど、Proxy を使って通信をさせてる. コレは、Proxy を中継させる事で、社員のアクセス管理を行う事が可能となり、不要なサイトへのアクセスを禁じたり、アクセスの統計を取得したりする目的で使われている.
  3. キャッシュサーバ
    Proxy の役割は、『代理サーバ』とは別に『キャッシュサーバ』としての役割を持つ. 通常、クライアントのブラウザなどにもこの『キャッシュ』機能は搭載されているが、その巨大版です。
$ curl -v --http1.0 -x http://localhost:18888/helloworld/ -U user:pass http://example.com/helloworld/

GET http://example.com/helloworld/ HTTP/1.0 / Host: example.com

$ curl -v --http1.0 http://localhost:18888/helloworld/

GET /helloworld/ HTTP/1.0 / Host: localhost:18888

キャッシュ

Webサイトのリッチ化に伴い、読み込みファイル数・サイズが増加傾向にある. 通信回線が速くなっても、毎回読み込んでいては表示速度が遅くなってしまう. キャッシュは、ダウンロード済みで内容に変化がなければ、新たに読み込むのではなく、 ダウンロード済みのものを表示してパフォーマンスをあげる仕組みである.

更新日時によるキャッシュ

HTTP1.0のキャッシュの仕組み. 当時は静的コンテンツがメインだったので、コンテンツの新旧を比較するだけで事足りていた.

仕組み

サーバーは、コンテンツの最終更新時刻をレスポンスヘッダのLast-Modifiedに入れて送信する. ブラウザ側はこのコンテンツの最終更新時刻を覚えておき、 次回リクエストした際にリクエストヘッダのIf-Modified-Sinceの中に含めて送信する. サーバー側のコンテンツに変更がなければサーバーはステータスコード304 コンテンツ未更新ステータスコードを送り ブラウザはキャッシュされたコンテンツを表示させるという仕組み.

Expiresによるキャッシュ

更新日時によるキャッシュの場合、キャッシュの有効性を確認するためにどうしても通信が発生する. その通信自体をなくす仕組みがHTTP1.0に追加されたExpiresによるキャッシュ.

仕組み

Expiresはレスポンスヘッダのひとつで、新しいファイルが存在するかどうかを確認することなく、Expiresの期限内であればブラウザでキャッシュ済みのファイルを強制的に適用する.

コンテンツが日々変わるページには不向きで、あまり変更の入らないcssやjsファイルとかによく使われる.
https://beauty.hotpepper.jp/svcSA/

Pragma: no-cache

クライアントからプロキシに対して指示を送ることもある. 実装依存の命令を含むリクエストヘッダーの置き場として、 HTTP1.0からPragmaヘッダーが定義された(no-cacheしか設定できない). リクエストしたコンテンツがキャッシュされていたとしても、 本来のサーバーまでリクエストを届けるようにするための仕組み.

HTTP/1.1ではCache-Controlにマージされました.

ETagの追加

同じ画面にアクセスしてもユーザの状態によって、コンテンツの内容が異なる場合がある. 例えば、ログイン前後や、会員ステータス(プレミアム会員、会員、会員ではない)でコンテンツを出し分ける場合など. このように動的に変更する要素が増えれば増えるほど、どの日時を根拠にキャッシュの有効性を判断すれば良いのか判断が難しくなる. その場合に使用できるのがHTTP/1.1で定義されたETagヘッダー.

仕組み

ETagはレスポンスヘッダのひとつで、 初回アクセス時に、サーバーはレスポンスにETagヘッダーにハッシュ値を付与する. ブラウザは2度目以降のアクセス時にIf-None-Matchヘッダーにダウンロード済みのETagの値をつけてリクエストする. サーバーはIf-None-Matchとこれから送りたいファイルのETagとを比較し、同じなら304 コンテンツ未更新を返し、ブラウザはキャッシュされたコンテンツを表示させるという仕組み.

ETag:"6307c-553dda1253000"
参考: https://www.ponparemall.com/

Cache-Control(1)

ETagと同時期にHTTP/1.1で追加されたのが、Cache-Control. Expiresよりも優先されるキャッシュの仕組み. まずはサーバーからレスポンスとして送付されるヘッダーについての紹介.

ディレクティブ 説明
public 複数ユーザーで共有できるようにキャッシュしてよい(プロキシOK, ブラウザOK)
private 特定ユーザーだけが使えるようにキャッシュしてよい(ブラウザOK)
max-age=n キャッシュの鮮度を秒で設定.86400を指定すると、1日キャッシュが有効でサーバーに問い合わせることなくキャッシュを利用する.それ以降はサーバーに問い合わせを行い304 コンテンツ未更新が返ってきた時のみキャッシュを利用する.
s-maxage=n max-ageと同等だが、共有(プロキシの)キャッシュに対する設定値
no-cache 一度キャッシュに記録されたコンテンツは、現在でも有効か否かをサーバに問い合わせて確認がとれない限り再利用してはならない、という意味
no-store キャッシュしない

参考:

Cache-Control(2)

リクエスト時にプロキシに対してキャッシュに関する指示をだす

Cache-Controlヘッダーをリクエストヘッダーに含めることでプロキシへ指示することができる. まずはクライアント側からリクエストヘッダーで使える設定値を紹介.

ディレクティブ 説明
no-cache Pragma: no-cacheと同等
no-store レスポンスのno-storeと同じで、プロキシサーバにキャッシュを削除するように指示する
max-age プロキシで保存されたキャッシュの有効期限を設定
max-stale キャッシュされたリソースの有効期限が切れていても指定時間内であればキャッシュを受け入れるよう設定
min-fresh 指定された時間は新鮮であるものを返すようにプロキシに要求する.逆に、指定時間内で有効期限が切れるリソースはレスポンスとして返せない.
no-transform コンテンツを改変しないようにプロキシに要求する.例えば、画像の圧縮とか
only-if-chached キャッシュからのみデータを取得するという指示で、設定時には初回をのぞいてオリジンサーバーへのアクセスは一切行わない

レスポンス時にプロキシに対してキャッシュに関する指示をだす

レスポンスヘッダーでサーバーがプロキシに対して送信するキャッシュ制御の指示についての紹介. 補足をすると、Cache-Control(1)で紹介したディレクティブもすべてプロキシに対して有効.

ディレクティブ 説明
no-transform プロキシがコンテンツを改変するのを抑制
must-revalidate no-cacheとほぼ同じだが、プロキシへの司令となる.キャッシュが期限切れだった場合、オリジンサーバでの確認無しにキャッシュを利用してはならない
proxy-revalidate must-revalidateと同じだが、共有キャッシュのみに対する要請

参考

Vary

ETagの説明では同じURLでも個人ごとに結果が異なるケースについて紹介された. 同じURLでもクライアントによって返す結果が異なることを示す場合はVaryを使用する.

仕組み

例えば、サーバがクライアントのUser-Agentリクエストヘッダーによって返す内容を変えているとする.この場合、サーバから返されたデータをproxyが素直にキャッシュしてしまうと、別のUser-Agentがproxyにアクセスしたときにサーバが意図しないデータがクライアントに返されてしまう危険がある.

その場合、レスポンスヘッダにVary: User-Agentが指定されていれば、 proxyはUser-Agentによって内容が変わることを知ることができるので、 キャッシュをしないとか、User-Agent毎に異なるキャッシュを保持するといった対応が 可能となる.

リファラ

ユーザーがどの経路からWebサイトに来たのかをサーバーが把握するために、 クライアントがサーバーに送るヘッダーをリファラーという.

Referer:https://www.google.co.jp

注意:
referrer ではなく referer.
スペルミスのままRFCに定義されちゃったらしい.

リファラーを制御する(1)

ユーザの通信内容を秘密にするHTTPSがHTTP/1.1から追加された. 保護された通信内容が保護されていない通信経路に漏れるのを防ぐため、 クライアントはリファラーの送信に制御を加えることをRFCで定められている.

アクセス元 アクセス先 送信するかどうか
HTTPS HTTPS する
HTTPS HTTP しない
HTTP HTTPS する
HTTP HTTP する

リファラーを制御する(2)

リファラーを制御する(1)を厳密に適用すると、サービス間に支障が出ることもあり、 リファラーポリシーなるものが提案され、現在ドラフトステータスとなっている.

困る事例: http://web-tan.forum.impressrd.jp/e/2015/04/14/19750

リファラーポリシーは以下の方法で設定できる.

  • Referrer-Policyヘッダー
  • <meta name="referrer" content="設定値">
  • aタグなどいくつかの要素のreferrerpolicy属性および、rel="noreferrer"属性
  • Content-Security-Policyヘッダー(廃止された模様)

リファラーポリシーとして設定できる値には次のようなものがある

  • no-referrer
    一切おくらない
  • no-referrer-when-downgrade
    現在のデフォルト動作と同じで、HTTPS→HTTP時は送信しない
  • same-origin
    同一ドメイン内のリンクに対してのみ、リファラーを送信
  • origin
    詳細ページではなく、トップページからリンクされたものとしてドメイン名だけを送信
  • strict-origin
    originと同じだが、HTTPS→HTTP時は送信しない
  • origin-when-crossorigin
    同じドメイン内ではフルのリファラーを、別ドメインにはトップのドメイン名だけを送信
  • strict-origin-when-crossorigin
    origin-when-crossoriginと同じだが、HTTPS→HTTP時は送信しない
  • unsafe-url
    常に送信

リファラーポリシーについてはこの記事がよくまってる
http://qiita.com/wakaba@github/items/707d72f97f2862cd8000

検索エンジン向けのコンテンツのアクセス制御

クローラー向けのアクセス制御の方法として、主に2つの手法が広く使われている.


参照: https://digital-marketing.jp/seo/sitemap-xml-and-robots-txt/

robots.txt

  • robots.txtファイルの用意
  • robots meta tagの設定

robots.txtはクロール最適化のために行うもの. 一方、robots meta tagは一つ一つのページのインデックスを最適化するために行うもの.
参照: https://bazubu.com/robots-txt-16678.html

robots.txtファイルはドメインのルートディレクトリに設置する必要がある. つまり、サブディレクトリ型のレンタルウェブスペースに設置されているサイトでrobots.txtファイルを使うことはできない.
参照: http://whitehatseo.jp/robots-txtの記述例と使い方を解説します/

robots.txtファイルの用意

robots.txtは、サーバーのコンテンツ提供者が、 クローラーに対してアクセスの許可・不許可を伝えるためのプロトコル. robots.txtは以下のような形式で読み込みを禁止するクローラーの名前と場所を指定する.

User-agent: *
Disallow: /cgi-bin/
Disallow: /tmp/

上記は、全クローラーに対して、/cgi-bin/フォルダと/tmp/フォルダへのアクセスを禁止している例となっている. User-agent: Googlebotのように、特定のクローラーに対しての指定も可能.

例: https://www.facebook.com/robots.txt

robots meta tagの設定

robots.txtと同じような内容をHTMLのメタタグに記述できる. robots.txtの方が優先されるが、こちらの方が細かく指定可能となっている.

<meta name-"robots" content="noindex" />

content属性にはさまざまなディレクティブが記述できる. Googlebotが解釈するディレクティブの詳細はGoogleのWebサイトに記載されている. 代表的なものを以下に示す.

ディレクティブ 意味
noindex 検索エンジンがインデックスするのを拒否する
nofollow クローラーがこのページ内のリンクを辿るのを拒否する
noarchive ページ内のコンテンツをキャッシュするのを拒否する

同じディレクティブはHTTPのX-Robots-Tagヘッダーにも記述できる X-Robots-Tag: noindex, nofollow

サイトマップ

サイトマップはWebサイトに含まれるページ一覧とそのメタデータを提供するXMLファイル. Flashを使って作られたコンテンツや、JavaScriptを多用して作られた動的ページからのリンクなど、 クローラーの実装によってはページが発見できない場合でもサイトマップによって補完できます.

<?xml version="1.0" encoding="utf-8">
<urlset xmls="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
    http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
  <url>
    <loc>http://example.com</loc>
    <lastmod>2006-11-18</lastmod>
  </url>
</xml>

このurlタグを登録したいページ数分作成します. locタグには絶対URLを指定する.

サイトマップは前述のrobots.txt内にも記載可能。 また、各検索エンジンに対してXMLファイルをアップロードする方法がある。 Googleの場合はSearch Consoleサイトマップツールを使う

Sitemap: https://beauty.hotpepper.jp/sitemap.xml

第6話: HTTPウェブプッシュ の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第6話<最終回>です。

HTTPウェブプッシュ

HTTPウェブプッシュは、ウェブサイトにスマホアプリのような通知機能を提供する仕組みです。 HTTPウェブプッシュはその機能の特性上、ブラウザが起動していなかったり、 オフラインでも、ユーザに通知を送れる必要があります。

サイバーエージェントさんのFRESH!というサービスで使われているです。 f:id:yagi_suke:20180127211148p:plain 画像: https://developers.cyberagent.co.jp/blog/archives/9772/

実装方法や困ったことも記載していて面白そうな記事になっているのでオススメしておきます。 - https://developers.cyberagent.co.jp/blog/archives/9662/

また、ディレクターがHTTPウェブプッシュを導入することで考えていたことも別記事になっていて面白そうです。 - https://developers.cyberagent.co.jp/blog/archives/9772/

Service Worker

ブラウザがないのになぜ通信できるのか。 その秘密は、オフラインのアプリを実現・サポートするために作られた Service Workerという機能にあります。

Service Workerは、ブラウザがWebページのスクリプトとは別のプロセスでスクリプトを バックグラウンドで実行できる機能のことです。現在登録されているイベントは以下の通りとなっています。

呼ばれるタイミング イベント名
インストール時 activate
フロントエンドからのpostMessage() message
フロントエンドがサーバーにアクセス fetch
プッシュ通知受信 push

HTTPウェブプッシュの流れ

引用元: http://d.hatena.ne.jp/jovi0608/20141204/1417697480

どのような仕組みで行われるのか、その概要を一枚の図にしました。 ちょっとごちゃごちゃ説明文を図に書いていますが登場人物は3人、

  • クライアント(ブラウザには、Webアプリケーション(Web App)、Service Worker、Push APIが動いています)
  • Webアプリケーションのサーバ(app.example.jp)
  • プッシュサーバ(push.example.jp) です。

f:id:yagi_suke:20180127211153p:plain 画像: http://d.hatena.ne.jp/jovi0608/20141204/1417697480

ざっとした流れは、

  1. クライアントからプッシュサーバの登録情報(endopoint, registrationID)をアプリサーバに通知。 (Push Serviceにアプリサーバがリクエストを送ることができるようになる)
  2. アプリサーバが、プッシュ通知をHTTP PUTのリクエストボディに付けてプッシュサーバに送信。
  3. プッシュサーバは、プッシュ通知をチャネルで区別し、送信クライアントを選定。
  4. プッシュサーバは、HTTP/2のサーバプッシュやFCM(Firebase Cloud Messaging)など利用してクライアントにプッシュ通知をSSEで送信。
  5. クライアントは、プッシュサーバからプッシュ通知を受けると、Service Worker上でPushイベントが発生。
  6. プッシュされたデータはキャッシュ更新なり、クライアント上でいかようにでも処理することができる。

といった流れになります。

事前作業が必要

上記1に入る前に、ブラウザを起動するときに、プッシュ通知の利用許諾を取る必要があります。 - プッシュ通知はオプトイン(承諾が必要ってこと) - 承諾されればプッシュサービスを利用登録する - サービスのIDと、送信用の鍵(プッシュサービスがブラウザを特定するためのもの)を取得

承諾されて有効になるとService Workerで登録したpushイベントで 通知が受け取れるようになります。

補足:
現状ウェブプッシュはプッシュサービスと連携することで実現しています。 プッシュサービスはChromeの場合はGoogle社の通知サービス、 Firefoxの場合はMozilla社の通知サービスを利用します。

緊急度の設定

Urgencyヘッダーをリクエスト時に付与すると、不必要なメッセージをフィルタリングできます。 メッセージ送信、および送信のときに付与します。 以下のように設定可能な値を示します。 省略時はnormalになります。

緊急度 バイスの状態 用途
very-low 電源がつながっていてWiFi接続あり 広告
low 電源がつながっているか、WiFiあり 話題の更新
normal 電源がつながっておらず、WiFiもない チャットやカレンダーのメッセージ
high バッテリー残量が少ない 電話の着信、もしくは時間厳守の通知

引用元や参考情報

第5話: WebRTC の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com

の第5話です。

WebRTC(Web Real-Time Communication)

WebRTCはブラウザ/サーバー間の通信だではなく、ブラウザとブラウザのP2P通信で使用します。 RTCは「リアルタイムコミュニケーション」の略で、テレビ電話などのリアルタイムコミュニケーションを 実現するための基盤として設計されています。

実現したいアプリケーションも他のプロトコルとは大きく異なるため、使われる機能群も大きく変わります。 通信の基盤に使うトランスポート層は、再送処理の面倒を見てくれるTCPではなく、 エラー処理や再送処理を行わないUDPをメインで使います。

またP2P通信なので、相手もブラウザを探すシグナリングルーター内部のプライベートアドレスしか 持たないコンピューターで動いているブラウザのため、NATを超えてプライベートアドレスだけで通信 できるような技術も使われています。

RTCPeerConnection

WebRTCの技術要素について説明していきますが、 ブラウザから使われるAPIという観点で説明していきます。

  • RTCPeerConnection: 通信経路の確保とメディアチャンネルのオープン
  • mediaDevices.getUserMedia: カメラ、マイクと動画・音声のハンドリング
  • RTCDataChannel: データチャンネルの通信

WebRTCのベースになっているのはIP電話です。 IP電話で使われている技術を一つにまとめ、JavaScriptAPIを定めたものです。 NAT超えなどの基礎をなすプロトコルはどれも古くからあるものです。

IP電話についてはこちら: http://www.pc-master.jp/internet/ip-denwa.html

P2P通信を確立するまで

ブラウザ間でP2P通信を行うには、相手のIPアドレスを知らなくてはなりませんし、 動的に割り振られるUDPのポート番号も知る必要があります。 また、その通信でやり取りできる内容についても、お互い合意しておく必要があります。 そのためP2P通信が確立するまでに、WebRTCではいくつかの情報をやり取りしています。

ICE(Interactive Connectivity Establishment)

ICEはNATを超えてP2P通信の接続を確立する手法です。 STUNとTURNのどちらかのサーバーを利用します。

(今回はSTUNで説明します)

NATを越えてP2P通信するには、シグナリング処理でお互いに自分の知らないグローバルの情報を 交換する必要があります。

f:id:yagi_suke:20180126011807p:plain

画像: https://html5experts.jp/mganeko/20618/

自分で知らない情報は、誰かから教えてもらうしかありません。 それを可能にするのがSTUNです。

STUNの仕組みはシンプルです。 NATで変換されたIPアドレス/ポートを、外側にいるSTUNサーバーに教えてもらいます。 先ほどの例で言えば、このようになります。

f:id:yagi_suke:20180126011818p:plain

画像: https://html5experts.jp/mganeko/20618/

自分のグローバル情報が分かったら、それをシグナリングサーバー経由で相手に渡します。 STUNサーバーから得られた情報は、シグナリングの過程でICE Candidateとして取得できます。 これをシグナリングサーバーなどを経由して相手に送ることができ、P2P通信が実現しています。

f:id:yagi_suke:20180126011812p:plain

画像: https://html5experts.jp/mganeko/20618/

SDP(Session Description Protocol: セッション記述プロトコル)

SDPはP2Pネゴシエーション時に互いのIPアドレストポート、双方で利用できるオーディオや動画の コーデック情報を共有するためのものです。

コーデック (Codec) は、符号化方式を使ってデータのエンコード(符号化)とデコード(復号)を双方向
にできる装置やソフトウェアなどのこと。 また、そのためのアルゴリズムを指す用語としても使われている。

参照: https://ja.wikipedia.org/wiki/コーデック

WebRTCでは「自分が使えるコーデック情報やIPアドレスなどをSDPというプロトコルのフォーマットで 記述して相手に渡し、相手からも互いに利用可能なコーデック情報のレスポンスをもらう」という手続きを 定めていますが、「相手に渡す」手段は定めていません。

渡す手段とかはこちらを参照すると良いとのことです。
https://html5experts.jp/mganeko/19814/

メディアチャンネルとgetUserMedia

相手との通信が確立したら、実際に通信を行います。 音声やビデオを取り扱うのがメディアチャンネルです。

navigator.mediaDevices.getUserMedia()

ビデオ会議などで使うウェブカメラやオーディオデバイスの、設定と取得を行うブラウザAPIです。

カメラは解像度、フレームレート、フロントカメラ/リアカメラなどを指定することもできます。 取得されたものはストリームという名前で呼ばれます。 このAPIは単独でも使うことができ、HTMLのvideoタグ上にカメラの映像を表示できます。
APIについて: https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia

オーディオはWeb Audio APIを利用することで高度な制御やモニタリングが行えます。 ステレオの音位の指定やミキシング、ボリューム調整などができます。 Audio Workerを利用すると音声信号をJavaScriptで分析できるようになるため、 しゃべっている人の検出なども可能となります。
Web Audio APIについて: https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API

DTLS

WebRTCのデータ通信には、メディアチャンネルとデータチャンネルの2つがあります。 どちらも、DTLS上の通信となっています。

DTLSについてはこちら
参考: http://www.nttpc.co.jp/yougo/DTLS.html

メディアチャンネル

音声・動画を伝送するWebRTC Mediaチャンネルです。 WebRTC MediaチャンネルはDTLS上のSRTPというプロトコルをつかっています。

音声や動画のストリーミングで情報が入れ替わってしまうのは問題なので、 順序の整列機能だけをUDPに追加しています。

データチャンネル

データチャンネルは音声以外のデータをP2Pで送受信するためのものです。 ユースケースにはファイルをチャットの相手に送信したり、 通信対戦のゲームの捜査情報を共有するのに使用していました。 データチャンネルはDTLSの上でSCTP(Stream Control Transmission Protocol)という プロトコルを載せています。

データのユースケースは映像・音声よりも多少幅があります。 データファイルのデータが途中で消失してしまったり、一部欠落すると困ることもあれば、 それより応答性が損なわれる方が問題になることもあります。

そこでSCTPではパフォーマンスとトレードオフで信頼性を設定できるようになっています。 UDP側に寄せるか、TCP側に寄せるか、選べるようになっています。

  TCP UDP SCTP
信頼性 到達保証あり 到達保証なし 変更可能
配送順序 順序付けあり 順序つけなし 変更可能
転送方式 バイト指向 メッセージ指向 メッセージ指向
輻輳制御 あり なし あり

データチャンネルの初期化は以下のように行います。 2つ目の引数が特性を制御するパラメータです。 省力時はTCP同等の配送順序保証の高信頼性モードになります。 このサンプルコードは順序を保証せず、再送もしないというUDP互換のモードを設定しています。 maxRetransmitTimeかmaxRetransmitsのどちらかを設定し、再送方式を手動にすると非信頼モードになります。

const connection = new RTCPeerConnection()

const dataChannel = connection.createDataChannel('data channel', {
  orderd: false,        // 順序保証?
  maxRetransmitTime: 0, // 再送し続ける期間
  maxRetransmits: 0     // 再送回数
})

ファイル送信などのユースケースであればデフォルトの高信頼性モードが適しています。 例えば、画像ファイルやEXCELファイルのデータの一部が変えたり順序が入れ替わってしまったら、 ファイルとして用をなさないからです。 ゲームのステータスで、UDPの1パケットに治るサイズであれば、少なくとも順序保証はありません。

参考になりそうなサイト: https://www.html5rocks.com/ja/tutorials/webrtc/datachannels/

引用元や参考情報

第4話: WebSocket の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第4話です。

WebSocket

WebSocketとは、サーバー/クライアント間で、オーバーヘッドの小さい双方向通信を実現する仕組みです。

WebSocketはフレーム探知で送受信しますが、相手が決まっているために送信先の情報などはもちません。 HTTPの基本要素のうち、ボディのみを送っているようなものです。 フレームはデータサイズなどを持っているだけで、オーバーヘッドも2から14バイトしかありません。 一度コネクションを確立した後は、サーバーとクライアントのどちらからも通信を行うことが可能です。

WebSocketは何がうれしいのか

Ajax, Commet, WebSocketの通信の比較を電話で表現してみたの図です。

f:id:yagi_suke:20180128175545p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.34
(備考: 一回返事をもらうごとに通信が切れる気がするけど、)

f:id:yagi_suke:20180128175547p:plain f:id:yagi_suke:20180128175551p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.35
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.36

f:id:yagi_suke:20180128175555p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.37

通信方式 特徴
Ajax 負荷が高い
サーバー側の情報取得は定期的
クライアント側の情報送信は別接続で行うことが多い
Commet 負荷が非常に高い
サーバー側の情報取得はリアルタイム
クライアント側の情報送信は別接続のAjax
WebSocket 負荷が小さい
サーバー側の情報取得はリアルタイム
クライアント側の情報送信も同じ接続内(双方向通信)

WebSocketはステートフル

WebSocketが他のHTTPベースのプロトコル異なるのは「ステートフルな通信である」という点です。
ステートフルとかステートレスについて: http://yohei-y.blogspot.jp/2007/10/blog-post.html

HTTPは高速性のためにKeep-Aliveなどの複雑な機構も備えるようになってきていますが、 基本的にリクエスト単位で接続が切れてもセマンティクス上は問題ありません。 ロードバランサーを使ってサーバーを複数台に分散しておき、リクエストのたびに別のサーバーが 応答していても問題ありません。

ちなみに、Server-Sent Eventsも送信済みIDを一元管理して保証できれば、 リクエストを捌くサーバーが入れ替わっても問題がないように設計されています。

JavaScriptのクライアントAPI

WebSocketは、HTTPの下のレイヤーであるTCPソケットに近い機能を提供するAPIです。 JavaScriptAPITCPソケットのAPIに近い形態になっています。 通信はサーバーが受信を受けている状態で、必ずクライアントから接続します。 (クライアント起点の通信)

  1. サーバーが特定のIPアドレス、ポート番号でサーバーを起動(Listen)
  2. クライアント(ブラウザ)がサーバーに通信開始の宣言をする(Connect)
  3. サーバーにクライアントから接続依頼がくるので、それを受け入れる(Accept)
  4. サーバーにはソケットクラスのインスタンスが渡される
  5. サーバーが受理すると、クライアントのソケットのインスタンスの送受信機能が有効になる

このListen/Connect/Acceptは筆者が参考のために追加したもので、 システムプログラミングの文脈でソケットについて説明するときに使われる関数名です。

JavaScriptのWebSocketのAPI名はこれと異なりますが、基本的な考え方に差はありません。 実際には一旦HTTPで接続してからアップグレードするため、内部の手順はやや複雑ですが、 外部から見たシーケンスは同じです。 基本的な接続と送信のコードを示します。 WebSocketクラスのコンストラクタで接続先のURLを指定して、send()メソッドでデータを送信します。

上記の手順でクライアントが接続に当たってすべきことは2つだけです。 このコンストラクタと、onopenイベントリスナの裏でこれだけのことが行われています。

const socket = new WebSocket('ws://game.example.com:12010/updates')

socket.onopen = () => { // データの送受信可能となる
  setInterval(() => {
    if (socket.bufferedAmount === 0) {
      socket.send(getUpdateData())
    }
  })
}

接続後のソケットに対して、クライアント側で行う操作は次の3つです。 - send([データ]): WebSocket接続を利用して、データをサーバーに送信する - onmessage: サーバーからメッセージを受信した時に呼ばれるイベントリスナ - close([コード [, 理由]]): WebSocket接続を切断

データとしては文字列、Blob, ArrayBufferなどを使うことができます。 受信には、onmessageメソッドを使います。 onmessageイベントの使い方はServer-Sent Eventsと同じです。

参考: https://developer.mozilla.org/ja/docs/Web/API/WebSocket

接続

まず通常のHTTPとして接続をスタートし、その中でプロトコルのアップグレードを行い、 WebSocketにアップグレードすることを要請します。

通常開始のリクエス
GET /chat HTTP/1.1             // 普通のHTTPリクエスト
Host: server.example.com
Origin: http://example.com
Upgrade: websocket             // この2つで、HTTPからWebSocketへの
Connection: Upgrade            // プロトコルのアップグレードを表現している
Sec-WebSocket-Key: A4QSEcsepWr4m2PLS2PJHA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Key クライアントとのコネクションの確立を立証する為に使われる。 ランダムに選ばれた16バイトの値をBASE64エンコードした文字列
  • Sec-WebSocket-Versions 接続のプロトコルバージョンを指示するためにクライアントからサーバへ送信され、 現在のWebSocketの最新バージョンは13なので13を固定する
  • Sec-WebSocket-Protocol WebSocketは単にソケット通信の機能だけを提供する。 その中でどのような形式を使うかはアプリケーションで決める必要がある。 コンテントネゴシエーションのように複数のプロトコルが選択できるように使われる。 このヘッダーはオプション
サーバーレスポンス
HTTP/1.1 101 Switching Protocols   // HTTPがUpgrade!!
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 7eQChgCtQMnVILefJAO6dK5JwPc=
Sec-WebSocket-Protocol: chat
  • Sec-WebSocket-Accept Sec-WebSocket-Keyを決まったルールで変換した文字列。 これにより、クライアントはサーバーとの通信確立を検証できる。
  • Sec-WebSocket-Protocol クライアントからサブプロトコルのリストを受け取ったときに、その中野一つを選択して返す。 クライアントは送信したプロトコル以外を受け取ったら接続を拒否しなければならない

ランダムな文字列からSec-WebSocket-Acceptの値は特定の文字列を接合した後にSHA1ハッシュを計算し、 それをBASE64エンコードした内容になります。

この後は双方向通信を開始します。

Socket.IO

WebSocketは強力なAPIですが、多くの場合、それをさらに使いやすくするSocket.IOという ライブラリを経由して使う方法に人気がありました。

Socket.IOのメリットは次の3点 - WebSocketが使用できないときは、XMLHttpRequestによるロングポーリングでエミュレーションし、 サーバー側からの送信を実現する機能がある - WebSocketの再接続を自動で行う - クライアントだけではなく、サーバー側で使える実装もあり、クライアントが期待する手順で フォールバックのXMLHttpRequest通信をハンドシェイクしたりできる - ロビー機能

WebSocketが使われ始めた当初は後方互換性を広く維持できるということで、 WebSocketといえばSocket.IOでした。

しかし、現在ではWebSocketが使えないブラウザは殆どありません。 企業内のセキュリティ管理のためのウェブプロキシなど、WebSocketが使えなくなる要因はいくつか ありますが、後方互換性目的で他のライブラリを使う理由はかなり減ってきています。 再接続処理などメリットがゼロではありませんが、今後は直接使われるか、 使うにしてもXMLHttpRequestフォールバックをオフにされることが増えるでしょう。

引用元や参考情報

第3話: Server-Sent Eventsの雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第3話です。

Server-Sent Events(SSE)

SSEはHTML5の機能のひとつです。 巨大なファイルコンテンツを「少しずつ送信」するHTTP/1.1のChunked形式の通信機能を応用し、 サーバーから任意のタイミングでクライアントにイベント通知できる機能です。

2014年にはGREEさんがすでにバックエンドに採用していました。
http://labs.gree.jp/blog/2014/08/11070/

サーバーから情報を送る方法には、Comet(4章参照)がありました。 クライアントから定期的にリクエストを送ることでサーバー側のイベントを検出する(ポーリング)、 あるいはリクエストを受け取った状態で返事を保留する(ロングポーリング)方法がよく使われていました。

ロングポーリングのフロー f:id:yagi_suke:20180126010540p:plain

画像: http://kimulla.hatenablog.com/entry/2016/01/17/リアルタイムなwebアプリを実現する方法%28ポーリン

Cometのロングポーリング + Chunked レスポンス を組み合わせて、 1度のリクエストに対して、サーバーから複数のイベント送信を実現したのがSSEです。

イベントストリームについて

SSEはChunked形式を使っていますが、HTTPの上に別のテキストプロトコルを載せています。 これはイベントストリームと呼ばれ、MIMEタイプはtext/event-streamです。

イベントストリームの例

id: 10
event: ping
data: { "time": 2017-09-25T01:27:00+000 }

id: 11
data: Message from pySpa
data: #eng channel
タグの種類 説明
id イベントを識別するID。再送処理で使用
event イベント名を設定
data イベントと共におくられるデータ(jsonがよく使われる)
retry 再接続の待ち時間のパラメータ(ミリ秒)

SSEのサンプルソース

// SSE APIは、EventSourceインターフェイスに含まれています。
// イベントを受け取るためにサーバへの接続を開始には、
// イベントを生成するスクリプトのURIを指定する、新たなEventSourceオブジェクトを作成します。
// インスタンスを生成したら、メッセージの受け取りを始めることができます。
const evtSource = new EventSource('ssedemo.php')

// このコードはeventフィールドを持たない、サーバからの通知を受信して、
// メッセージのテキストをドキュメントのHTMLにあるリストへ追加します。
evtSource.onmessage = function(event) {
  const newElement = document.createElement('li')

  newElement.innerHTML = `message: ${event.data}`
  eventList.appendChild(newElement)
}

// addEventListener() を使用して特定のイベントを待ち受けることもできます
// 前のコードと似ていますが、eventフィールドに"ping"が設定されたメッセージが
// サーバから送られたときに、自動的に呼び出されることが異なります。
// こちらはdataフィールドのJSONをパースして、情報を出力します。
evtSource.addEventListener('ping', function(event) {
  const newElement = document.createElement('li')
  const obj = JSON.parse(event.data)

  newElement.innerHTML = `ping at ${obj.time}`
  eventList.appendChild(newElement);
}, false);

ソース元: https://developer.mozilla.org/ja/docs/Server-sent_events/Using_server-sent_events

注意 - IEが対応していません。polyfillで対応可能。 https://developer.mozilla.org/ja/docs/Server-sent_events/Using_server-sent_events