generated at
PyCon JP 2018: Webアプリケーションの仕組み
寄り道しよう、仕組みの理解でさらに加速しよう
.
このスライド: https://goo.gl/3vBMzZ



おまえ誰よ
活動:
Pythonは2003年から使い始めた
Sphinxコミッター, PyCamp講師
Python関連書籍の翻訳と執筆


アジェンダ
Webサーバーの動作を観察
Webサーバーを作ってみよう
ブラウザからのリクエストに応答する
cookieとセッション
データ保存
まとめ


最近のWebアプリ開発
上から下まで幅広い範囲の知識が必要
Webの基礎技術を知らなくてもOK
TCP/IPや、HTTPなど
とは言うけれど.. やること多すぎ!

Webフレームワークの機能
一般的なWebフレームワークの機能の全体像
WSGIインターフェース
View
Model (O/R Mapper)

質問1
Pythonの Webフレームワーク、何を使ってる?
1. Django -> 50人
2. Flask -> 40人
3. その他 -> 5人 -> Bottle 4, Tornado 1

質問2
使っているWebフレームワークの機能、把握してる?
1. だいたい把握してる -> 1人
2. すこし把握してる -> 5人
3. 全然わからない、雰囲気で使っている -> 50人以上

DjangoやFlaskの機能範囲
Django
Flask

機能多い
ドキュメント量で機能を計測
FlaskのドキュメントはPDFで346ページ
DjangoのドキュメントはPDFで1888ページ
この膨大なドキュメント読んで把握とか難しい
ドキュメントの量 == 難易度 ?
把握できないから比較できない?
背景が分からないから便利な感じがしない?
これだけの機能をもつWebフレームワークはなぜ生まれたのか

ゼロから自作して追体験しよう
なにも無かった時代はどうやって作っていた?
フレームワークのない2000年頃

Web黎明期のシンプルな世界
2000年頃、Web黎明期には色々なかった
2005年 Django登場
(1998年 Zope はあった)
HTML 4.01、CSS 1.0が使えるブラウザが登場しはじめた頃
猫がマウスを追いかけるためにJavaScriptを使う

Webサイトの要件(現在)
動的ページ:
Webフレームワークの利用が前提
同時アクセス:
HTML, CSS, 画像と多数のリクエストをさばく必要がある
性能:
セキュリティーチェックや、ページ組み立てなど、やることが多い
可用性:
サイトが落ちてるとTwitterで話題にされる

Webサイトの要件(黎明期)
動的ページ:
URLが実行プログラムと1:1 (CGI)
同時アクセス:
同時1接続でもまあなんとかなる
性能:
遅くなるほど複雑なことをしない
可用性:
たまにサイト落ちてても立ち上げ直せばOK

やってみよう
ブラウザの動作を観察しよう
Webサーバーの動作を観察しよう
Webサーバーを作ってみよう
ブラウザからのHTTPリクエストに応答する
データ保存

ブラウザの動作を観察
ブラウザからWebサーバーにアクセスしてサイトを表示するまで
何が起きている?(SNSで最近話題のやつ)
内部で色々な通信が発生している
ブラウザでサーバーにHTTPリクエストを送信するとHTTPレスポンスが返ってくる
ブラウザのデバッガーで確認

Webサーバーの動作観察
telnetを使う
サーバーにアクセスして
HTTPリクエストを送信すると
HTTPレスポンスが返ってくる
telnet(http)
$ telnet example.com 80 Trying 93.184.216.34... Connected to example.com. Escape character is '^]'. GET / HTTP/1.1 Host: example.com HTTP/1.1 200 OK Cache-Control: max-age=604800 Content-Type: text/html; charset=UTF-8 Date: Sun, 09 Sep 2018 05:56:41 GMT Etag: "1541025663+gzip+ident" Expires: Sun, 16 Sep 2018 05:56:41 GMT Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT Server: ECS (sjc/4E8D) Vary: Accept-Encoding X-Cache: HIT Content-Length: 1270 <!doctype html> <html> <head> <title>Example Domain</title> ...

