セッションに対応する

セッションとは

「セッション」という言葉にはコンピュータネットワーク用語に限っても 結構いろいろな意味がありますが、 Webアプリケーション開発者にとっては、ざっくり 「ひとりのユーザ(ブラウザ)に対してひとつ割り当てられる、 Webサーバのメモリ上のオブジェクト」と考えてそう問題はないでしょう。

何度も書いているように、HTTPによる通信は基本的にステートレスです。 つまり、ひとつのブラウザから、 Webサーバに連続していくつかのリクエストを送った場合、 それらのリクエストはすべて別々のものとして扱われます。

とはいえそれでは不便なので、 前回とりあげたCookieにより 各ブラウザ※1に 固有のID(セッションID)を割り当て、 それに対応するメモリ領域をサーバ側で確保すれば、 各ブラウザごとに任意の情報を保持することができます。 これがセッションです。

具体的な使い方としては、たとえば「何ページにもわたる入力フォーム」 において、入力フォームの内容を最後の登録画面まで保持する、 といった用途が考えられます。 1ページ目で商品等を選び、2ページ目で住所氏名を入力、 3ページ目でクレジットカード番号を入力、4ページ目でアンケートに答えると 5ページ目に内容の確認画面が表示され、 そこで「登録」をクリックするとやっと登録が完了する、 といったWebサイトでは、1ページ目から4ページ目までの入力内容を、 5ページ目まで引き継がなければなりません。

これを実現するには、 たとえば各ページの入力内容を順次hiddenに埋めて次のページまで引き継ぐ、 という方法も考えられます(たとえば うちの掲示板で、入力内容を確認画面まで引き継ぐのには、 hiddenを使用しています)。 しかし、何ページにも渡ってhiddenで情報を持ち歩くのは面倒ですし、 hiddenを使うということは以前入力した内容をクライアントから 再取得するということですから、 クライアント側で悪意ある改竄をされたら困るのであれば、 入力チェックが再度必要になります。

その点、セッションを使用してサーバ側で情報を保持すれば、 何画面も引き継ぐ場合でもプログラミングが楽になりますし、 何しろサーバのメモリ内のものですからクライアントから改竄される心配もありません。

Webサーバがたくさんある場合

上で、セッションについて「Webサーバのメモリ上のオブジェクト」と書きました。 しかし、個人で作る簡単なWebアプリケーションとかならともかく、 ある程度大規模なWebアプリケーションだと、たいていWebサーバは複数台存在します。 Webサーバの手前に負荷分散装置があり、 それがたくさんのユーザからのリクエストを各Webサーバに振り分けるわけです。

あるユーザのブラウザからのリクエストが、あるときはWebサーバAに割り当てられ、 次のリクエストではWebサーバBに割り当てられたとき、 セッションオブジェクトが Webサーバのメモリ上にあったのでは継続して参照できません。

これを避けるにはいくつかの方法があります。

ひとつは、負荷分散装置において、 あるユーザからのリクエストは常に同じWebサーバに振り分けるようにすることです。 これは、負荷分散装置がHTTPレスポンスに(勝手に)Cookieを埋め込み、 以後はそのCookieを参照して同じWebサーバにリクエストを送る、 という方法で実現されます。 ただし、この方法では、Webサーバが壊れたとか、 アプリケーションサーバ(Tomcat等)を再起動した場合等には、 セッションが失われてしまいます。

別の方法として、セッションの情報をセッション専用のサーバ(セッションサーバ)に 保持したり、データベースに保持したりする方法もあります。

そういう意味では、 セッションを「Webサーバのメモリ上のオブジェクト」 と説明するのは不正確ではあるのですが、 セッションサーバやデータベースにセッションを保持する場合も、 アプリケーションプログラマからは「Webサーバのメモリ上のオブジェクト」 であるかのように見えるようにフレームワークなりが面倒を見てくれますから ※2、 ここでは気にしないことにします。

