2020-04-16
HTTP学習メモ with Webを支える技術
既に読んだことはあるが、定期的に読み返すべき良書なので読み返している
Apple純正のメモによくメモしているのだけど、せっかくなのでブログにも置いておく
(今後はブログがメモ帳替わりになるように頑張っていきたい)
-
HTTPはTCP/IPをベースにしている
- TODO: TCP/IPは別途学習する
-
基本的な処理の流れは
- クライアント(ブラウザなど)が、情報を提供するサーバー(Railsなど)に接続し、各種リクエストを出して、レスポンスを受け取る
- これはリクエスト・レスポンス型プロトコルと呼ぶ
- HTTPは同期型プロトコルのため、リクエストを出したクライアントはレスポンスが返るまで待機する
- クライアントとユーザーエージェントは厳密には意味が異なるが、ほとんどの場合大差がないので同じ意味と捉えて大丈夫
- 実際にブラウザでgoogle.comにアクセスした場合は
- まずDNSを使って、google.comのホスト名を名前解決する
- 名前解決で得たIPアドレスのTCP80番ポートへ接続し、リクエストヘッダと呼ばれるテキストを送信する
- サーバーは受け取ったリクエストヘッダを解析し、レスポンスを作成し、返す
- クライアントで行われること
- リクエストの構築
- リクエストの送信
- (レスポンスが返るまで待機)
- レスポンスの受信
- レスポンスの解析
- 最終処理(ブラウザならHTMLを構築して、画面に表示するなど)
- サーバーで行われること
- (リクエスト待機)
- リクエストの受信
- リクエストの解析
- 解析した結果から、適切なアプリへ処理の移譲
- 移譲したアプリから結果を受け取る
- レスポンスの構築
- レスポンスの送信
- クライアント(ブラウザなど)が、情報を提供するサーバー(Railsなど)に接続し、各種リクエストを出して、レスポンスを受け取る
-
リクエストメッセージ
- GET /test HTTP/1.1
- リクエストラインと呼ばれるもので、これはヘッダではない
- HTTPメソッド、リクエストURI、プロトコルバージョンから成る
- URIにはURIフラグメント(ページ内リンクに飛んだ時に末尾につく#hogeみたいなやつ)を除いたパス以降の文字列(相対パス)が入るが、実は絶対パスでもよい
- ただしHostヘッダも必須であるため、ドメインはHostヘッダに任せ、リクエストURIは相対パスにするのがシンプルかもしれない(ブラウザの仕様か?)
- Host: example.com
- 必須のヘッダ
- ヘッダ(Hostヘッダ以外は省略可能)
- リクエストメッセージの2行目以降がヘッダ
- 1つのメッセージは複数のヘッダを持つことができる
- 各ヘッダは「名前:値」という構成をしている
- ボディ(省略可能)
- ヘッダのあとにボディが続くことがある
- ボディには、そのメッセージを表す本質的な情報が入る
- よくあるのはリソースの作成、更新の際にボディにリソースの表現そのものが入る
- GET /test HTTP/1.1
-
レスポンスメッセージ
- HTTP/1.1 200 OK
- ステータスラインと呼ばれるもので、これまたヘッダではない
- プロトコルバージョン、ステータスコード、テキストフレーズから成る
- ヘッダ(省略可能)
- レスポンスの2行目以降もリクエストと同様にヘッダと呼ばれる
- ボディ(省略可能)
- レスポンスメッセージにはボディが含まれることがある
- ヘッダとボディは空行で区切られ(ヘッダの終了は空行で識別される)、ブラウザなどではボディにHTMLが含まれることが多い
- HTTP/1.1 200 OK
-
アプリケーション状態(セッション状態)
- クライアントがどういう情報を持っているか、どのような状態なのか、みたいな情報をサーバー側が保持している状態のこと
- HTTPはステートレスなプロトコルなので、(特別なことをしなければ)通信毎に状態が初期化された状態で処理が始まる
- 例えばFTPはステートフルなので、クライアントがFTPサーバへログインしてから、ログアウトするまで、そのクライアントがどのディレクトリにいるのかといったアプリケーションの状態をサーバーが管理している
-
HTTPメソッド
-
GET/POST/PUT/DELETE/HEAD/OPTIONS/TRACE/CONNECTの8つしかない
- TRACEとCONNECTはほぼ使われていないらしいのでとりあえずスルー
- GET
- リソースの取得
- POST
- リソースの作成
- リソースを作成するURIにPOSTすると、レスポンスは201 createdとなり、Locationヘッダに作成したリソースのURI、ボディに内容が入って返ってくる
- リソースの追加
- リソース作成ほど一般的ではないが、代表的なPOSTの機能
- リソースの追加の場合、レスポンスは200 OKが返ってくる(サーバーの実装に依存する)
- リソースの先頭についかされるか、末尾に追加されるかはサーバーの実装に依存する
- そもそもリソースの追加としてPOSTはできないようになっていることもある(サーバーの実装に依存)
- URIを見ただけじゃPOSTの挙動はわからない
- 他のメソッドでは対応できない処理
- 例えば検索結果を取得する場合、多くの場合はGETリクエストを投げるが、パラメータが非常に長かった場合は、URIの長さ上限(仕様上制限はないが、ブラウザの上限があったりする)に引っかかり、GETでは対応できない場合がある
- そんなときにPOSTを使い、リクエストボディにパラメータを含めるようにすれば、どれだけ長くても対応可能となる
- 確かにransackで大量に検索フォーム作ると、パラメータとんでもない長さになることがあるので、そんなときに使えそう
- リソースの作成
- PUT
- リソースの更新
- すでに存在するリソースへPUTすると、次の2つのパターンでレスポンスが返る
- 200 OKで、ボディにリソースを更新した結果の表現が入るパターン
- 204 No Contentで、ボディに何も持たないレスポンスを返すパターン
- すでに存在するリソースへPUTすると、次の2つのパターンでレスポンスが返る
- リソースの作成
- 例えば、まだ存在しない /newitem へのPUTの場合、サーバーはリソースの作成と解釈し、201 createdを返す
- POSTの場合は新しく作成したリソースのURIがLocationヘッダで返ったが、PUTの場合はクライアントがすでにURIを知っている(/newitem)ので、Locationヘッダを返す必要がない
- /newitemがすでに存在する場合は、↑の更新処理となる
- リソースの更新
- POSTとPUTのリソース作成の使い分け
- POSTはクライアントがリソースのURIを指定できない(/listsへのPOSTでは/lists/2になるかもしれないし、/lists/89かもしれない)
- PUTはクライアント側でURIを決定できる(例えばwikiのようにクライアントが決めたタイトルがそのままURIになるようなもの => /posts/俺の考えた記事 みたいなURI)
- 特別な理由がない限りは、リソース作成はPOSTで行い、URIはサーバー側で決定するという設計が望ましい
- DELETE
- リソースの削除を行う
- レスポンスは200だったり、204だったりする(レスポンスボディを持たないため)
- HEAD
- GETに似ているが、リソースのヘッダだけを取得するので、ボディを持たない
- ネットワーク帯域を節約しつつ、リソースの大きさや更新日などを取得することができる
- OPTIONS
- リソースがサポートしているメソッドを取得できる
- レスポンスにAllowヘッダが返り、そこに許可されているメソッドが書かれている
-
現実に一番利用されているHTTPメソッドはGETとPOST
- HTMLのフォームで指定できるメソッドがGETとPOSTだけという制限があるため
- しかし、ajaxで使われるXMLHttpRequestモジュールを使用すると、任意のメソッドを発行することができるようになり、近年制限が解消に向かっている
- ただし、セキュリティ上の理由や、XMLHttpRequestをサポートしないブラウザの場合、やはり2つしか使えない状況がある
- ↑のような状況においても、サーバにPUTやDELETEを伝える方法が2つ存在する
- _methodパラメータを使う
- Railsが使っている手法
- フォームの隠しパラメータに_methodというパラメータを用意し、そこに本来送りたいHTTPメソッドを記述しておくというもの
- フォームを利用してリクエストを送る場合に有効
- content-typeヘッダが application/x-www-form-urlencoded のときに利用できる
- サーバー側は_methodを見て、そのリクエストをどのメソッドとして扱うかを処理する必要がある
- X-HTTP-Method-Overrideヘッダを使う
- POSTの内容がXMLなど、application/x-www-form-urlencoded以外の場合に使う方法
- リクエストヘッダに X-HTTP-Method-Override: PUT のように本来使いたかったHTTPメソッドを指定することにより送信が可能
- サーバー側はX-HTTP-Method-Overrideヘッダを見て、そのリクエストをどのメソッドとして扱うかを処理する必要がある
-
冪等性と安全性
- HTTPメソッドは性質によって以下のような分類がされている
- GET, HEAD ... 冪等であり安全
- PUT, DELETE ... 冪等だが安全ではない
- POST ... 冪等ではなく、安全でもない
- 「冪等」とは「ある操作を何回行っても必ず結果が同じであること」を意味する
- 「安全」とは「操作対象のリソースの状態を変化させないこと」を意味する
- 状態を変化させることを副作用という
- GET, HEAD, PUT, DELETEは冪等であるため、クライアントはリクエストの重複を気にすることなく、何度でも送信することができる
- さらにGET, HEADはリソースの状態を変化させない
- PUT, DELETEは対象リソースの状態を変化させる
- PUT ... ユーザー名を「たなか」から「やまだ」へ変化させるリクエストを何度投げても、200が返ればユーザー名は「やまだ」になるので、冪等であり安全ではない
- DELETE ... アカウント削除するためにDELETEでリクエストをし、200が返ればアカウントは削除される
- 2度目のリクエストではアカウントが見つからないので404が返るが、「対象のリソースが削除される」という状況に変わりはないため、冪等であるといえる
- POSTは冪等ではなく安全でもないため、リクエストした結果がどうなるのかわからない
- ショッピングサイトで注文する際、ブラウザバックをしたとき「もう一度送信しますか?」とダイアログが出るのは、POSTを再送信しようとしている場合
- この状態でOKを押すと、二重注文となる可能性があるので、クライアントはPOSTを複数回送ることに慎重でなければならない
- HTTPメソッドは性質によって以下のような分類がされている
-
HTTPメソッドの誤用
- HTTPの仕様により、各HTTPメソッドが冪等かどうか、安全かどうかは決められているものの、API設計の誤りにより冪等ではなくなったり、安全では無くなったりする可能性がある
- あるURIにリクエストが来た時、どんな処理をするのかは実装によるので、
GET /resources/1/delete
(GETなのにリソースの削除している)みたいな目的を無視した使い方もできてしまう - POSTは万能なメソッドだが、他のHTTPメソッドで実現可能であればそちらで実装すべき
- PUTが冪等でなくなる例
- /tomatoにGETすると、トマトの値段が返るとする
- /tomatoにPUTすると、トマトの値段が更新できるとする
- GET /tomato の結果で100と返ってくる(現在100円)
- PUT /tomato に対して +50 と50円高くするリクエストを送信すると、150が返ってくる
- もう一度全く同じリクエストを送信すると、今度は200が返ってきてしまう(送信するたびに結果が変わるので冪等ではない)
- これは差分の送信を受け入れているためであり、この場合は更新後の価格を受け入れるべき
- 例えば PUT /tomato には更新後の価格である200を送信すると、200が返るようにする
- すると同じリクエストを送信しても常に200が返るため、冪等である
- DELETEが冪等でなくなる例
- /latestはエイリアスであり、現在は /ver1.3 というリソースを示している。(バージョンアップされると自動的に /ver1.4 を示すようになる)
- /latestに対してDELETEリクエストを送信すると、 /ver1.3 というリソースが削除される
- /ver1.3が削除されたため、/latest は /ver1.2 を示すようになる
- 再度 /latest にDELETEを送信すると、今度は /ver1.2 が削除されてしまう
- 送信するたびに結果が変わってしまうので、冪等性が失われている
- この場合は /versions/1.2 のようなURIに対してリクエストを受け入れるべき
- エイリアスリソースのような、ある特定のリソースを永続的に示すのではなく、時間や状況によって示すリソースが変化するものは、PUTやDELETEなどを受け入れないように設計すべき
-
正直本の内容そのまま書いてる感が強いので、できるだけ自分の思ったこととか挟んでいきたかったんだけど
内容がシンプルなのであまり挟めなかった
もし今後実際に↑の内容について、より詳細に調査することがあれば追記していきたい