Webサーバーの動作観察
Pythonでサイトアクセス (1/2)
Pythonの urllib.request.urlopen を使う
例として http://example.com にアクセス
open-example-com (python)
$ python3 Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 26 2018, 19:50:54) [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> from urllib.request import urlopen >>> uo = urlopen('http://example.com') >>> uo.status 200 >>> uo.headers.items() [('Cache-Control', 'max-age=604800'), ('Content-Type', 'text/html; charset=UTF-8'), ('Date', 'Sun, 09 Sep 2018 05:59:50 GMT'), ('Etag', '"1541025663+ident"'), ('Expires', 'Sun, 16 Sep 2018 05:59:50 GMT'), ('Last-Modified', 'Fri, 09 Aug 2013 23:54:35 GMT'), ('Server', 'ECS (oxr/8313)'), ('Vary', 'Accept-Encoding'), ('X-Cache', 'HIT'), ('Content-Length', '1270'), ('Connection', 'close')]
HTTPステータスが200 (OK)、 Content-Type text/html; charset=UTF-8 なのが分かる

Webサーバーの動作観察
Pythonでサイトアクセス (2/2)
example.comのHTTPレスポンスボディを確認
open-example-com (python)
>>> print(uo.read().decode('utf-8')) <!doctype html> <html> <head> <title>Example Domain</title> <meta charset="utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style type="text/css"> ...
HTMLが返ってきていることが分かる

Webサーバーを作る
Pythonでsocketを開いてHTTPリクエストを受け付け
1. socketをTCP 8000番ポートで開く
Well Known Port (1~1024)は管理者権限が必要
2. HTTPリクエストを受け取るコードを書く
3. HTTPレスポンスを返すコードを書く
4. ブラウザからアクセスする


Webサーバーを作る
socketを開く
webapp0.py
import socket def view(raw_request): print(raw_request) return 'HTTP/1.1 501\r\n\r\nSorry\n' def main(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('127.0.0.1', 8000)) s.listen() while True: conn, addr = s.accept() with conn: raw_request = b'' while True: chunk = conn.recv(4096) raw_request += chunk if len(chunk) < 4096: break raw_response = view(raw_request.decode('utf-8')) conn.sendall(raw_response.encode('utf-8')) if __name__ == '__main__': main()
参考:
コード解説は socketのbind を参照
\r\n\r\n は、ヘッダーとボディを分ける HTTPレスポンスのルール

Webサーバーを作る
実行と確認
shell
$ python3 webapp0.py
ブラウザで http://127.0.0.1:8000/ にアクセス
ブラウザに Sorry と表示される

テキトウなHTTPレスポンス
ブラウザで http://127.0.0.1/ にアクセスするたびに、異なるエラー、異なる文字が表示される
webapp0b.py
import random def view(raw_request): print(raw_request) resp_list = [ 'HTTP/1.1 404 Not Found\r\n\r\nNo Page\n', 'HTTP/1.1 402 Payment Required\r\n\r\nOkane Choudai\n', 'HTTP/1.1 501 Not Implemented\r\n\r\nMada Dayo\n', ] resp = random.choice(resp_list) return resp # 省略


HTMLを返す
webapp1.py
def view(raw_request): print(raw_request) resp = '''HTTP/1.1 200 OK <html><body> <h1>Hello World!</h1> </body></html> ''' return resp


HTTPリクエストのパスを見て / 以外は404を返す
webapp2.py
def view(raw_request): header, body = raw_request.split('\r\n\r\n', 1) # 最初のCRLFで分割 print(header) print(body) headers = header.splitlines() # リクエストラインを分割 method, path, version = headers[0].split(' ', 2) if path == '/': resp = dedent('''\ HTTP/1.1 200 OK <html><body> <h1>Hello World!</h1> </body></html> ''') else: resp = dedent('''\ HTTP/1.1 404 NOT FOUND NO PAGE ''') return resp

リクエスト/レスポンス処理をちょっと整理
requestの解析とresponseの組立てを関数化
webapp3.py
import socket def make_request(raw_request): if isinstance(raw_request, bytes): raw_request = raw_request.decode('utf-8') print(raw_request) header, body = raw_request.split('\r\n\r\n', 1) headers = header.splitlines() method, path, proto = headers[0].split(' ', 2) request = { 'headers': headers[1:], 'body': body, 'REQUEST_METHOD': method, 'PATH_INFO': path, 'SERVER_PROTOCOL': proto, } return request def make_response(status, headers, body): status_line = ('HTTP/1.1 ' + status).encode('utf-8') hl = [] for k, v in headers: h = '%s: %s' % (k, v) hl.append(h) header = ('\r\n'.join(hl)).encode('utf-8') if isinstance(body, str): body = body.encode('utf-8') raw_response = status_line + b'\r\n' + header + b'\r\n\r\n' + body print(raw_response) return raw_response def view(request): if request['PATH_INFO'] == '/': body = ''' <html><body> <h1>Hello World!</h1> </body></html> ''' resp = ('200 OK', [('Content-Type', 'text/html')], body) else: resp = ('404 NOT FOUND', [('Content-Type', 'text/plain')], 'NO PAGE') return resp # (status str, headers tuple, content) def app(raw_request): request = make_request(raw_request) status, headers, body = view(request) if isinstance(body, str): body = body.encode('utf-8') raw_response = make_response(status, headers, body) return raw_response def main(): ... # raw_response = view(raw_request.decode('utf-8')) raw_response = app(raw_request) # conn.sendall(raw_response.encode('utf-8')) conn.sendall(raw_response) if __name__ == '__main__': main()


HTMLとCSSと画像を表示する
HTMLにcssファイルと画像ファイルへのリンクを追加
URLとのマッピング
ファイルアクセス
webapp4.py
def view(request): if request['PATH_INFO'] == '/': body = ''' <html> <head> <link href="/static/style.css" rel="stylesheet"> </head> <body> <h1>Hello World!</h1> <img src="/static/image.jpg"> </body></html> ''' resp = ('200 OK', [('Content-Type', 'text/html')], body) elif request['PATH_INFO'] == '/static/style.css': headers = [ ('Content-Type', 'text/css'), ] resp = ('200 OK', headers, open('static/style.css', 'rb').read()) elif request['PATH_INFO'] == '/static/image.jpg': headers = [ ('Content-Type', 'image/jpg'), ] resp = ('200 OK', headers, open('static/image.jpg', 'rb').read()) else: resp = ('404 NOT FOUND', [('Content-Type', 'text/plain')], 'NO PAGE') return resp

URLのパスでview関数を分ける
webapp5.py
import os from mimetypes import guess_type ... def index_view(request): body = ''' <html> <head> <link href="/static/style.css" rel="stylesheet"> </head> <body> <h1>Hello World!</h1> <img src="/static/image.jpg"> </body></html> ''' return ('200 OK', [], body) def file_view(request): path = request['PATH_INFO'] path = path.lstrip('/') # remove first / if not os.path.isfile(path): return notfound_view(request) ct, _ = guess_type(path) if ct is None: ct = 'application/octet-stream' headers = [ ('Content-Type', ct), ] return ('200 OK', headers, open(path, 'rb').read()) def notfound_view(request): return ('404 NOT FOUND', [], 'NO PAGE') patterns = { '/static/': file_view, '/': index_view, } def dispatch(request): path_info = request['PATH_INFO'] for path, view in patterns.items(): if path_info.startswith(path): return view return notfound_view def app(raw_request): request = make_request(raw_request) view = dispatch(request) # 追加 status, headers, body = view(request) if isinstance(body, str): body = body.encode('utf-8') raw_response = make_response(status, headers, body) return raw_response


現在の要件を満たす
同時アクセス: HTMLだけでなくCSSや画像も表示するので多重アクセスできないとページ表示が重い
可用性: サイトが落ちてるとTwitterで話題にされる
信頼性: セキュリティーの向上
性能: 遅いと文句言われる
性能のよいCloudのサービスを使う(例: Amazon Aurora)

ライブラリに任せよう
Gunicorn (あるいはuWSGI)にまかせよう
親プロセスがHTTPリクエストを受け付け、ワーカープロセスに委譲
プロセスが死んでも生き返る
親プロセスがモニタープロセスとしてワーカープロセスを起動、監視
こういった機能を自分で実装せずに済む

Gunicornから自作Webアプリを起動する
GunicornはWSGIプロトコルに対応したWebアプリケーションサーバー
自作WebアプリもWSGI準拠にすればGunicornから起動できる
webapp5wsgi.py
... def wsgiapp(environ, start_response): request = environ view = dispatch(request) status, headers, body = view(request) if isinstance(body, str): body = body.encode('utf-8') start_response(status, headers) return [body] ...
起動: gunicorn -w 2 webapp5wsgi:application

より高速で堅牢なサービス提供
Gunicornよりも上位の処理を専用のミドルウェアに任せる
高速な静的ファイル配信
省メモリ
Webサーバーを多重化してアクセスを振り分ける
死活監視して、応答のないサーバーには送信しない
etc..

ここからcookieとsessionの話

cookie
cookieWebサーバーから渡される、複数のkey,valueのペア
Webサーバーが、ブラウザに覚えて置いて欲しいkey,valueをHTTPレスポンスで送ってくる
HTTPレスポンスヘッダー (http)
Set-Cookie: SID=31d4d96e407aad42 Set-Cookie: name=清水川
ブラウザは、覚えているcookieWebサーバーに送信する
HTTPリクエストヘッダー(http)
Cookie: SID=31d4d96e407aad42 Cookie: name=清水川
ブラウザcookieを別のドメインに送ってしまうとまずい
情報漏洩の原因になったり、行動トラッキング嗜好分析など、色々なことに使えてしまう
ブラウザのデバッガーで見てみよう

session
sessionは、特定ユーザーの情報を決められた期間だけ保存しておく入れ物
ECサイトで、買い物かごの中身を入れておくといった、一時的なデータの保存にも使われる
セッションデータの実体をどこに保存するかはサイトによって異なる
Webアプリケーションを実行してるサーバーの、メモリ、ファイル

セッションデータをcookieに保存
サーバー側でセッションデータを保持しなくてもよいので、サーバー提供者は楽
HTTPレスポンスヘッダーでsessionを持たせる(HTTP)
Cookie: session=V2Vi44Ki44OX44Oq44Kx44O844K344On44Oz44Gu5LuV57WE44G/==\n
データはユーザーに閲覧されてしまうし、書き換えられる
signed cookieを使っていればユーザーによる改竄は防げる
cookieには最大4kbしかデータを持てない

セッションデータをWebサーバーに保存
メモリ
ファイル:
/tmp/session-31d4d96e407aad42
気づくと /tmp がDISK FULLになったり
Webサーバーを多重化した場合、サーバーごとに保存されてしまう
ブラウザでアクセスするたびに、異なるセッションデータを参照してしまう

セッションデータをKVS等に保存
KVSデータベースに保存、IDを振って参照する
セッションデータを参照するIDをtokenに変換してcookieに持たせておく
名前は何でも良いけど、Djangoのデフォルトでは sessionid が使われる
HTTPヘッダー
HTTPレスポンスヘッダーでsessionidを持たせる(http)
Set-Cookie: sessionid=31d4d96e407aad42439850e9df4354
HTTPリクエストヘッダーでsessionidを伝える(http)
Cookie: sessionid=31d4d96e407aad42439850e9df4354
session tokenを複製すると別ブラウザでもログイン状態になれる

session tokenを複製してアクセス
Pythonでもcookieに複製したsessionidを入れてサーバーアクセスすれば、ログイン状態になれる
clone-session.py
import requests c = {'sessionid': 'pfcrhqghmflwb......'} res = requests.get('https://connpass.com/dashboard', cookies=c) print(res.text)

データ保存の話
WebサーバーJSONや、pickleshelve 等のファイルで保存
シンプルで分かりやすい
性能が悪く、同時アクセスに弱いし、Webサーバーを多重化したときの共有に困る
データベースサーバーを用意して、SQLを使ってデータを保存、参照する

まとめ
今のWebアプリケーション開発に使われるフレームワークやスタックがなぜ必要とされているか、どのような利点があるのか


これからも開発を高速に進めるために
Webの基礎技術を学ぼう
Webフレームワークやツールに左右されない
応用するのに必要
Web以外でも、低レイヤー仕組みの理解は重要
いつ学ぶ?
疑問をもったら寄り道しよう
場当たり的な対処に時間を使うより、仕組みの理解をしよう

参考文献
HTTPプロトコルの基礎を学んで、いまのWeb技術がどのように作られていったのか、現在どのように利用されているのかを知り、時代の変化に左右されない、Webの基礎技術を学ぶ。
Software Design 2016年10月号 Webサーバはなぜ動くのか?
WebサーバーブラウザWebアプリケーションの役割、HTTP通信の中身、Webアプリケーションの基礎技術について紹介
見開きで1つの話題。イラストで分かりやすい説明。Webの全体像から、HTTPでやりとりする仕組み、さまざまなデータ形式Webアプリケーション開発などで必要なWebの基礎技術が紹介されている。
見開きで1つの話題。イラストで分かりやすい説明。サーバーの種類、サーバー間をつなぐネットワーク技術サーバー仮想化障害対策負荷分散セキュリティなど、サーバーに関連する話題を広く浅く紹介。

動画
記事
2018-10-06 08:56:07: shimizukawaわいわい
2018-10-06 08:56:07: shimizukawa わいわい
2018-09-17 20:43:39: shimizukawa 初ホッテントリわーい