PyCon JP 2018: Webアプリケーションの仕組み

寄り道 しよう、 仕組みの理解 でさらに加速しよう

.

https://gyazo.com/e317cddc08ed302633366fde592d0364

おまえ誰よ

活動:

Python は2003年から使い始めた

Sphinx コミッター, PyCamp 講師

Python関連書籍の翻訳と執筆

https://gyazo.com/0c7c457d5f57c1fb162d3f9bc7911d78

https://gyazo.com/5fe0b66a6b9804ea8c4a49091d333879

アジェンダ

最近の Webアプリケーション開発

Webサーバー の動作を観察

Webサーバー を作ってみよう

ブラウザからのリクエストに応答する

cookieとセッション

データ保存

まとめ

最近のWebアプリ開発

上から下まで幅広い範囲の知識が必要

Webフレームワーク を使えれば

Webの基礎技術 を知らなくてもOK

とは言うけれど.. やること多すぎ！

https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c

https://gyazo.com/286ddcbb0079cb5710f2ff6e28deeb1e

Webフレームワークの機能

一般的な Webフレームワーク の機能の全体像

WSGI インターフェース

View

https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c

質問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

https://gyazo.com/4994265e9ccbbe9e659f75ee5c534ea2

Flask

https://gyazo.com/47954898a618341b0d9dfbe25c6adae7

機能多い

ドキュメント量で機能を計測

Flaskのドキュメント はPDFで 346 ページ

Djangoのドキュメント はPDFで 1888 ページ

この膨大なドキュメント読んで把握とか難しい

ドキュメントの量 == 難易度 ?

把握できないから比較できない?

背景が分からないから便利な感じがしない?

これだけの機能をもつ Webフレームワーク はなぜ生まれたのか

ゼロから自作して追体験しよう

Webフレームワーク を使わずに Webアプリケーション を作るには?

なにも無かった時代はどうやって作っていた?

フレームワークのない2000年頃

Web黎明期のシンプルな世界

2000年頃、Web黎明期には色々なかった

2006年 Amazon AWS 登場

2005年 Django登場

2004年 さくらインターネット レンタルサーバー開始

インターネットプロバイダ が提供する CGI サーバー or 自宅サーバー

猫がマウスを追いかけるために JavaScript を使う

Webサイトの要件(現在)

動的ページ:

Webフレームワーク の利用が前提

同時アクセス:

HTML, CSS, 画像と多数のリクエストをさばく必要がある

性能:

セキュリティーチェックや、ページ組み立てなど、やることが多い

可用性:

サイトが落ちてるとTwitterで話題にされる

Webサイトの要件(黎明期)

動的ページ:

同時アクセス:

同時1接続でもまあなんとかなる

性能:

遅くなるほど複雑なことをしない

可用性:

たまにサイト落ちてても立ち上げ直せばOK

やってみよう

ブラウザ の動作を観察しよう

Webサーバー の動作を観察しよう

Webサーバー を作ってみよう

ブラウザ からの HTTPリクエスト に応答する

cookie と session

データ保存

ブラウザの動作を観察

ブラウザ から Webサーバー にアクセスしてサイトを表示するまで

何が起きている？（SNSで最近話題のやつ）

内部で色々な通信が発生している

ブラウザでサーバーに HTTPリクエスト を送信すると HTTPレスポンス が返ってくる

ブラウザのデバッガーで確認

Webサーバーの動作観察

telnetを使う

telnet で

サーバーにアクセスして

HTTPリクエスト を送信すると

HTTPレスポンス が返ってくる

code: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 にアクセス

code:open-example-com (python)

$ python3

Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 26 2018, 19:50:54)

Type "help", "copyright", "credits" or "license" for more information.

>> from urllib.request import urlopen

>> uo.status

200

>> uo.headers.items()

HTTPステータス が200 (OK)、 Content-Type が text/html; charset=UTF-8 なのが分かる

Webサーバーの動作観察

Pythonでサイトアクセス (2/2)

example.comの HTTPレスポンスボディ を確認

code: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番ポートで開く

2. HTTPリクエスト を受け取るコードを書く

3. HTTPレスポンス を返すコードを書く

4. ブラウザからアクセスする

Webサーバーを作る

socketを開く

code:webapp0.py

import socket

def view(raw_request):

print(raw_request)

return 'HTTP/1.1 501\r

\r

Sorry

'

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

\r

は、ヘッダーとボディを分ける HTTPレスポンス のルール

Webサーバーを作る

実行と確認

code:shell

$ python3 webapp0.py

ブラウザで http://127.0.0.1:8000/ にアクセス

ブラウザに Sorry と表示される

テキトウなHTTPレスポンス

ブラウザで http://127.0.0.1/ にアクセスするたびに、異なるエラー、異なる文字が表示される

code:webapp0b.py

import random

def view(raw_request):

print(raw_request)

resp_list = [

'HTTP/1.1 404 Not Found\r

\r

No Page

',

'HTTP/1.1 402 Payment Required\r

\r

Okane Choudai

',

'HTTP/1.1 501 Not Implemented\r

\r

Mada Dayo

',

]

resp = random.choice(resp_list)

return resp

# 省略

HTMLを返す

code: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を返す

code:webapp2.py

def view(raw_request):