セッションをCookie以外で実現する方法

セッションを実現するには、セッションIDを何らかの方法で画面から画面へと 引き継いでいかなければなりません。 現在、通常使われる方法は、上で説明したように、Cookieを使う方法だと思います。

Cookie以外の実現方法として考えられるのは、以下のふたつです。

URL Rewriting

「?sessionId=xxxxxx」のように、 GETパラメタとしてセッションIDを埋め込んでしまう方法です。

昔のdocomoのガラケーのように、 Cookieが使えないブラウザでも使用できるという利点がありますが、 Referer等でセッションIDが漏洩しやすいという欠点があります。

今時Cookieの使えない古いガラケーを相手にすることもないでしょうが、 むしろ「タブブラウザが別タブとCookieを共有してしまう」といったケースでは 使うことがあるかもしれません。

hidden

hidden要素でセッションIDを送信する方法です。

ただし、hiddenはformのPOSTによってしか送信されないので、 単純なリンクではセッションが途切れてしまいます。 そこで、JavaScriptを使ってすべてのリンクをformのPOSTで実現する、 といった対応を行う必要があります。

Tomcatでセッションを使ってみる

Henacatにセッションを組み込むことを考える前に、 まずTomcatでどのようにセッションを使うのかを確認します。

以下は、「カウンタ」をセッションで実現した例です。 初回アクセスでは「No session」と表示され、 以後、リロードするたびに「Counter..○○」と表示される数字が増えていきます。

  1: import java.io.*;
  2: import java.util.*;
  3: import java.net.*;
  4: import javax.servlet.*;
  5: import javax.servlet.http.*;
  6: 
  7: public class SessionTest extends HttpServlet {
  8:     @Override
  9:     public void doGet(HttpServletRequest request, HttpServletResponse response)
 10:         throws IOException, ServletException {
 11:         response.setContentType("text/plain");
 12:         PrintWriter out = response.getWriter();
 13: 
 14:         HttpSession session = request.getSession(true);
 15:         Integer counter = (Integer)session.getAttribute("Counter");
 16:         if (counter == null) {
 17:             out.println("No session");
 18:             session.setAttribute("Counter", 1);
 19:         } else {
 20:             out.println("Coutner.." + counter);
 21:             session.setAttribute("Counter", counter + 1);
 22:         }
 23:     }
 24: }

このサーブレットにアクセスすると、初回アクセスのレスポンスヘッダに、 以下のようなSet-Cookieヘッダが付与されます。

Set-Cookie: JSESSIONID=FDE3631951EEC9BA33BE1F7B80051551; Path=/sessiontest/; HttpOnly

Tomcatでは、 このJSESSIONIDというキーで送付されている32桁の16進数文字列がセッションIDです。

なお、このCookieにHttpOnly属性が付与されているのは、 これを付けないとJavaScript等でセッションIDが参照できてしまうので、 ページにXSS脆弱性があった場合にはセッション IDを奪われてしまう可能性があるためです。

リダイレクトについて

前々回の記事の補足 「投稿した後リロードすると」において、

これを避けるには、投稿後、POSTに対するレスポンスとして 掲示板のHTMLを返すのではなく、 リダイレクトで再度掲示板を表示するURLに飛ばすようにします。 これは別に難しくはないですが、次回のネタに取っておきます。

……と書いておきながら、 その次の回でこれを扱うことをすっかり忘れていたので今回対応します。

サーブレットでリダイレクトを行うには、 HttpServletResponseのsendRedirectメソッドを使用します。 TestBBS.javaであれば、doPost()メソッドの最終行で、 現状のソースでは以下のように doGet()を呼び出して投稿一覧のHTMLを返しているところを、

  doGet(request, response)

以下のように、/testbbs/TestBBSにリダイレクトするようにします。

  response.sendRedirect("/testbbs/TestBBS");

上記のようにコードを修正してTomcatで実行したところ、 掲示板への投稿のPOSTに対して送付された レスポンスヘッダは以下のようになっていました。

