2018が開催されました。「ひろがるPython」をキャッチコピーに、日本だけでなく世界各地からPythonエンジニアたちが一堂に会し、様々な知見を共有します。Architectの清水川貴之氏。フレームワークが担っている役割を紐解きながら、Webアプリケーションの仕組みを学びます。Webフレームワークを使わずに原理を学ぶWebサーバーを作る清水川貴之氏(以下、清水川):ここまでで、クライアント側の動作はわかったので、今度はサーバーを作ってみます。Pythonでsocketを開いて、HTTPリクエストを受け付けて、ブラウザにページを表示する、ということをやってみたいと思います。ブラウザはデフォルトで80番ポートにアクセスをしてます。いう意味かと言うと、さっきのexample.comであれば、ドメイン名の後ろに「:80」というのが暗黙で付くんですね。これが、ポート番号です。Portと呼ばれていて、管理者権限がないと使えません。知られているメールやDNSに使われているポートが1024番までにあるので、勝手に使われるのを避けるために、管理者権限がないと開くことができません。今回は8000番ポートを使ってやってみたいと思います。HTTPリクエストを受け取るコードを書いて、レスポンスするコードを書いて、ブラウザからアクセスするということをやっていきます。コードを全部書いているとトーク時間内に終わらないので、socketを開くコードを用意しておきました。Webアプリケーションを書く人は、view関数というのを書いて、リクエストを受け取ってレスポンスを返す、ということをよくやると思います。それに倣ってview関数を用意してみました。501」、「Sorry」を返す、というだけのview関数です。生のレスポンスなので、改行。CRLF、CRLF、と2回入れなければいけないですね。メイン部分を見るとsocketで、AF_INETのSOCK_STREAMなのでTCP/IPで開いて、127.0.0.1のIPアドレスで、8000番ポートでバインドをして、listenをして、acceptすると、通信の待ち受け状態になります。connとしておくと閉じるのが省略できて楽ですね。その中で生のリクエストを受け取る文字列を1個用意しておいて、4096バイトごとにレシーブして、用意しておいた文字列にくっつけて、ぐるぐる回すと。4096バイト未満だったら、向こうが送信終了しているから、ここでbreakしてwhileを抜けます。先ほどのview関数に受け取ったデータ列を、今回はもう決め打ちでutf-8でdecodeして渡して、view関数から返ってきた文字列をconnのsendallで、ブラウザ側に返してあげる。encode(‘utf-8’)で逆にエンコードしてあげています。実際に実行してみましょう。用意しておいた「webapp0.py」を起動すると、なにも表示されないんですけれども、ローカルホスト8000番にアクセスをすると、「Sorry」という表示が出て、下(ブラウザのデバッガー)を見ると「501」というステータスがちゃんと返ってきています。HTTP/1.1」で、ブラウザがこういう情報を送ってきているというのがわかります。/favicon.ico」というリクエストが来ていて、これについても「Sorry」を返しているはずです。favicon.icoはブラウザがタブやタイトルバーに表示するアイコンです。サイトにアクセスすると、ブラウザが勝手にこれを取りにいくんですね。いう動きができていることがわかりました。いうことで、このコードですごく簡易的なWebサーバーが作れたということになります。HTTPレスポンスを返す次に、適当なHTTPレスポンスを返しましょう。今だと「Sorry」しか表示されないので、もうちょっといじります。view関数を分けてあるので、view関数の中をいじるだけでいろいろと遊べます。randomをimportして、random.choiceで、resp_listの中から1個選んで返します。内容は404、402、501になっています。これも実行してみます。Choudai」と表示されました。Required」というステータスです。HTTPのステータスにこういうのが本当にあるんですよ。Page」、404ですね。1回リロードすると501、500系はサーバーエラーですね。501は、まだ実装していません、というステータスです。遊んでいる場合じゃない、時間もなくなってきたぞ、ということで、HTMLを返します。view関数の返り値は、ステータスコードを介してhtmlの本文を持つものにしました。実際に手で打っている時間がないので実行してしまいます。World!」という大きい文字で表示されました。ソースを見ると、今送ったHTMLの内容がちゃんと返ってきています。view関数を作ったのはいいんだけれど、どのURLにアクセスしても今、同じなんですね。World!」と返ってくるので。ふつうWebサーバーでURLを変えたら違うものをどんどん返したいですよね。いうことで、HTTPリクエストのパスを見て、「/」以外は404を返すようにしましょう、ということを実装します。「ヘッダー部分と本文の間は1行空く」というルールがあるので、それでもってsplitをして、ヘッダー部分と本文に分けてprintしています。ヘッダーは1行ごとに意味があるので、splitlinesで文字列を1行ごとに分割をして。/1.0なのか1.1なのかというversion」に分かれますので、3つに分割をしています。PAGEというデータを返すというコードが、このwebapp2.pyです。PAGEになりました。World!」がちゃんと表示される、という感じです。リクエスト/レスポンス処理を整理するどんどんいきます。リクエスト/レスポンス処理をちょっと整理します。ここまでコードを書いてきたんですが、実際にコードがどうなるかと言うと、view関数があって、リクエストラインを分割して、pathの分岐をやりました。メイン関数がこうだーっと長くて、リクエストやレスポンスを、生の文字列のままview関数に渡して。view関数のほうで本文とヘッダーをsplitで分割して、というように生々しいわけですね。生々しいことをやっていると日が暮れてしまうので、view関数はもうちょっと整理された情報が欲しいです。いうことで、整理します。コードの詳細は割愛しますが、生のリクエストテキストを受け取って、リクエストオブジェクトというものを作るので、make_requestという関数にまとめました。やっていることは分割して、method、path、protoを取って、というところまでは一緒です。ここで作るリクエストオブジェクトは実は単なる辞書です。headersとbodyと、REQUEST_METHOD、PATH_INFO、SERVER_PROTOCOL、というような辞書を作って返します。同じようにmake_responseは、返すステータス、ヘッダー、本文を受け取って、それでテキストを組み立てて、生のraw_responseを返してくれる、という便利関数です。するとview関数には、整形されたリクエストオブジェクトが届くので、requestのPATH_INFOを見て動作を切り替えます。「/」だったらこれを返す、そうでなかったら404処理をする。)でくくって、最初がステータスコード、その後ろがヘッダーのリストなのでタプルで、content-typeがtext/htmlで、というようなことを書いてあげて。複数あったら複数タプルを書けばいいですね。最後にbody、本文部分を返すようにしてあります。make_requestとmake_responseとviewを処理する部分ということで、appというアプリケーション的な関数を用意してみました。生リクエストを受け取ったらmake_requestを作って、status、headers、bodyに分割をして、make_responseをして返す。すると何が起きるかと言うと、最初にsocketからデータを受け取って、うにゃうにゃやっていたところのコードが、きれいになるわけですね。appでraw_requestを渡すとraw_responseが返ってくるから、raw_responseをsendallでブラウザに返しておしまい、というシンプルな。decodeやencodeというのはsocket処理のところから除外するということができました。コードを整理しただけですね。これでview関数が書きやすくなったんですが、やっとpathごとにいろいろとやりたいことができるようになってきたかな、というところです。HTMLとCSSと画像を表示する次はHTMLにcssでデザイン適用したり、画像を表示するというのをやってみたいと思います。画像を表示するためには、HTMLにcssファイルへのリンク、画像へのリンクを含めるのが必要になります。href=“/static/style.css”」を見る指示を書いてあげるだけ。src=」で「”/static/image.jpg”」を見る、とい指示を書くだけ。PATH_INFOが「”/static/style.css”」だったら、content-typeをtext/cssにして、「”/static/style.css”」を「’rb’」で開いて、それをbodyとしてタプルに突っ込む。「”/static/image.jpg”」だったら同じようにbodyに突っ込む、content-typeがimage/jpegです、という作りになっています。これを実際に実行してみましょう。World!」が赤くなって、image.jpgでネコの画像が表示される、というのをやりたかったんですが、ちょっと失敗しました。view関数の中に今、pathが「/」だったら、「style.css」だったら、「img」だったら、というようにいろいろ書きました。それぞれのファイル名に合わせて「これだったらimage/jpeg」というのも全部指定していたんですが、これだとちょっとやってられないので、この「pathごとに動作を変える」というview関数の行動について、「URLごとに違うview関数を用意する」というのをやってみたいと思います。実際にやってみたいと思います、と言っても、1つのview関数を複数に分けているだけですね。index_viewにして、indexページのものだけを扱う。file_viewにしてファイルを取り出す、ということだけをやる。ファイルを取り出すというところだけをやる部分に、guess_typeという、ファイルの拡張子からcontent-typeを自動判定するコードをちょっと入れてあります。guess_type」とやると、拡張子からファイルのmimetypeを自動的に判別できます。これでcontent-typeにimage/jpegやtext/cssを個別に書かなくてよくなりました。notfound_viewも用意しました。URLごとに切り替えるif文をいちいちコード内に書いていると大変なので、patternsという変数に入れて辞書にして、「static/」で来たらfile_viewを見る、「/」だったらindex_viewを見る、というようなコードにしてあります。dispatchは、今までrequest[‘PATH_INFO’]を見てif文で分岐していたコードを置き換えます。今書いたpatternsをitemsでぐるぐる回して、先頭一致したpathを見つけたらそのviewの関数オブジェクトをreturnします。なにもマッチしなかったらnotfound_viewを返します。appは、さっきのviewを呼び出すというところは変わってないですが、viewはdispatchから取れてくるものだよ、というのを1行書きました。さっきもうデモしてしまったわけですが、変更前と同じような動作がこれで動いてます。view関数が分かれて、patternでURLを切り分けて、という感じでだんだんとWebフレームワークっぽくなってきました。現在のWebの要件を満たすためにということで、あらためて現在の要件です。同時アクセスについては、今フレームワーク開発的なことをやりましたが、この延長でどこまで同時アクセスの性能が上げられるかは、けっこう難しいと思います。可用性のところで「サイトが落ちているとTwitterで話題にされる」とあります。これについてはここまで話した範囲外のところでいろいろと、Webサーバーを止めないように対策が必要になります。信頼性については、さっきリクエストのテキストをこうやって分割して......と実装しましたけれど、すごく危険です。なんとかインジェクションと言って、サーバーに変な文字列が送られてきたときに、サーバー側でそれをそのまま鵜吞みにして動かすと、セキュリティ上の問題が起きる、情報漏洩するということがあったりします。いうのを対策済みのWebフレームワークを使うといいのではないかと思います。性能については、このプログラムはPythonで全部書きましたが、どうやってもPythonコードだけでは性能的に厳しいので、いろんなサービスを活用します。Pythonだけで書いていくのは限界があるので、最終的には誰かがC言語等で作った高速なものを使う。今日は、たくさんコードを書いてきましたが、ライブラリに任せるとあっという間に実装が終わってしまいます。GunicornはWSGIに対応したWebアプリケーションサーバーなんですが、マルチプロセスで動作するので並行処理ができます。親プロセスがHTTPリクエストを受け取って、多重起動したワーカープロセスにどんどんリクエストを渡していきます。親プロセスで起動したワーカープロセスが死んだら、立ち上げ直してくれます。先ほどデモしている途中でプログラムが落ちたりしていましたが、Gunicornを使っていれば、落ちたらまた起動してくれます。このへんを自分で実装するのは結構大変だし、自分で書いたら性能が大抵良くないんですが、GunicornやuWSGIを使えば、自分で実装しなくて済みます。今、話を出したWSGIですが、GunicornはWSGIプロトコルに対応したWebアプリケーションサーバーで、こんな感じです。Webアプリケーションサーバーとして、Gunicorn、uWSGI、mod_wsgiをあげました。フレームワーク側としてDjango、Flask、Pyramidを例としてあげましたが、WSGIでインターフェースが共通化されています。どのアプリケーションサーバーと、どのWebフレームワークを組み合わせても、あいだの通信が共通化されているので、高性能なGunicornとFlaskをつなぐとか、多機能なuWSGIとDjangoをつなぐぞとか、シーンに応じて組み合わせを変えて使えます。Gunicronから自作Webアプリを起動する自分で作ったアプリもWSGIに準拠すればGunicornから起動できます。先ほどのコードは、appという関数の中で色々やっていましたが、WSGIに対応させるのに必要なことは、そんなに多くありません。ファイルを開いて、これはもとのappのコードですけども、このappのコードを書き換えます。複製をして、関数名をwsgiappにして、この関数は受け取る引数が決まっているので、environ(環境変数)とstart_responseを受け取ります。リクエストオブジェクトも辞書なので、そのまま使っちゃいます。bodyがあるので、make_responseに渡してましたけど、代わりにstart_responseにstatusとheadersを渡します。bodyは最後にreturn文で返します。いうwsgiappを作りました。これで、今作っていたアプリケーションが実際にWSGI経由ででGunicornで起動します。gunicornコマンドにwebapp5:wsgiappを使ってね、と引数指定して起動します。World!」とちゃんと表示されました。今wsgiapp関数を作りましたが、もともと実装していたappとか、いくつかの関数がいらなくなったので消します。appから呼んでいたmake_requestも使ってないので消します。make_responseも消します。サーバーを起動するmain関数部分も消します。残ったのはindex_viewとfile_viewとnotfound_viewとpatternとdispatchとwsgiappで、今日の前半のほうの話をしていた時のコードは全部いなくなってしまいました。いうことで、WSGIってこういうものだよということと、Webアプリケーションサーバーを作るって、こうやって下から積み上げて必要なことを組み合わせていくと、だんだんと多機能になってしまうということです。WSGIサーバを導入するメリットちなみにgunicornを起動するときのワーカー数を指定するオプションはデフォルト1だったと思うんですが、2とか4とか6とかを指定するとその分ワーカープロセスが起動して、並列リクエストに対応することができます。workerが6個出るわけです。自分でゼロから作っていると、HTTPのリクエストの処理だけでも結構大変なんですが、gunicornとかに任せるほうが全然早いし、便利だし、多機能です。上位にNginxやApacheを置く構成を最初のほうで、絵の中に登場していたんですが、そういう構成を実際のWebサービスでは使います。いうと、高速な静的ファイル配信とか省メモリであるとかが必要だからです。Pythonって結構メモリを食うんですね。静的ファイルを送るためにPythonを起動していると大変なので、NginxとかApacheに任せちゃう。スローリード攻撃というのがあって、レスポンスをのんびり読み取るクライアントがたくさんいると、それだけでワーカーが100個あっても全部使われてしまって、次のリクエストを受け取れないことになったりするので、そういうのをバツっと切ってくれる機能がNginxとかにあります。gunicornだけでOKではなくて、Nginxをフロントに置きましょう、という話になってきます。Webサーバーそのものを多重化して、1個サーバーが落ちても、ロードバランサーでサーバーのどれに送るかを死活監視して、選んでやってくれる、といった構成が必要になってきたりします。同じようにWebサーバーの下にあるcacheストレージ、sessionストレージ、RDBMSなんていうものも全部多重化をしましょうということになります。このへんはちょっと前までは各自オンプレのサーバー上でやっていたんですが、今だと、Cloud上でやっているのが多いんじゃないかなと思います。フレームワークの仕組みを理解する必要性ここからcookieとsessionの話をしたかったんですが、時間がなくなってしまったので、すみません続きはWebで。(会場笑)ここまでで、sessionとかcookieの話でもいいんですが、なにか気になることがあればご質問とかありますか......?最後に1分だけください。参加者:GunicornとuWSGIどちらがよいかという指針はありますか?清水川:正直に言うと、私はuWSGIを使ってなくて、ただいろいろと話を聞くと、WSGIに対応したアプリケーションサーバーとしてGunicorn、uWSGI、Waitress、Twisted、CherryPy、mod_wsgiとか、いろいろあるんですが、この中でもuWSGIはものすごく多機能と聞いています。これだけでGunicornと何を組み合わせてやっている部分を全部uWSGIでやったらすぐ済むとか、パフォーマンスが良いとか、いろいろとそういう話は聞いています。今日のデモでは、1歩ずつこれをやればこうなる、と実装して、次の問題を解決するために実装して、とやってきました。これを繰り返していった結果、フレームワークとしてできあがってきて、便利なように整備をしたらDjangoとかFlaskができあがってくるという流れが、どのフレームワークにも必ずあると思います。フレームワークは、基礎技術の上に成り立っているもので、便利に使うためのガワの部分です。ガワの使い方を理解すると、ものすごく早く作れるようになります。「HTTPってなんだっけ、わかんないな」とか、「cookieってよくわかんない」とやっていると、なかなか応用が効きません。Djangoのドキュメント、Flaskのドキュメントに書いていることが分からないな、と思ったときに基礎技術に寄り道をして、疑問に思った部分を自分でゼロから実装してみる、というのが良いのではないかと思います。学ぶタイミングは、疑問を持ったら1度目はメモだけしておいて、もう1回疑問に思ったらちょっと調べる、というような感じがいいんじゃないかと思います。開発をやっていると、「パラメータを少しずつ変えながら追い詰めていってみよう」という感じでやるよりも、基礎技術に目を向けて、ブラウザのデバッガーでHTTP通信を見たら、なんかおかしいというのが見てわかって、すぐ解決できるなんてこともあります。場当たり的に「この変数をこうやったらうまく動いた」とかではなくて、仕組みに目を向けてみるのも重要じゃないかと思います。参考文献を最後に載せてありますので、よければあとで参照してみてください。HTTP』とか、『ソフトウェアデザイン』2016年10月号とか、いろいろな本に書いてあるものです。今日は、Python版として、お伝えしました。JPに関する記事をまとめています。JPに関する新着記事が公開された際に、通知を受け取ることができます。