Real World HTTPの輪読会をした part2
で、「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番台のリダイレクトの懸念事項
リダイレクトには懸念事項もある.
- URLは2000文字を目安にするべき(GETのクエリーには送信データ量に制限がある)
- データが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エラーとなる.
補足:
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タグの中で返しているページが多いらしい.
GitHubはja
の日本語設定高くしているのって、時期に日本語対応してくれるのかなと思った.
<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属性
CookieをJavaScriptから触ることができないようにする属性. クロスサイトスクリプティングなど、悪意のある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などの通信を中継する仕組み.
プロキシサーバ導入のメリット
- 送信元を隠す
Proxy を介して通信する事で、WEBページにとってはProxy が送信元として捉えられる. この為、送信元を隠す目的で使用される. 例えば、アメリカのProxyを使えば、アメリカからアクセスしているように思わせる事も可能. - セキュリティ
大企業などでは、社内からInternet を閲覧する際に、必ずと言って良いほど、Proxy を使って通信をさせてる. コレは、Proxy を中継させる事で、社員のアクセス管理を行う事が可能となり、不要なサイトへのアクセスを禁じたり、アクセスの統計を取得したりする目的で使われている. - キャッシュサーバ
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 | キャッシュしない |
参考:
- キャッシュについて整理してみた
http://qiita.com/karore/items/2dc6ab8347c940ea4648#pencil2-httpレスポンスヘッダ-で制御 - max-ageとs-maxageの違いについて
https://suin.io/534 - セキュリティに絡むとこ...
http://tech.mercari.com/entry/2017/06/22/204500
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と同じだが、共有キャッシュのみに対する要請 |
参考
- HTTPヘッダーフィールド2
http://d.hatena.ne.jp/s-kita/20080927/1222508924
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つの手法が広く使われている.
- robots.txt
robots.txtは、主に検索エンジンの巡回を指示するファイル. - サイトマップ
検索エンジンにサイト内のURLや動画の情報を告知するファイル.
参照: 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!というサービスで使われているです。 画像: 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) です。
画像: http://d.hatena.ne.jp/jovi0608/20141204/1417697480
ざっとした流れは、
- クライアントからプッシュサーバの登録情報(endopoint, registrationID)をアプリサーバに通知。 (Push Serviceにアプリサーバがリクエストを送ることができるようになる)
- アプリサーバが、プッシュ通知をHTTP PUTのリクエストボディに付けてプッシュサーバに送信。
- プッシュサーバは、プッシュ通知をチャネルで区別し、送信クライアントを選定。
- プッシュサーバは、HTTP/2のサーバプッシュやFCM(Firebase Cloud Messaging)など利用してクライアントにプッシュ通知をSSEで送信。
- クライアントは、プッシュサーバからプッシュ通知を受けると、Service Worker上でPushイベントが発生。
- プッシュされたデータはキャッシュ更新なり、クライアント上でいかようにでも処理することができる。
といった流れになります。
事前作業が必要
上記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章より 〜
の第5話です。
WebRTC(Web Real-Time Communication)
WebRTCはブラウザ/サーバー間の通信だではなく、ブラウザとブラウザのP2P通信で使用します。 RTCは「リアルタイムコミュニケーション」の略で、テレビ電話などのリアルタイムコミュニケーションを 実現するための基盤として設計されています。
実現したいアプリケーションも他のプロトコルとは大きく異なるため、使われる機能群も大きく変わります。 通信の基盤に使うトランスポート層は、再送処理の面倒を見てくれるTCPではなく、 エラー処理や再送処理を行わないUDPをメインで使います。
またP2P通信なので、相手もブラウザを探すシグナリング、ルーター内部のプライベートアドレスしか 持たないコンピューターで動いているブラウザのため、NATを超えてプライベートアドレスだけで通信 できるような技術も使われています。
RTCPeerConnection
WebRTCの技術要素について説明していきますが、 ブラウザから使われるAPIという観点で説明していきます。
- RTCPeerConnection: 通信経路の確保とメディアチャンネルのオープン
- mediaDevices.getUserMedia: カメラ、マイクと動画・音声のハンドリング
- RTCDataChannel: データチャンネルの通信
WebRTCのベースになっているのはIP電話です。 IP電話で使われている技術を一つにまとめ、JavaScriptのAPIを定めたものです。 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通信するには、シグナリング処理でお互いに自分の知らないグローバルの情報を 交換する必要があります。
画像: https://html5experts.jp/mganeko/20618/
自分で知らない情報は、誰かから教えてもらうしかありません。 それを可能にするのがSTUNです。
STUNの仕組みはシンプルです。 NATで変換されたIPアドレス/ポートを、外側にいるSTUNサーバーに教えてもらいます。 先ほどの例で言えば、このようになります。
画像: https://html5experts.jp/mganeko/20618/
自分のグローバル情報が分かったら、それをシグナリングサーバー経由で相手に渡します。 STUNサーバーから得られた情報は、シグナリングの過程でICE Candidateとして取得できます。 これをシグナリングサーバーなどを経由して相手に送ることができ、P2P通信が実現しています。
画像: 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というプロトコルをつかっています。
- S: セキュリティ
- RTP: リアルタイムプロトコル
音声や動画のストリーミングで情報が入れ替わってしまうのは問題なので、 順序の整列機能だけを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の通信の比較を電話で表現してみたの図です。
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.34
(備考: 一回返事をもらうごとに通信が切れる気がするけど、)
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.35
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.36
画像: 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です。 JavaScriptのAPIもTCPソケットのAPIに近い形態になっています。 通信はサーバーが受信を受けている状態で、必ずクライアントから接続します。 (クライアント起点の通信)
- サーバーが特定のIPアドレス、ポート番号でサーバーを起動(Listen)
- クライアント(ブラウザ)がサーバーに通信開始の宣言をする(Connect)
- サーバーにクライアントから接続依頼がくるので、それを受け入れる(Accept)
- サーバーにはソケットクラスのインスタンスが渡される
- サーバーが受理すると、クライアントのソケットのインスタンスの送受信機能が有効になる
この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章参照)がありました。 クライアントから定期的にリクエストを送ることでサーバー側のイベントを検出する(ポーリング)、 あるいはリクエストを受け取った状態で返事を保留する(ロングポーリング)方法がよく使われていました。
ロングポーリングのフロー
画像: 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