PyCon JP 2018: Webアプリケーションの仕組み
.
おまえ誰よ
活動:
Python関連書籍の翻訳と執筆
アジェンダ
ブラウザからのリクエストに応答する
cookieとセッション
データ保存
まとめ
最近のWebアプリ開発
上から下まで幅広い範囲の知識が必要
とは言うけれど.. やること多すぎ!
Webフレームワークの機能
View
質問1
質問2
1. だいたい把握してる -> 1人
2. すこし把握してる -> 5人
3. 全然わからない、雰囲気で使っている -> 50人以上
DjangoやFlaskの機能範囲
Django
Flask
機能多い
ドキュメント量で機能を計測
この膨大なドキュメント読んで把握とか難しい
ドキュメントの量 == 難易度 ?
把握できないから比較できない?
背景が分からないから便利な感じがしない?
ゼロから自作して追体験しよう
なにも無かった時代はどうやって作っていた?
フレームワークのない2000年頃
Web黎明期のシンプルな世界
2000年頃、Web黎明期には色々なかった
Webサイトの要件(現在)
動的ページ:
同時アクセス:
HTML, CSS, 画像と多数のリクエストをさばく必要がある
性能:
セキュリティーチェックや、ページ組み立てなど、やることが多い
可用性:
サイトが落ちてるとTwitterで話題にされる
Webサイトの要件(黎明期)
動的ページ:
同時アクセス:
同時1接続でもまあなんとかなる
性能:
遅くなるほど複雑なことをしない
可用性:
たまにサイト落ちてても立ち上げ直せばOK
やってみよう
データ保存
ブラウザの動作を観察
何が起きている?(SNSで最近話題のやつ)
内部で色々な通信が発生している
ブラウザのデバッガーで確認
Webサーバーの動作観察
telnetを使う
サーバーにアクセスして
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)
例として 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)
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サーバーを作る
1. socketを
TCP 8000番ポートで開く
4. ブラウザからアクセスする
Webサーバーを作る
socketを開く
webapp0.pyimport 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()
参考:
Webサーバーを作る
実行と確認
shell$ python3 webapp0.py
ブラウザで http://127.0.0.1:8000/
にアクセス
ブラウザに Sorry
と表示される
テキトウなHTTPレスポンス
ブラウザで http://127.0.0.1/
にアクセスするたびに、異なるエラー、異なる文字が表示される
webapp0b.pyimport 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.pydef 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.pydef 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.pyimport 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.pydef 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.pyimport 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で話題にされる
信頼性: セキュリティーの向上
性能: 遅いと文句言われる
ライブラリに任せよう
プロセスが死んでも生き返る
こういった機能を自分で実装せずに済む
Gunicornから自作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
より高速で堅牢なサービス提供
高速な静的ファイル配信
省メモリ
etc..
ここからcookieとsessionの話
cookie
HTTPレスポンスヘッダー (http)Set-Cookie: SID=31d4d96e407aad42
Set-Cookie: name=清水川
HTTPリクエストヘッダー(http)Cookie: SID=31d4d96e407aad42
Cookie: name=清水川
session
sessionは、特定ユーザーの情報を決められた期間だけ保存しておく入れ物
セッションデータをcookieに保存
HTTPレスポンスヘッダーでsessionを持たせる(HTTP)Cookie: session=V2Vi44Ki44OX44Oq44Kx44O844K344On44Oz44Gu5LuV57WE44G/==\n
データはユーザーに閲覧されてしまうし、書き換えられる
セッションデータをWebサーバーに保存
メモリ
ファイル:
/tmp/session-31d4d96e407aad42
等
気づくと /tmp
がDISK FULLになったり
セッションデータをKVS等に保存
名前は何でも良いけど、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.pyimport requests
c = {'sessionid': 'pfcrhqghmflwb......'}
res = requests.get('https://connpass.com/dashboard', cookies=c)
print(res.text)
データ保存の話
シンプルで分かりやすい
まとめ
今のWebアプリケーション開発に使われるフレームワークやスタックがなぜ必要とされているか、どのような利点があるのか
これからも開発を高速に進めるために
応用するのに必要
いつ学ぶ?
参考文献
HTTPプロトコルの基礎を学んで、いまの
Web技術がどのように作られていったのか、現在どのように利用されているのかを知り、時代の変化に左右されない、
Webの基礎技術を学ぶ。
動画
記事