POSTメソッド

POST

HTTPにはいくつかのメソッドがありますが、 実際にアプリケーションプログラマが意識しなければならないのは 実質GETとPOSTのみです。

ここまで対象としてきたのはGETメソッドだけでしたが、 今回はPOSTを扱います。

HTMLのform要素

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で何が送られてくるのか

さて、例によって、テスト用のサーバにブラウザから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

この実験結果から、以下のことがわかります。

リクエストボディの中に、&で区切られて、 「要素の名前(name属性)=値」の形式で値が送られている
上記で言えば、11行目を&で区切った最初の項目は 「text_name=abc+%93%FA%96%7B%8C%EA」です。 この=の左の「text_name」は、form.htmlの7行目にある input要素のname属性です。
値は%つきでエンコードされている
では=の右側はどうかといえば、「abc+%93%FA%96%7B%8C%EA」です。 これは前回扱った URLエンコードに似ていますが、前回のエンコード法とは微妙に異なり、 空白が「+」に置き換えられています。
これはapplication/x-www-form-urlencodedと呼ばれるエンコード法です。 前回扱ったエンコード方法とapplication/x-www-form-urlencodedの 相違については Wikipediaあたりが参考になるかと思います。 具体的なエンコード方法についてはHTML4.01の仕様書あたりがよいのではないでしょうか。
日本語部分は、Shift-JISコードからエンコードされています。 これは今回form.htmlの文字コードがShift-JISだからです (前回の補足「 クエリストリング部分の文字コード」も参照のこと)。
また、テキストエリアに対して送信されたデータ(「textarea_name=」の続き) を見ると、改行が「%0D%0A」つまりCR LFになっていることがわかります。
ラジオボタンやチェックボックスでは、選択された要素についてだけ value属性の値が送付されている
ラジオボタンは、name属性がradio_nameである4つのinput要素のうち 選択されている要素(ふたつめの要素)のvalue属性だけが送付されていますし、 チェックボックスでは2番目と3番目が選択されていますから 「check_name=check2&check_name=check3」のように ふたつの要素についてvalue属性が送付されています ※1
<input type="hidden">の要素については、 value属性で指定した値が送られている
他のinput要素は、ユーザが手で入力した内容を送信しますが、 hidden要素はHTML中にあらかじめ埋め込まれたvalue属性の値が送られます。
これを何に使うのかといえば、 ページ間でデータのやり取りをするために使います。 たとえばうちの掲示板は、 投稿フォーム→確認画面→投稿完了画面という画面遷移になっていますが、 確認画面のHTMLに、hidden要素で投稿内容を埋め込んでいます (実験する場合はテスト用掲示板を使ってください)。 投稿フォームからPOSTされた投稿内容をhiddenとして確認画面に埋め込み、 そこからさらにPOSTされたものをデータベースに書き込んでいるわけです。
GETにせよPOSTにせよ、Webアプリケーションは、 たくさんのユーザからばらばらに届くリクエストを処理しなければなりません。 ユーザAさんが投稿フォームで送信ボタンをクリックし、 確認画面でまたボタンをクリックしたとして、 このふたつのボタンクリックは完全に独立したリクエストとして届きます (こういう形態をステートレスと言います)。 よって、投稿内容等を次の画面に引き継ごうと思ったら、 このようにhiddenで画面に埋めるなり、(ここでもいずれ扱うつもりの) セッションという仕組みを使う必要があります。
このステートレスという縛りが、 Webアプリケーション開発を何かと難しいものにしているのですが、 その手の話はいずれそのうち。
<input type="file">の要素については、 ファイル名だけが送られている。
入力フォームでpost.htmlというファイルを指定したのですが、 サーバに送られているのはファイル名だけで、ファイルの内容は届いていません。 通常、<input type="file">はファイルの アップロードに使うわけで、ファイル名だけ届いても役に立ちません。 これはHTMLの書き方が悪いわけで、次の項に続きます。

1ページに複数のボタンを配置したい

上にも書きましたが、POSTメソッドは 「アンケートフォームのようなものを埋めてもらって送信する」 という用途を想定しているようです。

特に業務アプリなどでは、1画面にたくさんのボタンが並んでおり、 それぞれに機能が異なる、というケースが多いでしょう。 そのような場合は、サーバ側でどのsubmitボタンが押されたのかを 区別しなければなりません。

考えられる方法は、以下の通りです。

multipart/form-data

上記の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 の値を付けるように定められているためです。

Webアプリを作る

GETメソッドであれば、指定されたファイルを返せば、 静的ページのみ対応のWebサーバとして機能しますが、 POSTメソッドは、 何らかのプログラムで値を受け取らなければ意味がありません。

というわけで、次はいよいよWebアプリケーションを作ります。

2013/12/08公開


前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る