Webを支える技術のメモ
第一章 Webとはなにか
Webの用途
- Webサイト
- 今みなさんが見ているこの記事や検索サイト、ショッピングサイトなど
- UI(ユーザーインターフェース)
- プリンタやテレビなどの各種デバイスの設定画面
- HTMLによるヘルプの記述
- プログラム用API
- XMLやJSONなど、プログラムで解釈・処理しやすいもの
Webを支える技術
Webを支える最も基本的な技術
- URI(Web上に存在する情報を特定するための識別子)
- HTML(これらの情報を表現するためのXMLをもとにした文書フォーマット)
- HTTP(これらの情報を取得したり発注したりするためのプロトコル)
これらの技術に支えられたWebは情報システムとしてハイパーメディアシステムと分散システムの2つの側面がある。
ハイパーメディア
ハイパーメディア以前のメディア(書籍や映画)は情報を線形的に順に取得(読んだり視聴したり)する。ハイパーメディアはテキストや画像などの様々な情報をハイパーリンクまたは(単にリンク)でつなぎ、ユーザーはそれらの情報をリンクをもとに非線形的に自由に取得することができる。
分散システム
「分散システム」の対義語として「集中システム」があるが、これは1つの中央コンピュータがすべてを修理する形式である。一方、1つのコンピュータで扱いきれる情報は限られており、より膨大な情報を取り扱えるようにしたシステムが「分散システム」である。その特徴として
- 複数のコンピュータに存在するデータを一元的に扱う。
- 1台のコンピュータでは扱いきれない膨大な情報を操作できる。
- 1台のコンピュータでは難しい機能や性能を実現できる。
が挙げられる。特にWebは分散システムとしては過去最大規模のシステムである。
第二章 Webの歴史
Webの歴史的背景をハイパーメディアと分散システムの2つの側面から説明
Web以前のハイパーメディア
Web以前にもMemexやXanadu、HyperCardなど様々なハイパーメディアが開発された。これらのシステムは複雑さが最大の問題点であり、逆にWebが普及したのは必要最低限のリンク機能だけを搭載したその単純さであると考えられる。
Web以前の分散システム
Web以前の分散システムとしてはRPC (Remote Procedure Call)が挙げられる。
この技術を用いるとリモートのサーバで実行しているプログラムをクライアント側から呼び出せるという特徴がある。この技術は現在でも分散システムの実装に用いられているが、このシステムが現実的に動作するのは通信相手がある程度決まっているイントラネットのみでより大規模な異種分散環境ではスケールしない。
Webのように大規模な分散システムには何が必要なのかはWebの分散システムを見ることで明らかにする。
Webの誕生
-
1990年にスイスのCERNという研究所で働いていたTim Berners-LeeがWebの提案書を記述。
同年のクリスマス休暇に最初のバージョンのブラウザとサーバを完成させる。 -
Webの普及を一気に推し進めたのが、1993年に公開されたMosaicというブラウザである。このブラウザはそれまで文字情報しか扱えなかったのに対し、インラインで画像を混在させることができた。MosaicはInternet ExplorerやFirefoxといった現在のブラウザの源流となった。
ハイパーメディアとしてのWeb
Webのハイパーメディアとそれ以前のハイパーメディアとの一番の違いはインターネットを利用していること。?
これにより不特定多数の情報をリンクさせ合うことができ、システムを大規模化しやすくなった。反面、情報を集中的に管理することが難しくなりリンク切れを起こしやすいリスクも存在する。Webのハイパーリンクはシンプルな単方向リンクであり、ユーザにとってもわかりやすく実装も簡単であり、これがWebの普及した理由と言える。
様々なハイパーメディアフォーマット
初期のWebではHTMLが唯一のハイパーメディアフォーマットだったが、HTMLでは対応できない様々な要望が生まれ新しいハイパーメディアフォーマットが誕生。microformatsやRSSが提案されたが最終的にIETFでAtomが標準化される。HTMLやAtomはXMLをもとにしたマークアップ言語でデータを記述するためには冗長過ぎたため単純なデータフォーマットが提案され、デファクトスタンダードとなったのがJSONである。
分散システムとしてのWeb
RPCの弱みとして通信相手が限られてしまうということを挙げたが、Webはクライアントとサーバ側の通信をHTTPというシンプルなプロトコルで固定したことによって多種多様なコンピュータ環境からでもWebサービスにアクセスが可能となった。
Webの標準化
もともとWeb以前のインターネット標準はIETFのRFCとして定められてきた。(HTTP1.1はRFC2616)
しかしWebが急速に普及する中で使用策定が追いつかず各社の実装がばらばらになった結果、相互運用性のかける状態が発生。この問題を解決するため1994年にBurners-LeeがW3Cを設立。HTML、XML、HTTP、URI、CSSなどの標準化作業が行われた。
RESTの誕生と普及
この流れの中でカリフォルニア大学アーバイン校の大学院生だったRoy-FieldingがWebがなぜこんなにも成功し、大規模なシステムが成立したのかをソフトウェアアーキテクチャの観点から分析し「REST」と呼ばれるアーキテクチャスタイルとしてまとめた。(詳しくは3章で記述)またSOAPと呼ばれるをもとにしたアーキテクチャスタイルが提案されたが、2002年に登場したAmazon WebサービスによってRESTの普及に弾みが付き、当時はSOAPを用いた形式も同サービス用いられていたが2004年から始まったWeb2.0の中でWeb APIが提供するリソースをHTTPやURIで簡単に操作できるRESTスタイルのほうが受け入れられるようになった。
第三章 REST
アーキテクチャスタイル
アーキテクチャスタイルは複数のアーキテクチャに共通する性質、様式、作法を指す言葉。
アーキテクチャを決定する際の羅針盤となるもの。
具体例としてはMVCやパイプ&フィルタ、イベントシステムが挙げられる。
アーキテクチャスタイルとしてのREST
RESTはクライアント/サーバというアーキテクチャスタイルにいくつか制約を加えた
アーキテクチャスタイルで、特にネットワークシステムのアーキテクチャスタイルである。
RESTはWeb全体として、そして個別のWebサービスやAPIのアーキテクチャスタイルでもある。
リソース
リソースはRESTにおける重要な概念の1つです。Webにおけるリソース例を下に記す。
- 東京の天気予報
- 技術評論社の「Webを支える技術」のページ
- 東京駅の写真
- 筆者の最近のブックマーク
これらのリソースを一言で説明すると
「Webに存在する、名前を持ったありとあらゆる情報」となる。
そしてリソースにおける名前は必ず他のものと区別できなければならない。
リソースの名前としてのURIの具体例として
- 東京都中央区の天気予報(https://weather.yahoo.co.jp/weather/13/4410/13102.html)
- 技術評論社の「Webを支える技術」のページ(http://wdpress.gihyo.jp/plus/978-4-7741-4204-3)
が挙げられる。リソースの特徴をまとめると以下のようになる。
- リソースとは、Web上の情報
- 世界中のリソースは、それぞれURIで一意の名前を持つ。
- URIを用いることで、プログラムはリソースが表現する情報にアクセス可能
- 1つのリソースは複数のURIを持つことができる。
- HTML形式やPDFなど複数の表現を持つことができる。
- 状態があり、時間の経過に従ってリソースの状態が変化すると表現も変化する。
RESTアーキテクチャスタイルの構成
クライアント/サーバに他のアーキテクチャスタイルを追加してRESTを構成していく。
まずRESTを構成するアーキテクチャスタイルを紹介する。
- クライアント/サーバ
- ステートレスサーバ
- キャッシュ
- 統一インタフェース
- 階層化システム
- コードオンデマンド
クライアント/サーバ
HTTPというプロトコルでクライアントとサーバが通信。クライアントはサーバにリクエストを送り、サーバはそれに対してレスポンスを送る。単一のコンピュータですべてを処理するのではなく、ユーザインターフェースとしてのクライアントとデータストレージとしてのサーバに分離して処理できる。複数のサーバを組み合わせれば可用性をあげられる。
ステートレスサーバ
サーバがステートレスであるとはクライアントのアプリケーション状態をサーバが管理しないことを意味する。これによりサーバ側の実装を簡略化できる。
ただし、現実にはステートレスではないWebサービスやWeb APIが多数存在する。その代表はCookieをつかったセッション管理でRESTの視点から見ると間違った拡張である。しかし、Cookieを使ったフォーム認証をやめるわけにもいかないのでステートレスではない特質を理解し必要最低限に利用することが求められる。
キャッシュ
リソースの鮮度に基づいて一度取得したリソースをクライアント側で使い回す方式。
利点はサーバとクライアントの通信を減らすことであるが、古いキャッシュを利用することによって情報の信頼性が下がる可能性があること。
統一インターフェース
URIで指定したリソースに対する操作を統一した限定的なインターフェースで行うことを指す。たとえばHTTP1.1では、メソッドが8個しか用意されていない。
しかし、これにより全体のアーキテクチャがシンプルになり、クライアントとサーバの実装の独立性が向上する。
階層化システム
統一インターフェースの利点の1つとしてシステムが階層化しやすいというメリットがある。
- 階層化の例
- クライアント --- ステートレスサーバ
- クライアント --- プロキシ --- ステートレスサーバ
- クライアント --- ステートレスサーバ --- インターフェースの異なるレガシーシステム
このようにシステムをいくつかの階層に分離するアーキテクチャスタイルを階層化システムという。
コードオンデマンド
サーバ側のプログラムコードをクライアント側がダウンロードし利用するというアーキテクチャスタイル。JavaScriptやFlash、Javaアプレットなどがこれに該当する。これによりクライアントプログラムで用意している機能をあとから拡張できるようになる。近年のWebではこれらの重要度が増している。
一方で、HTTPのプロトコルを利用した通信をしていないので、アプリケーションプロトコルの可視性が低下するというデメリットがある。
RESTアーキテクチャスタイルに基づいた設計
RESTはアーキテクチャスタイルなので、システムのアーキテクチャは原則これに従わなければならない。しかし、実際のソフトウェアやシステム設計においてはこの理想から妥協しなければならないところも出てくる。また、ほとんどのRESTスタイルを除外しなければならない場合、無理やりRESTスタイルを採用する必要はない。代表例としてサーバを介さない通信であるP2Pが挙げられる。
第四章 URIの仕様
URIの構文
http://blog.example.jp/entries/1
このURIを構成するパーツは次のようになる。
- URIスキーム(URIが利用するプロトコル)
- http
- ホスト名(DNSで名前が解決できるドメイン名かIPアドレス)
- blog.example.jp
- インターネット上で必ず一意になる。
- パス(ホスト名の中でのリソースを一意に指し示すもの)
- /entries/1
複雑なURI
http://yohei:pass@blog.example.jp:8000/search?q=test&debug=true#n10
- URIスキーム(略)
- http
- ユーザ情報(リソースにアクセスする際に利用するユーザ名とパスワード)
- yohei:pass
- 後の「@」は区切り文字
- ホスト名(略)
- blog.example.jp
- 後の「:」は区切り文字
- ポート番号(ホストにアクセスする際のプロトコルで用いられるTCPのポート番号)
- 8000
- パス(略)
- /search
- 後の「?」は区切り文字
- クエリパラメータ(クライアントから動的にURIを生成する際に用いいられる)
- q=test&debug=true
- クエリが複数あるときは「&」でつなぐ。
- 検索サービスに検索キーワードを引き渡すときが動的なURI生成の例
- URIフラグメント(リソース内部の細かい部分を特定するときに利用)
- #n10
- HTML文書の場合id属性の値がn10であることを示す。
絶対URIと相対URI
相対URIは絶対URIからURIスキームとホスト名を除いてパスだけで表現したもの
ベースURI
相対URIはそのままでは起点となるURIがどこなのか分からないため解釈できない。
この起点となるのがベースURIである。(下表参照)
ベースURIhttp://example.jp/foo/bar
として
相対URI | 絶対URI |
---|---|
hoge/fuga | http://example.jp/foo/bar/hoge/fuga |
../../hoge | http://example.jp/hoge |
?q=hoge | http://example.jp/foo/bar?q=hoge |
このベースURIを指定する方法として以下の方法がある。
- あるリソースを取得したときに、そのURIをベースURIとして相対URIを取得する方法
- HTMLやXMLの中で明示的にページURIを指定する方法
URIと文字
URIで使用できる文字
- アルファベット:A-Za-z
- 数字:0-9
- 記号:-.~:@!$&'()
%エンコーディング
許可されている文字を使用する場合には%エンコーディングでその文字をエンコーディングする。
例えば下のサイトは
https://ja.wikipedia.org/wiki/あ
下のようにエンコーディングされてブラウザとサーバの間で転送される。
https://ja.wikipedia.org/wiki/%E3%81%82
この%エンコーディングはどの文字エンコーディングを使用するかで違いが生じる。
しかし、現代的なWebさいとの多くは文字エンコーディングとしてUTF-8を採用しているため、UTF-8を採用するのが無難と言える。
URIの実装上気をつけなければならないのは相対URIの解決と%エンコーディングの2つである。
第五章 URIの設計
良いURIやきれいなURIのことを「クールURI」と呼ぶ。
Burnres-Leeは「URIは変わらないべきである。変わらないURIこそが最上のURIである。」という主張をクールURIに込めた。クールURIの設計をこの章でまとめる。
URIを変わりにくくするためには
ポイントは以下の3点
- URIにプログラミング言語依存の拡張子やパスを利用しない。
- javaやrb、plなど
- javaの例:servlet/LoginServlet
- PHPに変えた途端servletの部分の記述は変更されてしまう。
- またクラス名「LoginServlet」はRubyやPerlでは先頭の「L」は小文字が文化
- メソッド名やセッションIDを含めない。
- リファクタリングでメソッド名を変えるとURIも変更されてしまう。
- セッションIDはログインのたびに変更されてしまう。
- URIはそのリソースを表現する名詞である。
- 「名詞」なのでそのリソースを変更する、取得する等の「動作」はURIに含めるべきでない。
URIのユーザビリティ
クールURIとはシンプルなURIでもあり、実装依存を排除しシンプルなURIにすればURIを変更しにくくなる。これは比較的設計者目線のメリットだが、シンプルなURIは覚えやすく、開発者ではない普通の人にも使いやすくなるというメリットがある。
URIを変更したいとき
変わらないURIこそが、クールなURI。この定義に立ち返ると現在運用しているURIを安易に変更してはならない。しかし、ハードウェアの老朽化やシステム全体の機能変更・追加などでどうしてもシステムを入れ替えなければならないことがある。その場合、リダイレクトを用いて古いURIを新しいURIに転送するとよい。
URI設計のテクニック
コンテントネゴシエーション
グローバル企業の場合、プレスリリースを1つ公開するといっても英語や日本語など様々な言語で記述することが一般的である。このときHTTPにはコンテントネゴシエーションという便利な機能があり様々な言語のOSに対してその言語に対応したレスポンスを返すことができる。例えば日本語のOSから以下のようなリクエストが来たとする。
GET /2010/05/01/press HTTP/1.1
Host: example.jp
Accept-Language: ja,en_us;q=0.7,en;q=0.3
Accept-Languageヘッダではクライアントが表示してほしい言語を指定している。(詳しくは9章で記述)このリクエストに基づいて日本語のプレスリリースをレスポンスとして返す。
ここでもしURIのパスが/press
だけの場合、日本語のOSを用いている人は英語のプレスリリースを取得するためにわざわざブラウザの設定を変えなければならない。このような問題に対処するためにURIに言語の拡張子をつけ、リソースの言語を明示的に指定するURIを用いる(/press.en)。
また、同様にしてフォーマットの指定も可能(html, txt, json)。
補足
拡張子はURIの設計上悪だと述べたが、実装に依存しない拡張子の場合以上に述べたようなメリットもある。
マトリクスURI
URIは「/」を用いて階層表現ができる。
http://example.jp/diary/2010/05/01
これは日付情報が年→月→日という階層構造を持っているから。
一方で階層では表現できない情報も存在する。例えば、地図上のある点を指すWebサイトがあるとして緯度(lat)と経度(lng)が情報として必要になるが、このパラメータを階層化することは不適切である。この場合以下のようなURIが使われる。
http://example.jp/map/lat=35;lng=139
このように複数の軸のパラメータをセミコロン(;)で区切ってリソースを表現する。
URIの不透明性
クライアントを作る際、URIをクライアント側で操作したり、構築してはならない。
なぜならクライアント側でURIの構築をした場合、サーバ側でURIの構造を変更しよとすると密結合状態になりシステムが動かなくなるからである。常にURIはクライアント側で組み立てたり、クライアント側が拡張子からリソースの内容を推測できない、つまり「URIはクライアントにとって不透明である」状態を保たなければならない。
第六章 HTTPの基本
HTTPの重要性
HTTPはRESTの重要な特徴を実現しているWebの基盤となるプロトコルである。現在のWebにはバージョン1.1のHTTPが最も使われている。HTTPはHTMLやXMLなどのハイパーテキストのみならず、静止画、音声、Javascriptプログラムなどコンピュータで扱えるデータであれば何でも転送できる。
TCP/IPとは何か
インターネットのプロトコルは階層型になっている。
階層型プロトコル |
---|
アプリケーション層:HTTP、NTP、SSH、SMTP、DNS |
トランスポート層:UDP、TCP |
インターネット層:IP |
ネットワークインターフェース層:イーサネット |
ネットワークインターフェース層
物理的なケーブルやアダプタ
インターネット層
ネットワークで実際にデータをやり取りする部分でIPが担う。IPではデータの基本的な単位を「パケット」と呼ぶ。指定したIPアドレスを送り先としてパケット単位でデータをやり取りする。
IPでは、自分のネットワークインターフェースでデータを送り出すことだけを保証しているが、最終的な送り先まで到達することは保証しない。
トランスポート層
IPが保証しなかったデータの転送を保証するのがTCP。TCPでは送り先に対してコネクションを張りコネクションを使ってデータの抜け漏れをチェックし、データの到達を保証する。どのアプリケーションに渡るのかを決めるのがポート番号で1から65535までの番号が割り当てられており、HTTPではデフォルトで80が割り当てられている。
アプリケーション層
メールやHTTP、DNSなどの具体的なアプリケーションを実現する層。
TCPでプログラムを作る際にはソケットと呼ばれるライブラリを用いるのが一般的で、ソケットはネットワークでのデータのやり取りを抽象化したAPIである。接続、送信、受信、切断などの基本的な機能を備えている。HTTPサーバやブラウザはソケットを用いて実装する。
ほとんどのプログラミング言語にはHTTPを実装したライブラリが標準で搭載されているためソケットを用いたHTTPの独自実装はほとんど行われない。
リクエストとレスポンス
HTTPではクライアントが出したリクエストをサーバで処理してレスポンスを返す。このようなプロトコルをリクエスト/レスポンス型のプロトコルと呼ぶ。HTTPは同期型のプロトコルであるため、サーバでの処理に時間がかかる場合でもクライアントはレスポンスが返ってくるまで待機する。このようなやりとりの際に、クライアントとサーバで行われていることをみていく。
クライアントで行われること
- リクエストメッセージの構築
- リクエストメッセージの送信
- (レスポンスが返ってくるまで待機)
- レスポンスメッセージの受信
- レスポンスメッセージの解析
- クライアントの目的を達成するために必要な処理
画像やスタイルシートなどへのリンクがいくつも含まれている場合、1回だけではなく数十回リクエストを発行しなければならない場合もある。
サーバで行われること
- (リクエストの待機)
- リクエストメッセージの受信
- リクエストメッセージの解析
- 適切なアプリケーションプログラムへの処理の委譲
- アプリケーションプログラムから結果を取得
- レスポンスメッセージの構築
- レスポンスメッセージの送信
アプリケーションではデータベースから最新記事を取得したり、広告へのリンクを生成したりする。
HTTPメッセージ
リクエストメッセージとレスポンスメッセージをまとめて「HTTPメッセージ」と呼ぶ。
リクエストメッセージ
まずhttp://example.jp/test
に対する必要最小限のリクエストは
GET /test HTTP/1.1
Host: example.jp
1行目は「リクエストライン」と呼び、メソッド(GET)、リクエストURI(/test)、プロトコルバージョン(HTTP/1.1)から成る。リクエストURIには絶対URIを入れてもいい。また複雑なURIの場合は次のようになる。
GET /search?q=test&debug=true HTTP/1.1
Host: example.jp:8000
リクエストラインにはURIフラグメントを除いたパス以降の文字列が入る。URIフラグメントはクライアント側で処理するのでリクエストメッセージには含めない。ポート番号はHostヘッダで指定。
リクエストメッセージの2行目以降はヘッダが続く。ヘッダはリクエストメッセージのメタデータである。各ヘッダは「名前:値」という構成をしている。また、ヘッダの後にはボディが続くこともある。例えばリソースを新しく作成したり更新したりするときにリクエストのボディにリソースの表現そのものが入る。
レスポンスメッセージ
先程のhttp://example.jp/test
へのリクエストが成功すると次のレスポンスがクライアントに返される。
HTTP/1.1 200 OK
Content-Type: application/xhtml+xml; charset=utf-8
<html xmlns="http://www.w3.org/1999/xhtml">
・・・
</html>
基本的な構造はリクエストメッセージと同じだが、リクエストラインの部分はステータスラインと呼び、ステータスコード(200)とテキストフレーズ(OK)が入る。これらについては8章で記述する。
HTTPのステートレス性
まずステートレスとは何かを日常生活におけるステートレスなやり取りとステートフルなやり取りを比較して見ていく。
ステートレスなやり取り | ステートフルなやり取り |
---|---|
ご注文をお伺いします。 | ご注文をお伺いします。 |
ハンバーガーセットで。 | ハンバーガーセットで。 |
サイドメニューはいかがなさいますか? | サイドメニューはいかがなさいますか? |
ハンバーガーセットをポテトで。 | ポテトで。 |
ドリンクはいかがなさいますか? | ドリンクはいかがなさいますか? |
ハンバーガセットをポテトとコーラで。 | コーラで。 |
このようにステートレスなやり取りでは店員(サーバ)がお客様(クライアント)の注文を覚えていないため、毎回同じ注文内容を全て繰り返さなければならないのに対し、ステートフルなやり取りではその差分だけを説明すればいい。なお、「ハンバーガーセットをポテトで」というお客様(クライアント)の注文内容をクライアントのアプリケーション状態という。
ステートフルの欠点
このやり取りの欠点はサーバがクライアントが増加すればそれだけアプリケーション状態を覚えることが難しくなる点にある。特に不特定多数のクライアントを相手にする場合、クライアントごとに接続するサーバを特定できないためどのサーバでも同じアプリケーション状態を扱えるようにデータを同期しなければならずこの際のオーバーヘッドが無視できないほど大きくなる。そのため、スケールアウトしにくい。
ステートレスの利点
クライアントからのリクエストに必要な情報が全て含まれている「自己記述的メッセージ」がサーバ側に送られるため、ステートレスなサーバはアプリケーション状態を覚える必要がない。そのため、サーバは単純になり新しく来るリクエストの処理に集中すれば良い。この場合クライアントが増えてもサーバを増設するだけでどのサーバにリクエストを送っても構わないのでスケールアウトしやすい。
ステートレスの欠点
一方で、ステートレスには欠点もある。
- パフォーマンスの低下
サーバをステートレスにするにはクライアントから毎回必要な情報を全て送信しなければならないが、
「送信するデータ量の増加」、「認証などサーバに負荷がかかる処理を繰り返す」
といった理由からパフォーマンスに影響を与える。
- 通信エラーへの対処
ステートレスなアーキテクチャではネットワークトラブルが起きたときにリクエストが処理されたかどうかが分からない。
第七章 HTTPメソッド
HTTPには以下の8つのメソッドが定義されており、主に以下の6つのメソッドが使われる。
メソッド | 意味 |
---|---|
GET | リソースの取得 |
POST | 子リソースの作成、リソースへのデータ追加、その他の処理 |
PUT | リソースの更新、リソースの作成 |
DELETE | リソースの削除 |
HEAD | リソースのヘッダの取得 |
OPTIONS | リソースがサポートしているメソッドの取得 |
HTTPメソッドのうちGET、POST、PUT、DELETEはこれら4つで「CRUD」という性質を満たす。
「CRUD」はCreate(作成)、Read(読み込み)、Update(更新)、Delete(削除)というデータ操作の基本となる4つの処理のこと。CRUDとHTTPメソッドの対応関係は以下のようになる。
CRUD | 意味 | HTTPメソッド |
---|---|---|
Create | 作成 | POST、PUT |
Read | 読み込み | GET |
Update | 更新 | PUT |
Delete | 削除 | DELETE |
以下それぞれのメソッドについて説明する。 |
GET
GET /list HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: application/json
[
{"uri": "http://example.jp/list/item1"},
{"uri": "http://example.jp/list/item2"},
{"uri": "http://example.jp/list/item3"},
{"uri": "http://example.jp/list/item4"},
{"uri": "http://example.jp/list/item5"},
]
http://example.jp/list
に対するGETで、サーバはリクエストに対して指定されたURIに対応するデータをレスポンスとして返している。
POST
子リソースの作成
POST /list HTTP/1.1
Host: example.jp
Content-Type: text/plain; charset=utf-8
こんにちは!
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
Location: http://example.jp/list/item5
こんにちは!
新しい子リソースの作成をPOSTで指示し、ボディにリソースの内容を入れてある。このリクエストに応じて/list
の下に新たに/list/item5
というリソース(子リソース)を作成。201 Createdは新シリソースを生成したことを示す。
リソースへのデータの追加
ログリソースへのデータの追加を考える。まずはリソースをGETしてみる。
GET /list HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
2010-10-10T10:10:00Z, GET /list, 200
2010-10-10T10:11:00Z, POST /list, 201
2010-10-10T10:20:00Z, GET /list, 200
このリソースのURIはhttp://example.jp/log
で、CSV形式のログを表現。
新しいログを追加するにはPOSTを用いる。
POST /log HTTP/1.1
Host: example.jp
2010-10-10T10:13:00Z, GET /log, 200
HTTP/1.1 200 OK
200 OKが返ってきたのは新規リソースの作成ではなく、データの追加を意味したから。
あるリソースへのPOSTが作成を意味するのか追加を意味するのかはURIでは判別できず実装に依存するため、挙動はWebサービスやWeb APIの仕様書などで表現する。
他のメソッドでは対応できない処理
たとえば検索結果を表現するURIを例に考えてみる。
http://example.jp/search?q={キーワード}
通常であればこのURIをGETするころで検索を実行するが、キーワードが非常に長い場合、実装によってはこのURIが長さの上限にかかってしまう場合がある(仕様上は長さ制限はない)。この場合以下のようにPOSTを用いる。
POST /search HTTP/1.1
Host: example.jp
Content-Type: application/x-www-form-urlencoded
q=very+long+keyword+foo+bar+...........
GETではURIに含めていたキーワードをPOSTではリクエストボディに入れることができる。
PUT
リソースの更新
GET /list/item5 HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
こんにちは!
このリソースをPUTを使って更新する。
PUT /list/item5 HTTP/1.1
Host: example.jp
Content-Type: text/plain; charset=utf-8
こんばんは!
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
こんばんは!
このようにPUTへのレスポンスでリソースを更新した表現が入っている。
なお、ボディには何も入れずにレスポンスがボディを持たないことを示す204 No Contentを返しても良い。
リソースの作成
PUT /newitem HTTP/1.1
Host: example.jp
Content-Type: text/plain; charset=utf-8
新しいリソース/newitemの内容
HTTP/1.1 201 Created
Content-Type: text/plain; charset=utf-8
新しいリソース/newitemの内容
このPUTは存在しないURIへのリクエストのため、サーバはリソースを新たに作成すると解釈し、リクエストに成功すると201 Createdを返す。POSTの場合は新しく作成したリソースのURIをLocationヘッダで返していたが、PUTの場合クライアントがすでにURIを知っているためLocationヘッダを返す必要がない。
POSTとPUTの使い分け
どのように使い分けるかに正解はないが設計上の指針として次の事実がある。
「POSTでリソースを作成する場合、URIの決定権はサーバ側にあるが、PUTの場合はその決定権がクライアント側にある。」
たとえばTwitterのようにつぶやきのURIをサーバで自動的に決定する場合、POSTを用いるのが普通。逆にWikiのようにクライアントが決めたタイトルがそのままURIになるWebサービスの場合はPUTを使うのが適している。しかし、クライアントがリソースのURIを決定できるということは、クライアントを作るプログラマーがサーバの内部実装を熟知していなければならない。そのため、PUTのほうがサーバとの結合が密になるのでリソースの作成はPOST、URIはサーバ側が決めるという設計が望ましい(第5章参照)。
DELETE
DELETE /list/item2 HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
一般にDELETEのレスポンスはボディを持たないのでステータスコードには204 No Contentが使われる場合もある。
HEAD
GETがリソースを取得するメソッドに対して、HEADはリソースのヘッドだけを取得するメソッドである。
HEAD /list/item1 HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
レスポンスにはボディが含まれないためネットワークの帯域を節約しながらリソースの大きさを調べたり、更新日時を取得できたりする。
OPTIONS
そのリソースがサポートしているメソッドの一覧を返す。なお、OPTIONS自体はAllowヘッダには含めない。
OPTIONS /list/ HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Allow: GET, HEAD, POST
OPTIONSを実装する場合、多くのWebアプリケーションフレームワークでリソースに対応しているメソッドを返すように自前で実装しなければならない。
POSTでPUT/DELETEを代用する方法
6つのHTTPメソッドを説明してきたが、現実に一番良く利用されているのはGETとPOSTの2つだけである。これは、HTMLフォームに指定できるメソッドがGETとPOSTだけという制限に起因する。AjaxのXMLHttpRequestというモジュールを用いることで任意のメソッドを発行できるようにはなったが、このモジュールをサポートしない携帯電話向けブラウザはフォームしか利用できず、結果的にGETとPOST以外は使えない。またプロキシでこの2つのメソッド以外を制限している場合もある。このような状況でサーバにPUTやDELETEを伝える方法がある。
_methodパラメータ
_methodパラメータはRuby on Railsが採用。
<form method="POST">
<input type="hidden" id="_method" name="_method" value="PUT"/>
<textarea id="body">...</textarea>
</form>
PUT /list/item1 HTTP/1.1
Host: example.jp
Content-Type: application/x-www-form-urlencoded
_method=PUT&body=...
Webアプリケーションフレームワークなどサーバ側の実装は_methodパラメータを見て、このリクエスト自体をPUTとして扱う。
X-HTTP-Method-Override
_methodパラメータはフォームを用いてリクエストを送る際には有効だが、POSTの内容がXMLなどapplication/x-www-form-urlencoded以外の場合は利用できない。この場合にX-HTTP-Method-Overrideヘッダが利用できる。これはGoogleのGDataが採用している手法。
PUT /list/item1 HTTP/1.1
Host: example.jp
Content-Type: application/xml; charset=utf-8
X-HTTP-Method-Override: PUT
<body>...</body>
サーバ側の実装はX-HTTP-Method-Overrideヘッダを見てこのリクエストをPUTとして扱う。
条件付きリクエスト
HTTPメソッドと更新日時などのヘッダを組み合わせることでメソッドを実行するかどうかをリソースの更新日時を条件にサーバが選択できるようになる。このようなリクエストを「条件付きリクエスト」と呼ぶ。
ヘッダとしてはIf-Modified-Since、If-UnModified-Sinceなどが使われる。
べき等と安全性
通信エラーが起きたときにリクエストをどのように回復するかという問題を解決するための工夫がHTTPの仕様でなされている。HTTPメソッドはその性質によって以下のように分類できる。
メソッド | 性質 |
---|---|
GET、HEAD | べき等かつ安全 |
PUT、DELETE | べき等だが安全でない |
POST | べき等でも安全でもない |
べき等:ある操作を何回行っても結果が同じこと
安全:操作対象のリソースの状態を変化させないこと
PUTとDELETE(べき等だが安全でない)
どちらも更新、削除と操作対象のリソースの状態を変化させているという意味で「安全でない」が、何度同じ内容でリクエストを送信しても同じ内容が返ってくるので「べき等」
DELETEの場合は二回目以降404 Not Foundが返ってくるがリソースを削除したという結果は変わらない。
GETとHEAD(べき等かつ安全)
両方のメソッドが何回同じ内容をリクエストしても結果は変わらず、かつ操作対象のリソースを変化させることもない。
POST(べき等でも安全でもない)
POSTはべき等でも安全でもないため、リクエストの結果何が起こるかわからない。そのためステートレスなサーバではもし通信エラーの際にPOSTを再送信してしまうと二重注文のような問題が起こる可能性がある。
メソッドの誤用
GETやHEADがべき等かつ安全、PUTとDELETEがべき等であることはHTTPの仕様で定められているが、WebサービスやWeb APIの設計を誤ると、安全やべき等が保たれなくなる。
GETが安全でなくなる例
GET /resources/1/delete HTTP/1.1
Host: example.jp
この例では/resources/1
を削除するために/resources/1/delete
をGETしている。このようにGETでリソースを更新したり、削除したりすると安全ではなくなる。この例ではそもそも名詞であるはずのURIにdeleteという動詞が入っていることもおかしい。
他のメソッドでできることにPOSTを誤用
POSTは他のメソッドでは対応できない処理をする点において万能メソッドではあるが、その万能性ゆえにPOSTをもちいてGETやPUT、DELETEなどのメソッドを実現してしまう。他の適切なメソッドが用意されているのにPOSTを用いることは、べき等性や安全性を利用できなくなるので誤用である。
PUTがべき等でなくなる例
現在のトマトの価格が100円であるとして、PUTで価格を更新することを考える。
PUT /tomato HTTP/1.1
Host: example.jp
Content-Type: text/plain; charset=utf-8
+50
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
150
これにもう一度同じリクエストを送るとトマトの価格が200円になり、べき等性が成り立っていない。この場合、価格を差分で表現せずに変更後の値で表現するべき。
DELETEがべき等でなくなる例
ソフトウェアの最新バージョンを表現するhttp://example.jp/latest
というエイリアスリソースを考える。
DELETEによってエイリアスリソースそのものを削除するようにWebサービスやWeb APIを実装している場合べき等性が確保できているが、/latestが意味的に指し示す最新バージョンのリソースを削除してしまうと、
DELETE /latest HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
http://example.jp/1.2を削除しました。
DELETE /latest HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
http://example.jp/1.1を削除しました。
このように最初のDELETEで最新バージョン1.2、次のDELETEで1.1というようにDELETEがべき等でなくなってしまう。
この場合エイリアスリソースそのものを削除するように設計するのもいいですが、時間や状況で指し示すリソースが変化する特殊なリソースは特別な理由がない限り更新や削除などの操作ができなくなるように設計すると良い。
第八章 ステータスコード
ステータスコードの分類と意味
- 1xx:処理中
- 処理が継続していることを示す。リクエストを継続するか、サーバの指示に従ってプロトコルをアップデートして再送信する。
- 2xx:成功
- リクエスト成功
- 3xx:リダイレクト
- 他のリソースへのリダイレクトを示す。
- 4xx:クライアントエラー
- クライアントのリクエストが原因のエラー。エラーを解消しないと正常な結果が得られないので同じリクエストをそのまま再送信できない。
- 5xx:サーバエラー
- 原因はサーバ側にある。サーバ側の問題が解決すれば同一のリクエストを再送信して正常な結果が得られる可能性がある。
ステータスコードを先頭の文字で分類するのには2つの意味がある。
- クライアントはとりあえず先頭の文字を見ればサーバがどのようなレスポンスを返したのか理解でき、クライアントでどのような処理をするべきかの大枠を知ることができる。
- クライアントとサーバの約束事を最小限に抑えて、結びつきをなるべく緩やかにする。
よく使われるステータスコード
200 OK(リクエスト成功)
201 Created(リソースの作成成功)
これら2つは今まで出てきていたのでここでは割愛する。
301 Moved Permanently(リソースの恒久的な移動)
リクエストで指定したリソースが新しいURIに移動したことを示す。別のURIにクライアントが自動的に再接続する処理を「リダイレクト」と呼ぶ。
303 See Other(別URIの参照)
リクエストに対する処理が別のURIで取得できることを示す。
POST /login HTTP/1.1
Host: example.jp
Content-Type: application/x-www-form-urlencoded
username=yohei&password=foobar
HTTP/1.1 303 See Other
Location: http://example.jp/home/yohei
Content-Type: application/xhtml+xml; charset=utf-8
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>redirect</title></head>
<body>
<p><a href="http://example.jp/home/yohei">結果</a>を確認してください。</p>
</body>
</html>
GET /home/yohei HTTP/1.1
Host: example.jp
HTTP/1.1 200 OK
Content-Type: application/xhtml+xml; charset=utf-8
<html xmlns="http://www.w3.org/1999/xhtml">
.....
</html>
このように典型的にはブラウザからPOSTでリソースを操作した結果をGETで取得するときに使う。
400 Bad Request(リクエストの間違い)
リクエストの構文やパラメータが間違っていたことを示す(例としてパスワードが単調すぎるなど)。また、適切なクライアントエラーを示すステータスコードがない場合やクライアントにとって未知の4xx系のステータスコードが返ってきたときにも400 Bad Requestと同じ扱いで処理するよう仕様で定められている。
401 Unauthorized(アクセス権不正)
適切な認証情報を与えずにリクエストを行ったことを示す。
404 Not Found(リソースの不在)
指定したリソースが見つからないことを示す。
500 Internal Server Error(サーバの内部エラー)
サーバ側に何らかの異常が起きていて正しくレスポンスが返せないことを示す。また、適切なサーバエラーを示すステータスコードがない場合やクライアントにとって未知の5xx系のステータスコードが返ってきたときにも500 Internal Server Errorと同じ扱いで処理するよう仕様で定められている。
503 Service Unavailable(サービス停止)
サーバがメンテナンスなどで一時的にアクセスできないことを示す。
ステータスコードとエラー処理
4xx系と5xx系のステータスコードはエラーを表現しているがボディにどんなエラーメッセージを入れるかは規定していない。たとえば404 Not Foundでは「ご指定のページは見つかりませんでした。」というメッセージ入りのHTMLをボディに加えることが一般的である。人間用のWebサービスの場合はエラーメッセージがHTMLで何も問題はないが、プログラム用のWeb APIは注意が必要である。これはクライアントがHTMLを解釈できるとは限らないからで、クライアントが解釈できる形式でエラーメッセージを返すと良い。例えば13章で出てくるAtomPubを利用したWeb APIの場合、AtomPubクライアントはAtom形式なら解釈できるためエラーメッセージはAtomで返すのが1つの方法である。
第九章 HTTPヘッダ
HTTPヘッダの重要性
HTTPヘッダはメッセージのボディに対するメタデータを表し、メディアタイプや言語タグなどをつける役割があり、認証やキャッシュなどメソッドやステータスコードなどと組み合わせて実現する機能もあるためHTTPの重要な構成要素である。
日時
Date: Tue, 06 Jul 2010 03:21:05 GMT
HTTPでは日時は全てGMTを使用することになっている。以下に日時を持つヘッダ一覧を示す。
利用するメッセージ | ヘッダ | 意味 |
---|---|---|
リクエストとレスポンス | Date | メッセージを生成した日時 |
リクエスト | If-Modified-Since | 条件付きGETでリソースの更新日時を指定するときに利用する。 |
リクエスト | If-Unmodified-Since | 条件付きPUTや条件付きDELETEでリソースの更新日時を指定するときに利用する。 |
レスポンス | Expires | レスポンスをキャッシュできる期限 |
レスポンス | Last-Modified | リソースを最後に更新した日時 |
レスポンス | Retry-After | 再度リクエストを送信できるようになる日時の目安 |
MIMEメディアタイプ
リソースの表現の種類を示す。事例は今までも出てきていたので省略する。
Content-Type
ボディの内容がどのような種類なのかをメディアタイプで示す。
例えば「application/xhtml+xml」の場合、「/」の左側がタイプ、右側がサブタイプでタイプは勝手に増やすことができず現状以下の表の9つが定義されている。なお、サブタイプは自由に増やせる。
タイプ | 意味 | 例 |
---|---|---|
text | 人が読んで直接理解できるテキスト | text/plain |
image | 画像データ | image/jpeg |
audio | 音声データ | audio/mpeg |
video | 映像データ | video/mp4 |
application | その他のデータ | application/pdf |
multipart | 複数のデータからなる複合データ | multipart/related |
message | 電子メールメッセージ | message/rfc822 |
model | 複数次元で構成するモデルデータ | model/vml |
example | 例示用 | example/foo-bar |
登録済みのタイプとサブタイプはIANAが管理しており、こちらのサイトMedia Typesでその一覧を見ることができる。
charsetパラメータ
charsetパラメータは省略可能であるが、タイプがtextの場合は要注意である。というのはtextのデフォルトエンコーディングがISO8859-1であると設定されており、仮にメッセージに日本語文字列が含まれていてもISO8859-1でエンコーディングされ、文字化けを起こしてしまう可能性がある。また、xmlでエンコーディングを宣言してもtextタイプの場合はContent-Typeヘッダのcharsetパラメータが優先されるためcharsetパラメータを省略せず書いておくのが良い。
言語タグ
Content-Language: ja-JP
左側にはISO 639が定義する言語コード、右側にはISO 3166が定義する地域コードが入る。日本語の場合、日本でしか使用されないため言語コードと地域コードが等しいが英語の場合だと「en-US」「en-CA」など様々な国で使われる英語の種類が定義される。
コンテントネゴシエーション
Accept(処理できるメディアタイプを伝える)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
この場合優先度は、「text/html,application/xhtml+xml」がデフォルトの1、application/xmlが0.9、その他の全てのメディアタイプが0.8となる。
メディアタイプにサーバが対応していなかった場合、「406 Not Acceptable」が返る。
Accept-Charset(処理できる文字エンコーディングを伝える)
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
なお、ISO 8859-1はデフォルトで1となっているがより具体的なShift_JISが優先される。
Accept-Language(処理できる言語を伝える)
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Content-Lengthとチャンク転送
Content-Length
ボディの長さを指定できる。静的なファイルなどあらかじめサイズの分かっているリソースを転送する際にこのヘッダを利用するのが簡単。
Content-Length: 5538
チャンク転送
動的に画像を生成するようなWebサービスの場合にTransfer-Encodingヘッダが用いられる。
Transfer-Encoding: chunked
これを指定することで最終的なサイズがわからないボディを少しずつ転送できるようになる。
POST /test HTTP/1.1
Host: example.jp
Transfer-Encoding: chunked
Content-Type: text/plain; charset=utf-8
10
Thebrown for ju
10
mps quickly over
e
the lazy dog.
0
各チャンクの先頭にはチャンクサイズが16進数で入る。最後には必ず長さ0のチャンクと空行がつく。
認証
Basic認証
まずクライアントはサーバに
DELETE /test HTTP/1.1
Host: example.jp
Authorization: Basic dXNlcjpwYXNzd29yZA==
Authorizationヘッダの内容は認証方式(Basic)に続けて、ユーザ名とパスワードを「:」で連結し、Base64エンコードした文字列(デコードするとuser:password)。このエンコードは簡単にデコードされてしまうので、それが許される程度のセキュリティで良いのか、HTTPS通信で通信路上で暗号化するのかを考える必要がある。
Digest認証
ダイジェストとはあるメッセージに対してハッシュ関数を適用した結果のハッシュ値。まずクライアントは認証情報なしでリクエストを送信する。401 Unauthorizedが返ってくるが、WWW-Authenticateに含まれる値「チャレンジ」を使って次回のリクエストを組み立てる。チャレンジに含まれる値について以下に説明する。
- nonce
number used onceのことを意味し、リクエストごとに変化する文字列を指す。基本的にタイムスタンプやサーバだけが知り得るパスワードから生成する。タイムスタンプはnonceを使ったリクエストの有効期間を狭めるため。 - qop
quality of protectionの略でauthかauth-initを指定する。クライアントが送信するダイジェストの作成方法に影響を与える。authはメソッドとURIからダイジェストを作成し、auth-initはそれに加えてメッセージボディも利用する。POSTやPUTでボディを送信するときにauth-initを使うとメッセージ全体が改ざんされていないことを保証できる。 - opaque
クライアントには不透明な文字列で、同じURI空間へのリクエストでは共通してクライアントからサーバに送る。
これらの値を用いてどのようにダイジェストを生成するのかについて以下に記す。
- ユーザ名、realm、パスワードを「:」で連結し、MD5ハッシュ値を求める。
- メソッドとURIのパスを「:」で連結し、MD5ハッシュ値を求める。
- 「1」の値、サーバから得たnonce、クライアントがnonceを送った回数、クライアントが生成したnonce、qop、「2」の値を「:」で連結し、MD5ハッシュ値を求める。
この方法で求めたダイジェスト値をresponceというフィールドに入れてリクエストを送信。
Digest認証の欠点
- パスワードを暗号化することはできるが、メッセージは平文なので暗号化するためにHTTPSを用いる必要がある。
- リクエストのたびに401 Unauthorizedレスポンスを得る必要がある。
その他の認証方式
ここでは紹介しないが、AtomPubなどのWeb APIの認証に用いられるWSSE認証などもある。
キャッシュ
キャッシュ用ヘッダ
3種類のキャッシュ用ヘッダが存在する。
-
Pragma
- このヘッダに指定できる値は公式にはno-cacheのみでリソースをキャッシュしてはならないことを示す。
-
Expires
- キャッシュの有効期限を示す。
- クライアントはリソースにアクセスするときこの期限内か否かでサーバに再度アクセスするかキャッシュを利用するかを決定。
-
Cache-Control
- 上の2つよりキャッシュに複雑な設定を行いたいときに用いる。
条件付きGET
キャッシュ用ヘッダによってキャッシュが再利用できないと判断された場合でも条件付きGETを利用することで再利用できる可能性がある。これはサーバ側のリソースがクライアントローカルのキャッシュから変更されているかどうかを調べるヒントをリクエストヘッダに含めることでキャッシュがそのまま使えるかどうかを検証する。
- If-Modified-Since
ローカルキャッシュの更新日時をリクエストヘッダに含めて、これ以降変更されていないかを検証。変更されていなければ304 Not Modifiedを返す。 - If-None-Match
- If-None-MatchヘッダとETagヘッダを用いて行う検証で、If-None-Matchヘッダに指定した値が変更されているかどうかを検証。変更がなければ304 Not ModifiedとETagを返す。ETagはリソースの更新状態を比較するための文字列で更新したときに別の値になるので、更新しているかどうかをこのETagで確認することがきでる。