なぜフレームワークが必要なのか?
Web開発を支える基礎技術を身につける方法

Webアプリケーションの仕組み #2/3

PyCon JP 2018
に開催
2018年9月17日から18日にかけて、日本最大のPythonの祭典、PyCon JP 2018が開催されました。「ひろがるPython」をキャッチコピーに、日本だけでなく世界各地からPythonエンジニアたちが一堂に会し、様々な知見を共有します。プレゼンテーション「Webアプリケーションの仕組み」に登壇したのは、株式会社ビープラウド、IT Architectの清水川貴之氏。フレームワークが担っている役割を紐解きながら、Webアプリケーションの仕組みを学びます。講演資料はこちら

Webサーバーを作る

清水川貴之氏(以下、清水川):ここまでで、クライアント側の動作はわかったので、今度はサーバーを作ってみます。Pythonでsocketを開いて、HTTPリクエストを受け付けて、ブラウザにページを表示する、ということをやってみたいと思います。

ブラウザはデフォルトで80番ポートにアクセスをしてます。どういう意味かと言うと、さっきのexample.comであれば、ドメイン名の後ろに「:80」というのが暗黙で付くんですね。これが、ポート番号です。

HTTPのデフォルトのポート番号は80番なんですが、80番ポートはWell Known Portと呼ばれていて、管理者権限がないと使えません。よく知られているメールやDNSに使われているポートが1024番までにあるので、勝手に使われるのを避けるために、管理者権限がないと開くことができません。なので、今回は8000番ポートを使ってやってみたいと思います。

HTTPリクエストを受け取るコードを書いて、レスポンスするコードを書いて、ブラウザからアクセスするということをやっていきます。

コードを全部書いているとトーク時間内に終わらないので、socketを開くコードを用意しておきました。Webアプリケーションを書く人は、view関数というのを書いて、リクエストを受け取ってレスポンスを返す、ということをよくやると思います。それに倣ってview関数を用意してみました。

raw_requestを受け取ってprintをして、returnとして「HTTP/1.1 501」、「Sorry」を返す、というだけのview関数です。生のレスポンスなので、改行。CRLF、CRLF、と2回入れなければいけないですね。

メイン部分を見るとsocketで、AF_INETのSOCK_STREAMなのでTCP/IPで開いて、127.0.0.1のIPアドレスで、8000番ポートでバインドをして、listenをして、acceptすると、通信の待ち受け状態になります。

このプログラムに外からブラウザでアクセスが来たら、このaccept関数から返ってきて、コネクションオブジェクトとアドレスが取れるんですけれども、コネクションは最後に閉じなければいけないので、with connとしておくと閉じるのが省略できて楽ですね。

その中で生のリクエストを受け取る文字列を1個用意しておいて、4096バイトごとにレシーブして、用意しておいた文字列にくっつけて、ぐるぐる回すと。4096バイト未満だったら、向こうが送信終了しているから、ここでbreakしてwhileを抜けます。

先ほどのview関数に受け取ったデータ列を、今回はもう決め打ちでutf-8でdecodeして渡して、view関数から返ってきた文字列をconnのsendallで、ブラウザ側に返してあげる。encode(‘utf-8’)で逆にエンコードしてあげています。

実際に実行してみましょう。

あらかじめ用意しておいた「webapp0.py」を起動すると、なにも表示されないんですけれども、ローカルホスト8000番にアクセスをすると、「Sorry」という表示が出て、下(ブラウザのデバッガー)を見ると「501」というステータスがちゃんと返ってきています。

生のリクエストをprintするようにしていますので、「GET / HTTP/1.1」で、ブラウザがこういう情報を送ってきているというのがわかります。それから、これは想定外ですけれども、「GET /favicon.ico」というリクエストが来ていて、これについても「Sorry」を返しているはずです。

favicon.icoはブラウザがタブやタイトルバーに表示するアイコンです。サイトにアクセスすると、ブラウザが勝手にこれを取りにいくんですね。こういう動きができていることがわかりました。

ということで、このコードですごく簡易的なWebサーバーが作れたということになります。

HTTPレスポンスを返す

次に、適当なHTTPレスポンスを返しましょう。今だと「Sorry」しか表示されないので、もうちょっといじります。

view関数を分けてあるので、view関数の中をいじるだけでいろいろと遊べます。randomをimportして、random.choiceで、resp_listの中から1個選んで返します。内容は404、402、501になっています。これも実行してみます。