header, body = raw_request.split('\r

\r

', 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の組立てを関数化

code: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

\r

', 1)

headers = header.splitlines()

method, path, proto = headers 0 .split(' ', 2)

request = {

'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

'.join(hl)).encode('utf-8')

if isinstance(body, str):

body = body.encode('utf-8')

raw_response = status_line + b'\r

' + header + b'\r

\r

' + body

print(raw_response)

return raw_response

def view(request):

if request 'PATH_INFO' == '/':

body = '''

<html><body>

<h1>Hello World!</h1>

</body></html>

'''

else:

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とのマッピング

ファイルアクセス

code: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>

'''

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:

return resp

URLのパスでview関数を分ける

code: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):

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で話題にされる

信頼性: セキュリティーの向上

セッションハイジャック対策 等されている Webフレームワーク を使う

性能: 遅いと文句言われる

ライブラリに任せよう

マルチプロセス で 並列処理 できる

親プロセスが HTTPリクエスト を受け付け、 ワーカープロセス に委譲

プロセスが死んでも生き返る

親プロセスが モニタープロセス として ワーカープロセス を起動、監視

こういった機能を自分で実装せずに済む

Gunicornから自作Webアプリを起動する

Gunicornは WSGI プロトコルに対応した Webアプリケーションサーバー

https://gyazo.com/0be06ef21674581439344d215a2efa70

自作Webアプリも WSGI 準拠にすればGunicornから起動できる

code: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 よりも上位の処理を専用の ミドルウェア に任せる

Nginx や Apache

高速な静的ファイル配信

省メモリ

Webサーバーを多重化 してアクセスを振り分ける

etc..

https://gyazo.com/286ddcbb0079cb5710f2ff6e28deeb1e

ここからcookieとsessionの話

cookie

cookie は Webサーバー から渡される、複数のkey,valueのペア

Webサーバー が、 ブラウザ に覚えて置いて欲しいkey,valueを HTTPレスポンス で送ってくる

code:HTTPレスポンスヘッダー (http)

Set-Cookie: SID=31d4d96e407aad42

Set-Cookie: name=清水川

ブラウザ は、覚えている cookie を Webサーバー に送信する

code:HTTPリクエストヘッダー(http)

Cookie: SID=31d4d96e407aad42

Cookie: name=清水川

ブラウザ が cookie を別のドメインに送ってしまうとまずい

ブラウザの デバッガー で見てみよう

session

セッションデータ の実体をどこに保存するかはサイトによって異なる

ブラウザ の cookie

セッションデータをcookieに保存

サーバー側で セッションデータ を保持しなくてもよいので、サーバー提供者は楽

code:HTTPレスポンスヘッダーでsessionを持たせる(HTTP)

Cookie: session=V2Vi44Ki44OX44Oq44Kx44O844K344On44Oz44Gu5LuV57WE44G/==



データはユーザーに閲覧されてしまうし、書き換えられる

signed cookie を使っていればユーザーによる改竄は防げる

cookie には最大4kbしかデータを持てない

セッションデータをWebサーバーに保存

メモリ

ファイル:

/tmp/session-31d4d96e407aad42 等

気づくと /tmp がDISK FULLになったり

ブラウザ でアクセスするたびに、異なる セッションデータ を参照してしまう

セッションデータをKVS等に保存

KVS や データベース に保存、IDを振って参照する

セッションデータ を参照するIDをtokenに変換して cookie に持たせておく

名前は何でも良いけど、Djangoのデフォルトでは sessionid が使われる

HTTPヘッダー

code:HTTPレスポンスヘッダーでsessionidを持たせる(http)

Set-Cookie: sessionid=31d4d96e407aad42439850e9df4354

code:HTTPリクエストヘッダーでsessionidを伝える(http)

Cookie: sessionid=31d4d96e407aad42439850e9df4354

session tokenを複製すると別ブラウザでもログイン状態になれる

session tokenを複製してアクセス

Pythonでもcookieに複製したsessionidを入れてサーバーアクセスすれば、ログイン状態になれる

code:clone-session.py

import requests

c = {'sessionid': 'pfcrhqghmflwb......'}

print(res.text)

セッションハイジャック の対策が必要

データ保存の話

割愛 -> Webサーバーのデータ保存

Webサーバー に JSON や、 pickle 、 shelve 等のファイルで保存

シンプルで分かりやすい

性能が悪く、同時アクセスに弱いし、 Webサーバーを多重化 したときの共有に困る

まとめ

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

https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c

これからも開発を高速に進めるために

Webの基礎技術 を学ぼう

Webフレームワーク やツールに左右されない

応用するのに必要

Web以外でも、 低レイヤー の 仕組みの理解 は重要

いつ学ぶ？

疑問をもったら 寄り道 しよう

場当たり的な対処に時間を使うより、 仕組みの理解 をしよう

参考文献

Webサーバー や ブラウザ 、 Webアプリケーション の役割、 HTTP 通信の中身、 Webアプリケーション の基礎技術について紹介

見開きで1つの話題。イラストで分かりやすい説明。 Webの全体像 から、 HTTP でやりとりする仕組み、さまざまな データ形式 、 Webアプリケーション開発 などで必要な Webの基礎技術 が紹介されている。

動画

https://www.youtube.com/watch?v=L7j2zgtpV9c