HTTP/1.1 302 Found
Server: Apache-Coyote/1.1
Location: http://localhost:8080/testbbs/TestBBS
Content-Length: 0
Date: Thu, 01 Jan 2015 07:51:47 GMT

Tomcatは「302 Found」を返していることがわかります。

これに対してブラウザがどう応答するかですが、 「本来、POSTのリダイレクトに対してはPOSTを投げなければいけないところ、 ほとんどのブラウザはGETを投げる」 ということのようです※3。 というか実のところ今回の用途ではGETを投げてくれなければ困るのですが、 それならば303を使うべきところ、 いろいろ仕様と現実がうまく合っていないところがあるようです。

Henacatにおける実装

新設/メソッド追加するインタフェース

HttpSessionインタフェース

セッションオブジェクトを表現するインタフェースです。 これに対し、setAttribute()でデータを登録し、 getAttribute()でそのデータを取得します。

登録するデータのクラスはjava.lang.Objectなので、 任意のデータを登録できます。

void setAttribute(String name, Object value)

名前を指定してデータを登録します。

Object getAttribute(String name)

指定した名前で登録されたデータを取得します。

Enumeration<String> getAttributeNames()

このセッションに登録されているすべてのデータの名前を取得します。

void removeAttribute(String name)

指定した名前で登録されたデータを削除します。

String getId()

このセッションのセッションID(Cookieでやりとりされるもの)を返します。

HttpServletRequestインタフェース

セッションを取得するメソッドを追加しています。

HttpSession getSession()

セッションオブジェクトを取得します。存在していなければ作成して返します。

HttpSession getSession(boolean create)

セッションオブジェクトを取得します。存在していなければnullを返します。

HttpServletResponseインタフェース

上記したリダイレクトに関連するメソッドを追加しています。

void sendRedirect(String location)

