HTTPにはいくつかのメソッドがありますが、 実際にアプリケーションプログラマが意識しなければならないのは 実質GETとPOSTのみです。
ここまで対象としてきたのはGETメソッドだけでしたが、 今回はPOSTを扱います。POSTメソッドは、主にHTMLのform要素から送信します。 今回は実験用に以下のHTMLを用意しました(form.html)。
1: <html> 2: <head> 3: <title>テストフォーム</title> 4: </head> 5: <body> 6: <form action="http://localhost:8001/PostTest.cgi" method="post"> 7: テキストボックス:<input type="text" name="text_name"/><br/> 8: パスワード:<input type="password" name="password_name"/><br/> 9: テキストエリア:<br/> 10: <textarea name="textarea_name" rows="4" cols="40"> 11: </textarea> 12: <table border="1"> 13: <tr> 14: <td> 15: ラジオボタン:<br/> 16: <input type="radio" name="radio_name" value="radio1">1</input> 17: <input type="radio" name="radio_name" value="radio2">2</input> 18: <input type="radio" name="radio_name" value="radio3">3</input> 19: <input type="radio" name="radio_name" value="radio4">4</input> 20: </td> 21: </tr> 22: <tr> 23: <td> 24: チェックボックス:<br/> 25: <input type="checkbox" name="check_name" value="check1">1</input> 26: <input type="checkbox" name="check_name" value="check2">2</input> 27: <input type="checkbox" name="check_name" value="check3">3</input> 28: <input type="checkbox" name="check_name" value="check4">4</input> 29: </td> 30: </tr> 31: </table> 32: <input type="hidden" name="hidden_name" value="hidden_value日本語"/> 33: <input type="file" name="file_name"/><br/> 34: <input type="submit" name="submit_name" value="送るよ!"/> 35: </form> 36: </body> 37: </html>
これをブラウザで表示すると、以下のようになります。
HTMLの「フォーム」という要素の名前といい、 ひとつのform要素の中にたくさんのinput要素を入れて submitボタンでまとめて送信するという構造といい、 また、それを受けるHTTPのメソッドの名前がPOSTであることといい、 この機構が「住所氏名とか、 アンケートフォームのようなものを埋めてもらって送信する」 用途に向けて設計されていることがわかります。
今時のWebアプリケーションでは、画面にボタンがたくさん並んでいたり等、 「フォームを全部埋めてsubmit」 ではない構造にすることもよくあると思いますが、 Webの当初の利用法を考えればPOSTがこのようになっているのも 仕方がないように思います。
さて、例によって、テスト用のサーバにブラウザからPOSTを送信し、 実際に何が送られているかを見てみます。
今まではここで作ったServer01.javaを 使用していましたが、このプログラムは 「クライアントが終了のマークとして0を送ってくる」ということを 想定していてブラウザから使うには変ですし、POSTでは、 バイナリデータ送付時にブラウザが本当に0を送ってしまう可能性が あるので今回作り直しました(Server01.javaから機能を削っただけですが)。
TestServer.java
import java.io.*; import java.net.*; public class TestServer { public static void main(String[] argv) throws Exception { try (ServerSocket server = new ServerSocket(8001); FileOutputStream fos = new FileOutputStream("server_recv.txt")) { System.out.println("クライアントからの接続を待ちます。"); Socket socket = server.accept(); System.out.println("クライアント接続。"); int ch; // クライアントから受け取った内容をserver_recv.txtに出力 InputStream input = socket.getInputStream(); while ((ch = input.read()) != -1) { fos.write(ch); } socket.close(); } catch (Exception ex) { ex.printStackTrace(); } } }
TestServer.javaを起動後、 form.htmlをダブルクリックしてブラウザを立ち上げ、 以下のように値を入れて「送信」ボタンを押します。 見えないですが、パスワード欄には、「abc abc」と入力しています。
form.htmlの6行目のform要素のaction属性にて、 localhost:8001を指定していますので(PostTest.cgiという ファイル名を指定していますが、今回これには意味はありません)、 これにより、8001番ポートで待ち構えているTestServer.javaに POSTリクエストが送られることになります。
サーバに届いたデータの内容は以下です(11行目は長すぎるので 改行を入れていますが、実際には1行です)。
1: POST /PostTest.cgi HTTP/1.1 2: Host: localhost:8001 3: User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:24.0) Gecko/20100101 Firefox/24.0 4: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5: Accept-Language: ja,en-us;q=0.7,en;q=0.3 6: Accept-Encoding: gzip, deflate 7: Connection: keep-alive 8: Content-Type: application/x-www-form-urlencoded 9: Content-Length: 276 10: 11: text_name=abc+%93%FA%96%7B%8C%EA&password_name=abc+abc&textarea_name=abc+%93%FA%96%7B%8C%EA%0D%0A(行継続) def+%93%FA%96%7B%8C%EA%0D%0A&radio_name=radio2&check_name=check2&check_name=check3&hidden_name=hidden_(行継続) value%93%FA%96%7B%8C%EA&file_name=post.html&submit_name=%91%97%82%E9%82%E6%21
この実験結果から、以下のことがわかります。
上にも書きましたが、POSTメソッドは 「アンケートフォームのようなものを埋めてもらって送信する」 という用途を想定しているようです。
特に業務アプリなどでは、1画面にたくさんのボタンが並んでおり、 それぞれに機能が異なる、というケースが多いでしょう。 そのような場合は、サーバ側でどのsubmitボタンが押されたのかを 区別しなければなりません。
考えられる方法は、以下の通りです。
上記のform.htmlで送信されたデータは、 &区切りで「名前=値」の形になっていました。 こんな形式は、考えてみれば(巨大かもしれない)ファイルの アップロードには不向きです。
ファイルをアップロードする際には、form要素にenctype属性を付け、 「enctype="multipart/form-data"」のように指定します。
その修正を加えたHTMLで、TestServer.javaに送信を行ったところ server_recv.txtは以下のようになりました。
1: POST /PostTest.cgi HTTP/1.1 2: Host: localhost:8001 3: User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:24.0) Gecko/20100101 Firefox/24.0 4: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5: Accept-Language: ja,en-us;q=0.7,en;q=0.3 6: Accept-Encoding: gzip, deflate 7: Connection: keep-alive 8: Content-Type: multipart/form-data; boundary=---------------------------164711197029998 9: Content-Length: 19426 10: 11: -----------------------------164711197029998 12: Content-Disposition: form-data; name="text_name" 13: 14: abc 日本語 15: -----------------------------164711197029998 16: Content-Disposition: form-data; name="password_name" 17: 18: abc abc 19: -----------------------------164711197029998 20: Content-Disposition: form-data; name="textarea_name" 21: 22: abc 日本語 23: def 日本語 24: 25: -----------------------------164711197029998 26: Content-Disposition: form-data; name="radio_name" 27: 28: radio2 29: -----------------------------164711197029998 30: Content-Disposition: form-data; name="check_name" 31: 32: check2 33: -----------------------------164711197029998 34: Content-Disposition: form-data; name="check_name" 35: 36: check3 37: -----------------------------164711197029998 38: Content-Disposition: form-data; name="hidden_name" 39: 40: hidden_value日本語 41: -----------------------------164711197029998 42: Content-Disposition: form-data; name="file_name"; filename="post.html" 43: Content-Type: text/html 44: 45: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> 46: <html> 47: <head> 48: <meta HTTP-EQUIV="Content-Type" CONTENT="text/html;charset=Shift_JIS"> 49: <link rev=MADE href="mailto:PXU00211@nifty.ne.jp"> 50: <link rel="STYLESHEET" TITLE="default" TYPE="text/css" href="webserver.css"> 51: <script type="text/javascript" src="./syntaxhighlighter/scripts/shCore.js" charset="Shift_JIS"></script> 52: <script type="text/javascript" src="./syntaxhighlighter/scripts/shBrushJava.js"></script> 53: <link type="text/css" rel="stylesheet" href="./syntaxhighlighter/styles/shCore.css"/> 54: <link type="text/css" rel="stylesheet" href="./syntaxhighlighter/styles/shThemeDefault.css"/> 55: <script type="text/javascript"> 56: SyntaxHighlighter.all(); 57: </script> 58: <title>POSTメソッド</title> 59: </head> 60: <body> 61: <h1>POSTメソッド</h1> 62: <!--************************************************************--> 63: <h2>POST</h2> 64: <p> 65: HTTPにはいくつかのメソッドがありますが、 66: 実際にアプリケーションプログラマが意識しなければならないのは (後略)
8行目のContent-Typeリクエストヘッダに「multipart/form-data; boundary=---------------------------164711197029998」と指定されています。 boundary=の後ろに、「---…」に続いて謎の数字が入っていますが、 この文字列が、POSTで送られる各種の値の境界線(boundary)となります (厳密に言うと「--」を加えるので微妙に異なります。後述)。 この境界線で区切られたそれぞれの範囲に、 Content-Dispositionヘッダで要素のname属性が、 その後改行を挟んだボディ部分に値が入っていることがわかります。
42行目以降が<input type="file">で送信された ファイルのデータです。 ここでは、post.html(今あなたが読んでいるこのHTMLファイルです)を 送信しています。 45行目以降で、HTMLファイルの中身がそのまま送られていることがわかると思います。 今回はHTMLファイルなので内容はテキストですが、バイナリファイルを送信すると、 この場所にバイナリデータがそのまま入ります。 なお、長すぎるので省略していますが、 ファイルのデータの末尾にも、他のデータと同じように境界線が入ります。
境界線の文字列は、送信するデータが何であれ、 そのデータ中には登場しない文字列であることが求められています。 境界線でデータの終わりを判定するのですから当たり前ですが、 最近では動画のような大きなバイナリデータを送信することもあるわけで、 そういう場合、事前に一度ファイルをスキャンして絶対に登場しない 境界線を選ぶのか、それとも長くてランダムな境界線を作って 登場しないことを祈るのか、現状のブラウザ等の実装がどうなっているのかは 私は知りません……
multipart/form-dataの仕様は RFC 2388 で定められています。
なお、ぱっと見では気付かないと思いますが、上記のリクエストにおいて Content-Typeリクエストヘッダには 「multipart/form-data; boundary=---------------------------164711197029998」 と書いてあり、実際のデータの境界線である 「-----------------------------164711197029998」に対し、 ハイフンが2個足りません。 これは、 RFC 2046 において、ふたつのハイフンの後にContent-Typeヘッダのboundary の値を付けるように定められているためです。
GETメソッドであれば、指定されたファイルを返せば、 静的ページのみ対応のWebサーバとして機能しますが、 POSTメソッドは、 何らかのプログラムで値を受け取らなければ意味がありません。
というわけで、次はいよいよWebアプリケーションを作ります。