PyCon JP 2018: Webアプリケーションの仕組み
寄り道しよう、仕組みの理解でさらに加速しよう
.
このスライド: https://goo.gl/3vBMzZ
https://gyazo.com/e317cddc08ed302633366fde592d0364
おまえ誰よ
清水川 貴之
所属: BeProud / 一般社団法人PyCon JP 会計理事
活動:
Pythonは2003年から使い始めた
Sphinxコミッター, PyCamp講師
Python関連書籍の翻訳と執筆
https://gyazo.com/0c7c457d5f57c1fb162d3f9bc7911d78
https://gyazo.com/5fe0b66a6b9804ea8c4a49091d333879
アジェンダ
最近のWebアプリケーション開発
Webサーバーの動作を観察
Webサーバーを作ってみよう
ブラウザからのリクエストに応答する
cookieとセッション
データ保存
まとめ
最近のWebアプリ開発
Webアプリケーション開発
上から下まで幅広い範囲の知識が必要
Webアプリケーション開発に必要なスキル
Webフレームワークを使えれば
Webの基礎技術を知らなくてもOK
TCP/IPや、HTTPなど
とは言うけれど.. やること多すぎ!
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
https://gyazo.com/286ddcbb0079cb5710f2ff6e28deeb1e
Webフレームワークの機能
一般的なWebフレームワークの機能の全体像
WSGIインターフェース
URLディスパッチャー
Requestオブジェクト
View
テンプレートエンジン
Cookie
Session
Cache
Model (O/R Mapper)
Database Driver
Schema Migration
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
質問1
Pythonの Webフレームワーク、何を使ってる?
1. Django -> 50人
2. Flask -> 40人
3. その他 -> 5人 -> Bottle 4, Tornado 1
Pyramid, Tornado, Bottle, aiohttp, Zope, Twisted, TurboGears, CherryPy, ...
質問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年 さくらインターネット レンタルサーバー開始
(1998年 Zope はあった)
インターネットプロバイダが提供するCGIサーバー or 自宅サーバー
HTML 4.01、CSS 1.0が使えるブラウザが登場しはじめた頃
猫がマウスを追いかけるためにJavaScriptを使う
Webサイトの要件(現在)
動的ページ:
Webフレームワークの利用が前提
同時アクセス:
HTML, CSS, 画像と多数のリクエストをさばく必要がある
性能:
セキュリティーチェックや、ページ組み立てなど、やることが多い
可用性:
サイトが落ちてるとTwitterで話題にされる
Webサイトの要件(黎明期)
動的ページ:
URLが実行プログラムと1:1 (CGI)
同時アクセス:
同時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)
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レスポンスボディを確認
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番ポートで開く
Well Known Port (1~1024)は管理者権限が必要
2. HTTPリクエストを受け取るコードを書く
3. HTTPレスポンスを返すコードを書く
4. ブラウザからアクセスする
http://127.0.0.1:8000/
参考: 127.0.0.1 と 0.0.0.0 の違い
Webサーバーを作る
socketを開く
code: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サーバーを作る
実行と確認
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\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を返す
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\n\r\n', 1) # 最初のCRLFで分割
print(header)
print(body)
headers = header.splitlines()
# リクエストラインを分割
method, path, version = headers0.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\n\r\n', 1)
headers = header.splitlines()
method, path, proto = headers0.split(' ', 2)
request = {
'headers': headers1:,
'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とのマッピング
ファイルアクセス
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>
'''
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関数を分ける
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):
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や画像も表示するので多重アクセスできないとページ表示が重い
同時アクセスを受け付けるWebサーバーを作る
可用性: サイトが落ちてるとTwitterで話題にされる
Webサーバーを止めない対策
信頼性: セキュリティーの向上
セッションハイジャック対策 等されているWebフレームワークを使う
性能: 遅いと文句言われる
性能のよいCloudのサービスを使う(例: Amazon Aurora)
ライブラリに任せよう
Gunicorn (あるいはuWSGI)にまかせよう
Gunicornのアーキテクチャ
マルチプロセスで並列処理できる
親プロセスが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
sessionは、特定ユーザーの情報を決められた期間だけ保存しておく入れ物
ECサイトで、買い物かごの中身を入れておくといった、一時的なデータの保存にも使われる
セッションデータの実体をどこに保存するかはサイトによって異なる
ブラウザのcookie
Webアプリケーションを実行してるサーバーの、メモリ、ファイル
KVS (Redis, memcached, 等)
データベース
セッションデータをcookieに保存
サーバー側でセッションデータを保持しなくてもよいので、サーバー提供者は楽
code: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ヘッダー
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......'}
res = requests.get('https://connpass.com/dashboard', cookies=c)
print(res.text)
セッションハイジャック の対策が必要
データ保存の話
割愛 -> Webサーバーのデータ保存
WebサーバーにJSONや、pickle、 shelve 等のファイルで保存
シンプルで分かりやすい
性能が悪く、同時アクセスに弱いし、Webサーバーを多重化したときの共有に困る
データベースサーバーを用意して、SQLを使ってデータを保存、参照する
まとめ
今のWebアプリケーション開発に使われるフレームワークやスタックがなぜ必要とされているか、どのような利点があるのか
https://gyazo.com/736450ff7fcf51f087b1b51d05d6444c
これからも開発を高速に進めるために
Webの基礎技術を学ぼう
Webフレームワークやツールに左右されない
応用するのに必要
Web以外でも、低レイヤーの仕組みの理解は重要
いつ学ぶ?
疑問をもったら寄り道しよう
場当たり的な対処に時間を使うより、仕組みの理解をしよう
参考文献
Real World HTTP
HTTPプロトコルの基礎を学んで、いまのWeb技術がどのように作られていったのか、現在どのように利用されているのかを知り、時代の変化に左右されない、Webの基礎技術を学ぶ。
Software Design 2016年10月号 Webサーバはなぜ動くのか?
Webサーバーやブラウザ、Webアプリケーションの役割、HTTP通信の中身、Webアプリケーションの基礎技術について紹介
Web技術の基本
見開きで1つの話題。イラストで分かりやすい説明。Webの全体像から、HTTPでやりとりする仕組み、さまざまなデータ形式、Webアプリケーション開発などで必要なWebの基礎技術が紹介されている。
サーバーの基本
見開きで1つの話題。イラストで分かりやすい説明。サーバーの種類、サーバー間をつなぐネットワーク技術、サーバー仮想化、障害対策、負荷分散、セキュリティなど、サーバーに関連する話題を広く浅く紹介。
動画
https://www.youtube.com/watch?v=L7j2zgtpV9c
記事
【PyCon JP 2018】仕組みから理解するWebアプリケーション Webフレームワークを使わずに原理を学ぶ - ログミーTech(テック)
2018-10-06 08:56:07: shimizukawa.iconわいわい
なぜフレームワークが必要なのか? Web開発を支える基礎技術を身につける方法 - ログミーTech(テック)
2018-10-06 08:56:07: shimizukawa.icon わいわい
2018-09-17 20:43:39: shimizukawa.icon 初ホッテントリわーい
PyCon JP 2018