指定したURLにリダイレクトします。 正しくは、引数のURLは相対指定が可能でなければいけないのですが、 今回は手抜きのため(「http://」から始まる)フルパスによる指定と、 「/」からの指定のみに対応しました。

void setStatus(int sc)

ほぼリダイレクトに関連する内部処理の都合ですが、 HTTPステータスコードを指定するメソッドを追加しています。

定数SC_OKおよびSC_FOUND
setStatus()の引数として指定するための定数です。 OKとFoundしかないのか他のステータスコードはどうなった、 と言われそうですが、 現状、上記のとおりsetStatus()自体の用途を限定しているので このふたつしか用意していません(手抜きです)。

HttpSessionの実装

HttpSessionインタフェースの実装クラスがHttpSessionImplクラスです。

  1: package com.kmaebashi.henacat.servletimpl;
  2: import com.kmaebashi.henacat.servletinterfaces.*;
  3: import java.util.*;
  4: import java.util.concurrent.*;
  5: 
  6: class HttpSessionImpl implements HttpSession {
  7:     private String id;
  8:     private Map<String, Object> attributes
  9:         = new ConcurrentHashMap<String, Object>();
 10:     private volatile long lastAccessedTime;
 11:     
 12:     public String getId() {
 13:         return this.id;
 14:     }
 15: 
 16:     public Object getAttribute(String name) {
 17:         return this.attributes.get(name);
 18:     }
 19: 
 20:     @Override
 21:     public Enumeration<String> getAttributeNames() {
 22:         Set<String> names = new HashSet<String>();
 23:         names.addAll(attributes.keySet());
 24: 
 25:         return Collections.enumeration(names);
 26:     }
 27: 
 28:     public void removeAttribute(String name) {
 29:         this.attributes.remove(name);
 30:     }
 31: 
 32:     public void setAttribute(String name, Object value) {
 33:         if (value == null){
 34:             removeAttribute(name);
 35:             return;
 36:         }
 37:         this.attributes.put(name, value);
 38:     }
 39: 
 40:     synchronized void access() {
 41:         this.lastAccessedTime = System.currentTimeMillis();
 42:     }
 43: 
 44:     long getLastAccessedTime() {
 45:         return this.lastAccessedTime;
 46:     }
 47: 
 48:     public HttpSessionImpl(String id) {
 49:         this.id = id;
 50:         this.access();
 51:     }
 52: }

見てのとおり、Map(実際にはConcurrentHashMap)にて、 セッションデータを保持しています。

access()メソッドは、 セッションに最後にアクセスした時刻を更新するためのメソッドです。

セッションオブジェクトをいつまで保持していればよいのか、 正確なところはサーバ側ではわかりません。 たとえばWebアプリケーションを使い始めたユーザが突然ブラウザを ×ボタンで終了した場合、 それをサーバ側で検知することはできません。 そこで、一定時間アクセスがなかったセッションオブジェクトは 勝手に削除するようにしています(セッションタイムアウト)。 access()メソッドを呼ぶことで、セッションの最終アクセス時刻 (lastAccessedTime)が更新され、 このセッションオブジェクトの寿命を延ばすことができます。

SessionManager.java

たくさんのセッションオブジェクトを管理するクラスがSessionManagerクラスです。

  1: package com.kmaebashi.henacat.servletimpl;
  2: import com.kmaebashi.henacat.servletinterfaces.*;
  3: import java.util.*;
  4: import java.util.concurrent.*;
  5: 
  6: class SessionManager {
  7:     private static SessionManager instance;
  8:     private final ScheduledExecutorService scheduler;
  9:     private final ScheduledFuture<?> cleanerHandle;
 10:     private final int CLEAN_INTERVAL = 60; // seconds
 11:     private final int SESSION_TIMEOUT = 10; // minutes
 12:     private Map<String, HttpSessionImpl> sessions
 13:         = new ConcurrentHashMap<String, HttpSessionImpl>();
 14:     private SessionIdGenerator sessionIdGenerator;
 15: 
 16:     static SessionManager getInstance() {
 17:         if (instance == null) {
 18:             instance = new SessionManager();
 19:         }
 20:         return instance;
 21:     }
 22: 
 23:     synchronized HttpSessionImpl getSession(String id) {
 24:         HttpSessionImpl ret = sessions.get(id);
 25:         if (ret != null) {
 26:             ret.access();
 27:         }
 28:         return ret;
 29:     }
 30: 
 31:     HttpSessionImpl createSession() {
 32:         String id = this.sessionIdGenerator.generateSessionId();
 33:         HttpSessionImpl session = new HttpSessionImpl(id);
 34:         sessions.put(id, session);
 35:         return session;
 36:     }
 37: 
 38:     private synchronized void cleanSessions() {
 39:         for (Iterator<String> it = sessions.keySet().iterator();
 40:              it.hasNext();) {
 41:             String id = it.next();
 42:             HttpSessionImpl session = this.sessions.get(id);
 43:             if (session.getLastAccessedTime()
 44:                 < (System.currentTimeMillis()
 45:                    - (SESSION_TIMEOUT * 60 * 1000))) {
 46:                 it.remove();
 47:             }
 48:         }
 49:     }
 50: 
 51:     private SessionManager() {
 52:         scheduler = Executors.newSingleThreadScheduledExecutor();
 53: 
 54:         Runnable cleaner = new Runnable() {
 55:                 public void run() {
 56:                     cleanSessions();
 57:                 }
 58:             };
 59:         this.cleanerHandle
 60:             = scheduler.scheduleWithFixedDelay(cleaner,
 61:                                                CLEAN_INTERVAL, CLEAN_INTERVAL,
 62:                                                TimeUnit.SECONDS);
 63:         this.sessionIdGenerator = new SessionIdGenerator();
 64:     }
 65: }

サーブレットでは、正しくは、 セッションオブジェクトは「コンテキスト」単位に保持されます。 ただしHenacatにおいては簡単にするためセッションの保管庫は 「全体でひとつ」にしてしまいました。 そこで、SessionManagerクラスはSingletonになっています (Singletonはデザインパターンの一種です。ご存じない方はぐぐってください)。

SessionManagerの利用者は、まずgetInstance()でSessionManagerの インスタンスを取得します。

新たにセッションオブジェクトを生成したい場合は createSession()を呼び出します。 ここで、後述するSessionIdGeneratorクラスのgenerateSessionId() メソッドによりセッションIDが生成され、 そのIDでセッションオブジェクトが作られます。 生成されたセッションオブジェクトは、SessionManager内でMap (実装はConcurrentHashMap)にて保持されています。

登録済みのセッションを取得したい場合は セッションIDをキーにgetSession()を呼び出します。

SessionManagerでは、 セッションタイムアウトを実現するために、 ScheduledExecutorServiceを使用して 定期的にcleanSessions()を呼び出します。 cleanSessions()では、保持しているセッションオブジェクトを順に確認し、 SESSION_TIMEOUTの分数を超えたセッションオブジェクトを削除します。

ここで、getSession()メソッドとcleanSessions()にsynchronizedが 付いているのは、セッションタイムアウトぎりぎりにgetSession() されたときにそのセッションが削除されてしまわないようにするためです。 cleanSessions()は時間がかかるかもしれないので、 これ全体をsynchronized指定するのはやや乱暴ではあるのですが、 今回は簡単にするためこれでよしとしました。

SessionIdGenerator.java

SessionIdGeneratorクラスは、セッションIDを生成するクラスです。

  1: package com.kmaebashi.henacat.servletimpl;
  2: import java.security.*;
  3: 
  4: class SessionIdGenerator {
  5:     private SecureRandom random;
  6: 
  7:     public String generateSessionId() {
  8:         byte[] bytes = new byte[16];
  9:         this.random.nextBytes(bytes);
 10:         StringBuilder buffer = new StringBuilder();
 11:         
 12:         for (int i = 0; i < bytes.length; i++) {
 13:             buffer.append(Integer.toHexString(bytes[i] & 0xff).toUpperCase());
 14:         }
 15:         return buffer.toString();
 16:     }
 17: 
 18:     SessionIdGenerator() {
 19:         try {
 20:             random = SecureRandom.getInstance("SHA1PRNG");
 21:         } catch (NoSuchAlgorithmException ex) {
 22:             System.out.println(ex);
 23:             ex.printStackTrace();
 24:             System.exit(1);
 25:         }
 26:     }
 27: }

セッションIDは、 各ブラウザごとにセッションオブジェクトを識別できればよいので、 セッションという機能をただ動作させるだけであれば、 たとえば連番でセッションIDを振ってもセッションは動作します。 しかし、そのような方法では、 悪意を持った攻撃者に簡単に他人のセッションIDを推測されてしまいます。 クライアントからCookieでそのセッションIDでCookieを送信すれば、 他人のセッションを乗っ取ることができてしまいます。 これがセッションハイジャックと呼ばれる攻撃手法です。

これを避けるために、セッションIDは、 可能な限り予測困難な乱数を使わなければなりません。 Henacatでは、それを得るために java.security.SecureRandomクラスを使用しています。

java.security.SecureRandomクラスでは、 乱数生成のアルゴリズムを指定できます。 Henacatでは、「SHA1PRNG」を指定しています。 これは、 Tomcatの実装(org.apache.catalina.util.SessionIdGenerator) においてアルゴリズムの指定がなかったときの挙動に合わせたものです。

HttpServletRequestの実装

HttpServletRequestインタフェースにgetSession()メソッドを足しましたので、 その実装クラスであるHttpServletRequestImplにも実装を足さなければいけません。 関連部分を抜粋して掲載します。

(前略)
  8: class HttpServletRequestImpl implements HttpServletRequest {
(中略)
 13:     private Cookie[] cookies;
 14:     private HttpSessionImpl session;
 15:     private HttpServletResponseImpl response;
 16:     private final String SESSION_COOKIE_ID = "JSESSIONID";
(中略)
 67:     public HttpSession getSession() {
 68:         return getSession(true);
 69:     }
 70: 
 71:     public HttpSession getSession(boolean create) {
 72:         if (!create) {
 73:             return this.session;
 74:         }
 75:         if (this.session == null) {
 76:             SessionManager manager = SessionManager.getInstance();
 77:             this.session = manager.createSession();
 78:             addSessionCookie();
 79:         }
 80:         return this.session;
 81:     }
 82: 
 83:     private HttpSessionImpl getSessionInternal() {
 84:         if (this.cookies == null) {
 85:             return null;
 86:         }
 87:         Cookie cookie = null;
 88:         for (Cookie tempCookie : this.cookies) {
 89:             if (tempCookie.getName().equals(SESSION_COOKIE_ID)) {
 90:                 cookie = tempCookie;
 91:             }
 92:         }
 93:         SessionManager manager = SessionManager.getInstance();
 94:         HttpSessionImpl ret = null;
 95:         if (cookie != null) {
 96:             ret = manager.getSession(cookie.getValue());
 97:         }
 98:         return ret;
 99:     }
100: 
101:     private void addSessionCookie() {
102:         this.response.addCookie(new Cookie(SESSION_COOKIE_ID,
103:                                            this.session.getId()
104:                                            + "; HttpOnly"));
105:     }
106: 
107:     HttpServletRequestImpl(String method, Map<String, String> requestHeader,
108:                            Map<String, String> parameterMap,
109:                            HttpServletResponseImpl resp) {
110:         this.method = method;
111:         this.requestHeader = requestHeader;
112:         this.cookies = parseCookies(requestHeader.get("COOKIE"));
113:         this.parameterMap = parameterMap;
114:         this.response = resp;
115:         this.session = getSessionInternal();
116:         if (this.session != null) {
117:             addSessionCookie();
118:         }
119:     }
120: }

コンストラクタにおいて、内部メソッドgetSessionInternal()を呼び出し(115行目)、 取得したセッションをインスタンスフィールドに保持しています。 getSessionInternal()メソッドでは、 「JSESSIONID」という名前のCookieからセッションIDを取り出し、 SessionManagerにそれが存在すれば取得しています。 ここで、SessionManagerのgetSession()を呼ぶことで、 HttpSessionImplのaccess()メソッドが呼び出されますから、 一度生成したセッションは、アクセスがあるごとに、 たとえその画面ではセッションを使わなくても延命されます。

コンストラクタでセッションを取得していた場合、 getSession()メソッドではそのセッションを単に返すだけですが、 アプリケーションで初めてセッションを作成するときには SessionManagerのcreateSession()メソッドでセッションを生成しています(77行目)。

そして、セッションIDは、レスポンスヘッダとして返さなければいけません。 それを行っているのがaddSessionCookie()メソッドです。 TomcatではJSESSIONIDのCookieにpath属性も付いていますが、 Henacatでは簡単にするため省略しました。 addSessionCookie()メソッドは、コンストラクタ(117行目)および getSession()メソッドで新たにセッションを作ったケース(78行目)で 呼び出されます。 ここで、HttpServletResponseに対してCookieの追加を行えるようにするために、 HttpServletRequestImpl のコンストラクタでHttpServletResponseImplを渡すようにしました。

リダイレクト処理

HttpServletResponseにリダイレクト関連のメソッドを追加しましたので、 その実装クラスであるHttpServletResponseImplにてそれらを実装します。

(前略)
  7: class HttpServletResponseImpl implements HttpServletResponse {
(中略)
 14:     int status;
 15:     String redirectLocation;
(中略)
 47:     @Override
 48:     public void sendRedirect(String location) {
 49:         this.redirectLocation = location;
 50:         setStatus(SC_FOUND);
 51:     }
 52: 
 53:     @Override
 54:     public void setStatus(int sc) {
 55:         this.status = sc;
 56:     }
 57: 
 58:     HttpServletResponseImpl(OutputStream output) {
 59:         this.outputStream = output;
 60:         this.status = SC_OK;
 61:     }
 62: }

sendRedirectメソッドで行っているのは、 引数として受け取ったリダイレクト先URLを保持して、 this.statusにSC_FOUNDをセットしているだけです。

this.statusを見て処理を振り分けるのは、 サーブレットの処理の呼び出し元となるServletService.javaで行っています。

  1: package com.kmaebashi.henacat.servletimpl;
  2: import com.kmaebashi.henacat.servletinterfaces.*;
  3: import com.kmaebashi.henacat.util.*;
  4: import java.util.*;
  5: import java.nio.file.*;
  6: import java.net.*;
  7: import java.io.*;
  8: 
  9: public class ServletService {
 10:     private static HttpServlet createServlet(ServletInfo info)
 11:         throws Exception {
 12:         FileSystem fs = FileSystems.getDefault();
 13:         Path pathObj = fs.getPath(info.servletDirectory);
 14:         URLClassLoader loader
 15:             = URLClassLoader.newInstance(new URL[]{pathObj.toUri().toURL()});
 16:         Class<?> clazz = loader.loadClass(info.servletClassName);
 17:         return (HttpServlet)clazz.newInstance();
 18:     }
 19: 
 20:     private static Map<String, String> stringToMap(String str) {
 21:         Map<String, String> parameterMap = new HashMap<String, String>();
 22:         if (str != null) {
 23:             String[] paramArray = str.split("&");
 24:             for (String param : paramArray) {
 25:                 String[] keyValue = param.split("=");
 26:                 parameterMap.put(keyValue[0], keyValue[1]);
 27:             }
 28:         }
 29:         return parameterMap;
 30:     }
 31: 
 32:     private static String readToSize(InputStream input, int size)
 33:         throws Exception{
 34:         int ch;
 35:         StringBuilder sb = new StringBuilder();
 36:         int readSize = 0;
 37: 
 38:         while (readSize < size && (ch = input.read()) != -1) {
 39:             sb.append((char)ch);
 40:             readSize++;
 41:         }
 42:         return sb.toString();
 43:     }
 44: 
 45:     public static void doService(String method, String query, ServletInfo info,
 46:                                  Map<String, String> requestHeader,
 47:                                  InputStream input, OutputStream output)
 48:         throws Exception {
 49:         if (info.servlet == null) {
 50:             info.servlet = createServlet(info);
 51:         }
 52: 
 53:         ByteArrayOutputStream outputBuffer =  new ByteArrayOutputStream();
 54:         HttpServletResponseImpl resp
 55:             = new HttpServletResponseImpl(outputBuffer);
 56: 
 57:         HttpServletRequest req;
 58:         if (method.equals("GET")) {
 59:             Map<String, String> map;
 60:             map = stringToMap(query);
 61:             req = new HttpServletRequestImpl("GET", requestHeader, map, resp);
 62:         } else if (method.equals("POST")) {
 63:             int contentLength
 64:                 = Integer.parseInt(requestHeader.get("CONTENT-LENGTH"));
 65:             Map<String, String> map;
 66:             String line = readToSize(input, contentLength);
 67:             map = stringToMap(line);
 68:             req = new HttpServletRequestImpl("POST", requestHeader,map, resp);
 69:         } else {
 70:             throw new AssertionError("BAD METHOD:" + method);
 71:         }
 72:         
 73:         info.servlet.service(req, resp);
 74: 
 75:         if (resp.status == HttpServletResponse.SC_OK) {
 76:             ResponseHeaderGenerator hg
 77:                 = new ResponseHeaderGeneratorImpl(resp.cookies);
 78:             SendResponse.sendOkResponseHeader(output, resp.contentType, hg);
 79:             resp.printWriter.flush();
 80:             byte[] outputBytes = outputBuffer.toByteArray();
 81:             for (byte b: outputBytes) {
 82:                 output.write((int)b);
 83:             }
 84:         } else if (resp.status == HttpServletResponse.SC_FOUND) {
 85:             String redirectLocation;
 86:             if (resp.redirectLocation.startsWith("/")) {
 87:                 String host = requestHeader.get("HOST");
 88:                 redirectLocation = "http://"
 89:                     + ((host != null) ? host : Constants.SERVER_NAME)
 90:                     + resp.redirectLocation;
 91:             } else {
 92:                 redirectLocation = resp.redirectLocation;
 93:             }
 94:             SendResponse.sendFoundResponse(output, redirectLocation);
 95:         }
 96:     }
 97: }

ServletService.javaには、HttpServletRequestImplの引数の追加とか、 細かい修正も入っていますが、 リダイレクト関連の処理は85行目以降にあります。 HttpServletResponseImplのstatusを確認し、SC_FOUND(302)であれば、 URLを組み立てて(前述の通り、フルパスと「/」で始まるパスにだけ対応しています)、 SendResponseクラスのsendFoundResponse()メソッドを呼び出しています。 sendFoundResponse()メソッドの実装は、以下のように、Locationを指定して ステータスコード302を返しています。

  1: package com.kmaebashi.henacat.util;
(中略)
  5: public class SendResponse {
(中略)
 46:     public static void sendFoundResponse(OutputStream output,
 47:                                          String location)
 48:         throws Exception {
 49:         Util.writeLine(output, "HTTP/1.1 302 Found");
 50:         Util.writeLine(output, "Date: " + Util.getDateStringUtc());
 51:         Util.writeLine(output, "Server: Henacat");
 52:         Util.writeLine(output, "Location: " + location);
 53:         //Util.writeLine(output, "Content-Length: 0");
 54:         Util.writeLine(output, "Connection: close");
 55:         Util.writeLine(output, "");
 56:     }
(後略)

また、ServletService.javaでConstants.SERVER_NAMEという定数を参照していますが、 これは従来com.kmaebashi.henacat.webserver.ServerThread.javaにおいて 定義されていました。 今回、ServletService.javaでも参照するようになったので、 com.kmaebashi.henacat.utilパッケージに切り出しました。

  1: package com.kmaebashi.henacat.util;
  2: 
  3: public class Constants {
  4:     public static final String SERVER_NAME = "localhost:8001";
  5: }

上記のソース一式をzipで固めたものは、 ここからダウンロードできます。

ひとまずここで終わりです

ここまでの実装で、へなちょこWebサーバ兼サーブレットコンテナHenacatは、 Cookieやセッションも使えるようになりました。

現時点での行数は空行等を含めても1000行以下です。 Henacatが実用に耐えるかどうかというと疑問ではありますが、 Webアプリケーションの仕組みを知るための教材としては そこそこよいのではないでしょうか。 なにしろ1000行なら、ソース全体を読んでもそんなに大変ではありませんから。

実際にWebアプリケーションを開発する上では、 言語もJavaとは限りませんし、Javaだとしても、 いまどきサーブレットを素で使うことはなく 何らかのフレームワークを使うことでしょう。 しかし、どのような言語を使うにせよ、 どのようなフレームワークを使うにせよ、 ここで説明したHTTPの仕組みは避けて通れないものです。

フレームワークの教科書は読んだものの、 内部で何がどうなっているのかわからなくて気持ちが悪い、 トラブルが起きても原因がわからない、 調べてみても人に聞いても 「リクエストヘッダで実際に飛んでいるCookieの内容を確認しろ」とか 言われて何のことだかわからない、という人達に、 この文章が何らかの助けになれば幸いです

2015/01/05公開


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