54. Client-Server overview
【所要時間】
2時間17分(2018年9月21,25日)
【概要】
クライアントの動作について
【要約・学んだこと】
ここではサーバーがブラウザからdynamicリクエストを受け取ると何が起こるのかを詳しく見て行く。
Web servers and HTTP (a primer)
WebブラウザはHTTPを使ってweb serversと通信する。ページをクリックするとフォームを送信したり、検索を実行したり、ブラウザはHTTPリクエストをサーバーに送る。
このリクエストはこれらを含む。
- ターゲットサーバーとリソースを識別するURL(HTMLファイル、特にサーバー上のデータポイントや、実行のためのツール)
- 要求されたactionを定義するメソッド。(例えばファイルを取得、セーブ、データを更新など)。様々なメソッド/verbや、アクションに関連するリストは次の通り。
GET
: 特定のリソースを取得。 (e.g. 製品についての情報を持つHTML、製品リスト).POST
: 新しいリソースを作成。(e.g. wikiに新しい記事を加える、データベースに新しい連絡先を加える。).HEAD
: GETのようにbodyを取得することなく特定のリソースについてのメタデータを取得する。例えば、HEADリクエストがリソースが最後に更新された日時を探す。 そして、変更されている場合のみリソースをダウンロードするためにGETリクエストを使う。PUT
: 現在のリソースを更新する。もし存在しなければ新しいリソースを作る。DELETE
: 特定のリソースを削除する。TRACE
,OPTIONS
,CONNECT
,PATCH
: これらはあまり使われないのでここでは触れない。
追加情報はリクエストでエンコード(符号化)できる。
- URL parameters
GETがname/valueのペアを最後に加えることで、サーバー送られたURLにエンコードデータをリクエストする。
例えば、http://mysite.com?name=Fred&age=11 いつも?でURL parameterと残りのURLが区切られており、=で関係するvalueからそれぞれの名前を区切り、&でそれぞれのペアを分けている。
URL parameterはユーザーが変更してから再送信することができるため、本質的に安全ではない。結果として、URLparameter/GETリクエストはサーバー上のデータの更新をするリクエストには使われない。 - POSTデータ。POSTリクエストは新しいリソースをリクエストし、リクエストのbody内でエンコードされるデータだ。
- クライアントサイドクッキー。クッキーはクライアントについてのセッションデータをもち、サーバーがログインしたstatusと、リソースへのpermissions/accessesを決めるために使うことができるキーを含む。
ウェブサーバーはクライアントのリクエストメッセージを待ち、到着したらそれらを処理し、HTTPレスポンスメッセージでブラウザに返す。レスポンスはリクエストが成功したかどうかを示すHTTP Response status codeを含む。(たとえば”200 OK”は成功、もしリソースが見つからなければ”404 Not Found", もしユーザーがリソースを見ることが許可されていなければ”403 Forbidden”となる。)
GETリクエストが成功した応答のbodyは、リクエストされたリソースを含む。
HTMLページが返されると、Webブラウザにレンダーされる。ブラウザ処理の一部は他のリソースのリンクを発見する。(例えば、HTMLページは通常JSやCSSページを参照する)そして、別々のHTTPリクエストをそれらのファイルをダウンロードするために行う。
Static, dynamicどちらのウェブサイト(次のセッションで扱う)も、完全に同じ通信プロトコル、パターンを使う。
GET request/response example
リンクをクリックする、もしくはサイトで検索すれば簡単なGETリクエストを作ることができる。例えば”client server overview”という言葉の検索をMDNで実行すると送られるHTTPリクエストは、下記のテキストにとても似ている。(メッセージの一部はブラウザやセットアップに依存するので、同一ではない)
GET https://developer.mozilla.org/en-US/search?q=client+server+overview&topic=apps&topic=html&topic=css&topic=js&topic=api&topic=webdev HTTP/1.1
Host: developer.mozilla.org
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: https://developer.mozilla.org/en-US/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7
Accept-Language: en-US,en;q=0.8,es;q=0.6
Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; csrftoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT; dwf_section_edit=False; dwf_sg_task_completion=False; _gat=1; _ga=GA1.2.1688886003.1471911953; ffo=true
The request
リクエストの各行は情報を持つ。最初の部分はheaderと呼ばれ、リクエストについての有益な情報を含み、HTMLドキュメント(しかし、bodyにある実際のコンテンツとは違う)についての有益な情報を持つHTML headと似ている。
上記で述べたことに関する情報の大半は最初の2行が持つ。
- requestのタイプ (
GET
). - The target resource URL (
/en-US/search
). - The URL parameters (
q=client%2Bserver%2Boverview&topic=apps&topic=html&topic=css&topic=js&topic=api&topic=webdev
). - The target/host website (developer.mozilla.org)
- 1行目の最後の部分は特定のプロトコルバージョン(
HTTP/1.1
)を識別する短い文字列も含まれる。
最後の行はクライアントサイドのクッキーについての情報を含む。この場合、クッキーはセッションを管理するidを含む。 (Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; ...
).
残りの行は使用されたブラウザについての情報や、処理できる応答の種類が含まれている。例えば、
- My browser (
User-Agent
) is Mozilla Firefox (Mozilla/5.0
). - gzip圧縮情報を受け入れられる。 (
Accept-Encoding: gzip
). - 特定の文字 (
Accept-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7
) や言語(Accept-Language: de,en;q=0.7,en-us;q=0.3
)を受け入れる - The
Referer
lineはこのリソースへのリンクを含むWebページのアドレスを示す。 (i.e. リクエストがhttps://developer.mozilla.org/en-US/
から行われている)
HTTPリクエストはbodyを持つことができるが、この場合は空だ。
The response
このリクエストのレスポンスの最初の部分は下記のように表示される。ヘッダーは下記のような情報を含む。
- 1行目はレスポンスコード200 OKという、リクエストが成功したこと示す。
- レスポンスがtext/htmlでフォーマットされたということを確認できる。(Content-Type)
- UTF-8文字セットを使っているのが確認できる。(
Content-Type: text/html; charset=utf-8
). - ヘッドはどれくらいの大きさかを示す。(
Content-Length: 41823
).
bodyコンテンツをメッセージの最後で確認できる。これはリクエストに返された実際のHTMLを含んでいる。
HTTP/1.1 200 OK
Server: Apache
X-Backend-Server: developer1.webapp.scl3.mozilla.com
Vary: Accept,Cookie, Accept-Encoding
Content-Type: text/html; charset=utf-8
Date: Wed, 07 Sep 2016 00:11:31 GMT
Keep-Alive: timeout=5, max=999
Connection: Keep-Alive
X-Frame-Options: DENY
Allow: GET
X-Cache-Info: caching
Content-Length: 41823
<!DOCTYPE html>
<html lang="en-US" dir="ltr" class="redesign no-js" data-ffo-opensanslight=false data-ffo-opensans=false >
<head prefix="og: http://ogp.me/ns#">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<script>(function(d) { d.className = d.className.replace(/\bno-js/, ''); })(document.documentElement);</script>
レスポンスヘッダーの残りには、応答(いつつくられたかなど)、サーバー、ブラウザがどのようにこのページを処理するかを期待しているか(X-Frame-Options: DENY 行は、このページを他のサイトの<iframe>に埋め込まないようブラウザに支持する)に関する情報を含む。
POST request/response example
HTTP POSTはサーバーに保存される情報を含むフォームを送る時に作られる。
The request
下記のテキストはユーザーがサイトに新しいプロフィールを送信する時に作られるHTTPリクエストを示している。リクエストのフォーマットは先ほどのGETリクエストのサンプルとほぼ同じだが、1行目がこのリクエストをPOSTとして識別する。
POST https://developer.mozilla.org/en-US/profiles/hamishwillee/edit HTTP/1.1
Host: developer.mozilla.org
Connection: keep-alive
Content-Length: 432
Pragma: no-cache
Cache-Control: no-cache
Origin: https://developer.mozilla.org
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: https://developer.mozilla.org/en-US/profiles/hamishwillee/edit
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,es;q=0.6
Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; _gat=1; csrftoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT; dwf_section_edit=False; dwf_sg_task_completion=False; _ga=GA1.2.1688886003.1471911953; ffo=true
csrfmiddlewaretoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT&user-username=hamishwillee&user-fullname=Hamish+Willee&user-title=&user-organization=&user-location=Australia&user-locale=en-US&user-timezone=Australia%2FMelbourne&user-irc_nickname=&user-interests=&user-expertise=&user-twitter_url=&user-stackoverflow_url=&user-linkedin_url=&user-mozillians_url=&user-facebook_url=
主な違いはURLがparameterを持たないことだ。フォームからの情報はリクエストのbodyにエンコードされている。(例えば, 新規ユーザーのフルネームは: &user-fullname=Hamish+Willee
を使う。)
The response
リクエストからのレスポンスは下記のように表示される。”302 Found”コードのstatusは、postが成功したとブラウザに伝えていて、Location fieldにある特定のページをロードするというリクエストを2回目のHTTPリクエストが発行している。この情報は、それ以外はGETリクエストへのレスポンスと同様だ。
HTTP/1.1 302 FOUND
Server: Apache
X-Backend-Server: developer3.webapp.scl3.mozilla.com
Vary: Cookie
Vary: Accept-Encoding
Content-Type: text/html; charset=utf-8
Date: Wed, 07 Sep 2016 00:38:13 GMT
Location: https://developer.mozilla.org/en-US/profiles/hamishwillee
Keep-Alive: timeout=5, max=1000
Connection: Keep-Alive
X-Frame-Options: DENY
X-Cache-Info: not cacheable; request wasn't a GET or HEAD
Content-Length: 0
Static sites
staticサイトは特定のリソースがリクエストされるたびに、サーバーから同じハードコードされたコンテンツを返す。例えば、/static/myproduct1.html というページを持っている。同じページが全てのユーザーに返される。他の似たような製品をサイトに加える際は、別のページを加える必要がある。(myproduct2.htmlのような) これは非効率性のはじまりだ。ページの数が多い場合にはとても大変である。同じコードを全てのページで繰り返すことになる。さらに、もしページ構成を変えたい場合、全て個別に書き直す必要がある。
Note:
staticサイトは少ないページで、全てのユーザーに同じコンテンツを送りたい場合は素晴らしい。しかし、大きいページには管理に大変なコストがかかる。
どのように働くかを先ほどの構成ダイアグラムを見てみよう。
ユーザーがページをナビゲートしたい時、ブラウザはHTTP GETリクエストを特定のHTMLページのURLに送る。サーバーはファイルシステムからリクエストされたドキュメントを検索し、ドキュメントや”200 OK”といったHTTP Response status codeを含んだHTTPレスポンスを返す。
もしファイルがサーバー上に存在しなければ、サーバーが”404 Not Found”といった異なるstatusコードを返すこともあるかもしれず、ファイルは存在するが他の場所にリダイレクトされたなら、”301 Moved Permanently”と返すだろう。
staticサイトのサーバーはGETリクエストを処理するだけでいい。なぜならサーバーは修正可能なデータを一切保存しないからだ。HTTPリクエストデータ(URL parameterやCookieなど)に基づき、レスポンスも変更されない。
dynamicサイトはstatic fileの処理を同じように扱う。
Dynamic sites
ダイナミックHTTPリクエスト、レスポンスサイクルについて学ぶ。”事実を保つ”ために、コーチがHTMLフォームでチーム名とチームサイズを選択できるスポーツチームマネージャのウェブサイトのコンテクストを使う。
そして次のゲームの”ベストライナップ”の提案を返す。
ダイナミックサイトの構成はウェブアプリケーション、プレーヤー、チーム、コーチや彼らの関係性を持つデータベース、HTMLテンプレートによって作られる。
コーチがチーム名とプレイヤー番号をフォームで送信すると、下記の手順で処理が行われる。
- Webブラウザが、リソース(/best)のベースURLを使用して、サーバーへのHTTP GETリクエストをを作り、URL parameter(e.g.
/best?team=my_team_name&show=11
)またはURLパターン(e.g./best/my_team_name/11/)
の一部としてチームとプレーヤー番号をエンコードする。GET リクエストがリクエストがデータを取り寄せるだけ(修正はしない)なので使用される。 - ウェブサーバーはリクエストがダイナミックであることを検出し、処理のためにウェブアプリケーションに転送する。(ウェブサーバーは、構成で定義されたパターンマッチングルールに基づき、様々なURLを処理する方法を決める。)
- ウェブアプリケーションは、リクエストの意図がURL(/best/)に基づいて”ベストチームリスト”を取得するということを認識し、リクエストされた名前とプレーヤー番号をURLから見つけ出す。
ウェブアプリケーションはデータベース(どのプレーヤー”ベスト”かという定義をするために追加の”internal parameter”を使い、場合によってはクライアントサイドクッキーから、ログインしたコーチの身元を取得する)からリクエストされた情報を取得する。 - ウェブアプリケーションはデータ(データベースから)をHTMLテンプレート内部のプレースホルダに、動的にHTMLページを作る。
- ウェブアプリケーションは、HTTP statusコード200(“成功”)といっしょに Webブラウザ(ウェブサーバー経由で)に作成されたHTMLを返す。もし何かがHTMLのreturnを妨げると、ウェブアプリケーションは別のコードを返す。例えばチームが存在しないことを示す”404"だ。
- Webブラウザは返されたHTMLの処理を開始し、参照する他のCSS、JavaScriptファイルを取得するための、別のリクエストを送信する。
- ウェブサーバーはファイルシステムからstaticファイルをロードし、ブラウザに直接返す(正しいファイルの処理は設定ルールとURLパターンのマッチングに基づく。)。
データベースの記録を更新する処理は似たように扱われる。ただし、データベースの更新と同様、ブラウザからのHTTPリクエストがPOSTリクエストとしてエンコードする必要がある。
Doing other work
ウェブアプリケーションの仕事は、HTTPリクエストを受け取り、HTTPレスポンスを返すことだ。情報を取得、アップデートするためにデータベースと相互作用することが通常のタスクであるが、コードは同時に他のこともし、データベースとは一切相互作用しない。
ウェブアプリケーションが行う追加タスクの例として、サイトの登録確認のeメール送信がある。サイトはログや他の処理も行う。
Returning something other than HTML
サーバーサイドウェブサイトコードはレスポンスでHTMLスニペットやファイルを返す必要はない。代わりに他のタイプのファイル(text, PDF, CSV)やデータ(JSON, XML)を動的に作成し、返す。
Webブラウザへデータを返し、自身のコンテンツ(AJAX)を動的に更新出来るようにするという考えは、長い間存在していた。最近では、”シングルページアプリケーション”が人気だ。これはウェブサイト全体が、必要に応じて動的に更新される1つのHTMLで書かれている。このスタイルのアプリケーションを使って作成されたウェブサイトは、サーバーからWebブラウザに多くの計算コストをかける。そしてその結果、ウェブサイトの挙動はネイティブアプリにより似た見た目となる。(highly responsiveなど)
Web frameworks simplify server-side web programming
サイバーサイドウェブフレームワークは上記で書いた処理を扱うためのコードを簡単にする。
最も重要な処理の1つが異なるリソース/ページのURLを特定のハンドラー機能にマップする、簡単なメカニズムを提供することだ。
これは別々のリソースのそれぞれのタイプに関するコードの維持を簡単にする。メンテナンスに関しても利益がある。なぜならハンドラー機能を変えることなく、1箇所で特定の機能を配信するために、使用されるURLを変更することができるからだ。
例えば、下記のDjango(Python)コードが2つのURLパターンを2つのビュー機能にマップすることを考える。
最初のパターンは/bestのHTTPリクエストが、views moduleにあるindex()に渡されることを保証する。
“/best/junior”を持つリクエストは、代わりにjunior()view functionに渡される。
# file: best/urls.py
#
from django.conf.urls import url
from . import views
urlpatterns = [
# example: /best/
url(r'^$', views.index),
# example: /best/junior/
url(r'^junior/$', views.junior),
]
Note: url() function内の最初のparameterは少し奇妙だ(r’^junior/$’)。なぜなら”regular expressions”と呼ばれるパターンマッチングテクニックを使っているからだ。
ここでは、これらは(上記のハードコードされたvalueではなく)URLパターンを一致させ、それらをview functionのparameterとして使用することができるということが分かれば良い。
ウェブフレームワークはview functionがデータベースから情報を読み込むのを簡単にする役割もある。データの構成は、モジュール内で定義される。モデルは下層データベースに保存されているフィールドを定義するPython classだ。もし”team_type”フィールドを持つTeamという名前のモデルがあるなら、シンプルなクエリ構文を特定のタイプのチームを全て取り戻すことに使う。
下記の例では、正確に(大文字小文字を区別)team_typeが”junior”のチームを全て取得する。フォーマットは filed name(team_type)の後ろに二重のアンダースコア、使用するマッチのタイプ(この場合exact)だ。たくさんのマッチタイプがあり、それらをデイジーチェーンできる。また、返される結果の順序と数を制御することもできる。
#best/views.py
from django.shortcuts import render
from .models import Team
def junior(request):
list_teams = Team.objects.filter(team_type__exact="junior")
context = {'list': list_teams}
return render(request, 'best/index.html', context)
junior() function がjunior teamsのリストを取得した後、render() functionをコールし、オリジナルのHttpRequest, HTMLテンプレート、テンプレートに含まれる情報を定義した”context” objectをパスする。
render() functionはコンテキス、HTML template,トを使うHTMLを作成するのに便利なfunctionで、HttpResponse objectにそれを返す。
ウェブフレームワークは他にもたくさんのタスクを助けてくれる
【わからなかったこと】
【感想】