今回は前回に続いて、プロトコルについてです。前回はSMTPでしたが、今回はおそらく最もメジャーなプロトコルであるといっても過言ではないHTTPについてです。
「デカいRFC」の読み方
まずHTTPにはバージョンとして1.0と1.1があります。実質、今はほぼ100%のサイトが1.1だと思って良いでしょう。
HTT1.1はRFC2068で提唱され、RFC2616にObsoleteされています。ですのでRFC2616(と、RFC2616をUpdateしているRFC2817とRFC5785)を読めば、HTTP1.1のことが把握できます。とはいってもでかいRFCなので、読むのはちょっと大変かもしれません。ただ、これくらいポピュラーなRFCになると日本語訳もたくさんあるので、原文と一緒に日本語訳も読むと、理解が速いかもしれません(訳だけ読むのはあまりオススメできません) 。
また、RFCを読むとき(特にプロトコルに関するRFCを読むとき)は、BNF記法の知識が必要になります。BNFとはバッカスナウアフォーム(Backus-Naur Form)の略で、正規表現のような、任意の文字列を表現するのに定められた記法です。またBNFにはEBNF(Extended BNF)やABNF(Augmented BNF)といった発展形もあります。RFCではABNFがよく使われます。
BNFを知ることはプロトコルを覚えることにも役立つので、ちょっと例を見てみます。
たとえばPage17には、
HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT
とあります。
""で括られているのはリテラル(文字列)です。*は繰り返し、DIGITは数字を意味するシンボルです。ですので上記のBNFは、HTTP-Versionは「HTTP/数字1個以上.数字1個以上」と定義しているということになります。
BNF記法自体、その定義がRFC内に書いてあるので、それを読めば最初はちょっと苦労するかもしれませんが、それほど苦労せず読めるようになると思います。RFC2616だと、2.1節にABNFの定義が記述してあります。
HTTP/1.1の2大特徴
さて、もっとRFC2616を読み進めてみます。19.6節に、HTTP/1.0との違いが簡単にまとめられています。
まずHTTP/1.1の大きな特徴のひとつに、HOSTヘッダのサポートがあります。これはいわゆるname based virtualなWebサーバをサポートするためのものです。
いまではname based virtualなWebサーバは当たり前すぎますが、以前はバーチャルなWebサーバはIP basedか、そもそもバーチャルに対応していないという時代がありました。しかしそれではIPアドレスの枯渇に拍車をかけてしまうので、name based virtualという手法が考えられ、一気に広まりました。
簡単に言うと、name based virtualなWebサーバとは、HOSTヘッダという「何々という名前のWebサーバに接続していますよ」という情報を宣言するためのヘッダによって、返却するコンテンツを制御するものです(簡単に言ってないような…) 。
つまり、あるWebサーバがあったとして、そこにwww.foobar.comとwww.barbaz.comという2つのサイトの設定をname basedでもたせているとします。接続したクライアントがHOSTヘッダで、どちらのサイトのホスト名を伝えるかによって、サーバはそれに適したコンテンツを返却します。
たとえば
HOST: www.foobar.com
であればwww.foobar.comのコンテンツを、また
HOST: www.barbaz.com
であればwww.barbaz.comのコンテンツを返すということです。
ちなみにIP basedというのは、あるサーバに複数のIPをもたせてWebサーバをbindingしておき、どのIPに接続してくるかによって返却するコンテンツを制御するというものです。
そして、RFC2616の19.6.1.1には以下のように書いてあります。
Both clients and servers MUST support the Host request-header.
A client that sends an HTTP/1.1 request MUST send a Host header.
Servers MUST report a 400 (Bad Request) error if an HTTP/1.1
request does not include a Host request-header.
Servers MUST accept absolute URIs.
すなわち、HTTP/1.1の場合にはHOSTヘッダは必ずサポートしなければいけない、していないとHTTP/1.1非準拠である、ということになります。
もうひとつ、HTTP/1.1の大きな特徴に、Keep-Aliveの挙動があげられます。こちらは19.6.2に下記のように書いてあります。
Persisitent connections are the deafult for HTTP/1.1 messages; we introduce
a new keyword (Connection: close) for declaring non-persistence.
つまり、HTTP/1.1ではKeep-Alive接続がデフォルト挙動として定義されており、持続的接続を終了するためにConnection: closeという新しいキーワードが作られました。
HTTP/1.1の大きな特徴は上記の2点(name based virtual対応とKeep-Aliveのデフォルトの挙動)です。そして、name based virtualがあるが故に、HTTPを手で打てるというのが結構重要なのです(インフラエンジニアにとっては、ですが) 。
なぜかというと、ブラウザでWebの検証をする場合には、ブラウザはDNSで名前解決をして、その解決したIPに接続にいきます。つまり、DNSに登録されている名前は、
接続先IPのAレコード
であると同時に、
HOSTヘッダで伝えられるホスト名
でもあるのです。
ところが、検証などをしている時には、この2つを連動させたくないということがよくあります。もちろんDNS側で、Aレコードの答えをクエリ元によって変化させるという解決もありますが、それよりは手でHTTPを話せるほうが楽でしょう。もちろん、サイトのレンダリングの結果を見たいときには手でHTTPプロトコルを話しても意味がないですが。
手で打って覚えるHTTP
ではさっそく、HTTPの実例を見てみましょう。
$ telnet www.google.com 80
Trying 64.233.183.103...
Connected to www.google.com (64.233.183.103).
Escape character is '^]'.
GET / HTTP/1.0
HTTP/1.0 302 Found
Location: http://www.google.co.jp/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie: PREF=ID=dcd5652deaacf010:FF=0:TM=1303887216:LM=1303887216:S=cUNIWkuapqmEMcb3; expires=Fri, 26-Apr-2013 06:53:36 GMT; path=/; domain=.google.com
Date: Wed, 27 Apr 2011 06:53:36 GMT
Server: gws
Content-Length: 221
X-XSS-Protection: 1; mode=block
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.jp/">here</A>.
</BODY></HTML>
Connection closed by foreign host.
上記は、www.google.comに対してHTTP/1.0でHOSTヘッダもコンテントネゴシエーションもなしで接続した例です。
GET / HTTP/1.0
(空行)
というのが、クライアントが送信している(telnetで入力している)部分になります。あくまで、空行の入力があって、リクエストと解釈されるので注意が必要です。
それに対して、
HTTP/1.0 302 Found
というレスポンスが返ってきています。これはかいつまんで言うと、LocationヘッダにあるURLにリダイレクトしなさい、という意味になります。
Locationヘッダを見てみると、
Location: http://www.google.co.jp/
とありますので、通常ブラウザであればこのURLに接続し直すことになります。
HTTP/1.1での接続(1)
次に、www.google.comに対してHTTP/1.1でHOSTヘッダも付けて接続してみましょう。
$ telnet www.google.com 80
Trying 64.233.183.99...
Connected to www.google.com (64.233.183.99).
Escape character is '^]'.
GET / HTTP/1.1
HOST: www.google.com
HTTP/1.1 302 Found
Location: http://www.google.co.jp/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie: PREF=ID=7eab6b7eed7f7842:FF=0:TM=1303887994:LM=1303887994:S=4XH2bEZxD6t-DcEL; expires=Fri, 26-Apr-2013 07:06:34 GMT; path=/; domain=.google.com
Date: Wed, 27 Apr 2011 07:06:34 GMT
Server: gws
Content-Length: 221
X-XSS-Protection: 1; mode=block
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.jp/">here</A>.
</BODY></HTML>
ほとんど同じ内容になっていますが微妙に違います。
GET / HTTP/1.1
HOST: www.google.com
(空行)
が、クライアントから送信している(手入力した)部分です。空行を送るまではヘッダ扱いされます。
レスポンスもほとんど一緒ですが、違いとしては、HTTP/1.0の時はコネクションが切断されていた(Connection closed by foreign host.とtelnetがいっている)のに対し、今回はされていないということです(Connection closed by foreign host.はあくまでtelnetの出力でHTTPメッセージではありません) 。
ちなみにこういう場合、どうやって切断するかですが、telnetクライアントはたいてい CTRL-] というシーケンスを送ると接続を切断して
telnet>
というプロンプトになるので、そこでquitと入れて終了するか、もしくは適当にHTTPで解釈されない送信(a 一文字など)を送ればBad Requestとして切断されます。
HTTP/1.1での接続(2)
さらに続いて、Keep-Aliveでないことを宣言してHTTP/1.1にて接続してみましょう。
$ telnet www.google.com 80
Trying 64.233.183.105...
Connected to www.google.com (64.233.183.105).
Escape character is '^]'.
GET / HTTP/1.1
HOST: www.google.com
Connection: close
HTTP/1.1 302 Found
Location: http://www.google.co.jp/
Cache-Control: private
Content-Type: text/html; charset=UTF-8
Set-Cookie: PREF=ID=a967b73bc9b997b2:FF=0:TM=1303888386:LM=1303888386:S=xpUUMjtShIblnhJM; expires=Fri, 26-Apr-2013 07:13:06 GMT; path=/; domain=.google.com
Date: Wed, 27 Apr 2011 07:13:06 GMT
Server: gws
Content-Length: 221
X-XSS-Protection: 1; mode=block
Connection: close
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.jp/">here</A>.
</BODY></HTML>
Connection closed by foreign host.
今回の例では、
GET / HTTP/1.1
HOST: www.google.com
Connection: close
(空行)
という内容をリクエストとして送っています。すると、レスポンスが返ったあとに接続が切断されていることがわかります。
この例で重要なのは、telnetの引数に入れているホスト名と、HOSTヘッダで指定しているホスト名は「別にすることが可能」ということです。やろうと思えば、googleでないサーバに接続してgoogleのホスト名を指定することもできます。検証においてはこの「別にすることが可能」というのはとても重宝します(とはいっても自分に関係ないホストでやるのは良くないことですが) 。
今回は非常にざっくりですが、HTTPについて解説してみました。実際には他にもさまざまなヘッダやリクエスト、そしてレスポンスがあるので、ぜひいろいろと勉強してみてください。