「webapp0b.py」を実行してブラウザをリロードすると、「Okane Choudai」と表示されました。ステータスは402で、「Payment Required」というステータスです。HTTPのステータスにこういうのが本当にあるんですよ。もう一度リロードすると「No Page」、404ですね。もう1回リロードすると501、500系はサーバーエラーですね。501は、まだ実装していません、というステータスです。

では、さすがに遊んでいる場合じゃない、時間もなくなってきたぞ、ということで、HTMLを返します。

view関数の返り値は、ステータスコードを介してhtmlの本文を持つものにしました。実際に手で打っている時間がないので実行してしまいます。そうすると「Hello World!」という大きい文字で表示されました。ソースを見ると、今送ったHTMLの内容がちゃんと返ってきています。

view関数を作ったのはいいんだけれど、どのURLにアクセスしても今、同じなんですね。「/hoge」とやっても「Hello World!」と返ってくるので。ふつうWebサーバーでURLを変えたら違うものをどんどん返したいですよね。

ということで、HTTPリクエストのパスを見て、「/」以外は404を返すようにしましょう、ということを実装します。

「ヘッダー部分と本文の間は1行空く」というルールがあるので、それでもってsplitをして、ヘッダー部分と本文に分けてprintしています。

ヘッダーは1行ごとに意味があるので、splitlinesで文字列を1行ごとに分割をして。1行目については「GETの」、「どのpath」の、「HTTP /1.0なのか1.1なのかというversion」に分かれますので、3つに分割をしています。

もしpathが「/」だったら、「Hello World!」。それ以外だったら404 NOT FOUNDを返して、NO PAGEというデータを返すというコードが、このwebapp2.pyです。

実際に動かしてみると……「/hoge」なのでNO PAGEになりました。「hoge」を取って「/」だったら、「Hello 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ファイルへのリンク、画像へのリンクを含めるのが必要になります。

と言っても、PATH_INFOが「/」だったら返すHTMLに、headタグを書いて、「link href=“/static/style.css”」を見る指示を書いてあげるだけ。bodyには、「img 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です、という作りになっています。

じゃあ、これを実際に実行してみましょう。「$ python webapp3.py」、でwebapp3でリロードをすると……本来的にはCSSが適用されて、「Hello 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を自動判定するコードをちょっと入れてあります。Pythonは便利なので、「from mimetypes import 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を受け取ります。

環境変数は辞書です。リクエストオブジェクトも辞書なので、そのまま使っちゃいます。dispatchをして、viewから返ってきたstatus, headers, bodyがあるので、make_responseに渡してましたけど、代わりにstart_responseにstatusとheadersを渡します。bodyは最後にreturn文で返します。

こういうwsgiappを作りました。これで、今作っていたアプリケーションが実際にWSGI経由ででGunicornで起動します。

pip install gunicornでインストールをして、 gunicornコマンドにwebapp5:wsgiappを使ってね、と引数指定して起動します。実際にブラウザでリロードをすると、「Hello 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とかを指定するとその分ワーカープロセスが起動して、並列リクエストに対応することができます。

-w 6で実行すると、起動メッセージにBooting 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通信を見たら、なんかおかしいというのが見てわかって、すぐ解決できるなんてこともあります。場当たり的に「この変数をこうやったらうまく動いた」とかではなくて、仕組みに目を向けてみるのも重要じゃないかと思います。

参考文献を最後に載せてありますので、よければあとで参照してみてください。

今日話した内容は『Real World HTTP』とか、『ソフトウェアデザイン』2016年10月号とか、いろいろな本に書いてあるものです。今日は、Python版として、お伝えしました。

以上になります。

(会場拍手)

<続きは近日公開>

Occurred on , Published at

このログの連載記事

1 【PyCon JP 2018】仕組みから理解するWebアプリケーション Webフレームワークを使わずに原理を学ぶ
2 なぜフレームワークが必要なのか? Web開発を支える基礎技術を身につける方法
3 近日公開予定

スピーカー

関連タグ

人気ログ

ピックアップ

編集部のオススメ

ログミーTechをフォローして最新情報をチェックしよう!

人気ログ

ピックアップ

編集部のオススメ

そのイベント、ログしないなんて
もったいない!

苦労して企画や集客したイベント「その場限り」になっていませんか?