読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

CGIが動くWebサーバを,3分で自作しよう (ブラウザからのHTTPリクエストを処理する,簡易ソケットプログラム)

java ruby n分 ネットワーク プログラミング

Webサーバを3分で自作する。


ローカルPC内の

  • HTMLなどの固定ファイル
  • PHP, Rubyなど動的ページ

を,どちらもブラウザ越しでアクセスできるよう,インターネットに公開する。




以下を流し読みしながら,ソースコードをコピペすると,およそ3分でWebサーバが動く。


とりあえずGETのみ・テキストデータのみ・SJISのみ。

実用版ではないので,起動中のセキュリティリスクは自己責任で。

(1)窓口を作ろう

まずは,「リクエストを受け付けて,レスポンスを返す」という窓口の部分。

下記コードを javac Sv.java でコンパイル。Sv.class ができる。


Sv.java

import java.io.*;
import java.net.*;

public class Sv implements Runnable {
	
	// ユーザの窓口となるソケット
	private Socket sock_ = null;

	
	// サーバ起動時に実行する部分
	public static void main( String[] args )
	{
		if ( args.length != 1 )
		{
			System.err.println("usage: java Sv port");
			return;
		}
		try {
			// サーバソケットの作成
			ServerSocket svsock = new ServerSocket( Integer.parseInt( args[ 0 ] ) );
			while( true )
			{
				// クライアントからの接続を受け付ける
				Socket sock = svsock.accept();
				Sv sv = new Sv(sock);
				Thread tr = new Thread(sv);
				tr.start();
			}
		}
		catch ( Exception e )
		{
			e.printStackTrace();
		}
	}

	// インスタンス作成時
	public Sv(Socket sock)
	{
		this.sock_ = sock;
	}
	
	// インスタンス破棄時
	protected void finalize()
	{
		try {
			sock_.close();
		} catch (Exception e) {}
	}


	// スレッド化した時に実行する部分
	public void run()
	{
		
		// -------- リクエストを受信 --------

		
		String s = null;
		String from_user = null;
		try{
			BufferedReader in = new BufferedReader( new InputStreamReader( this.sock_.getInputStream() ) );
			if( (s = in.readLine()).length() > 0 )
			{
				// 要求ファイル名を切り出す
				// 例: GET /example/request/ HTTP/1.1
				from_user = s.split(" ")[ 1 ];
				from_user = from_user.substring( 1, from_user.length() );
			}
		}
		catch( IOException e )
		{
			System.out.println("読み取りエラー");
		}

		
		// -------- ファイル読み取り --------

		
		StringBuilder sb_msg = new StringBuilder();
		try{
			// バッチに渡す
			Process process = Runtime.getRuntime().exec( "response.bat \"" + from_user + "\"" );
			InputStream is = process.getInputStream();
			BufferedReader br = new BufferedReader( new InputStreamReader( is ) );
			String line;
			while ( ( line = br.readLine() ) != null )
			{
				sb_msg.append( line + "\r\n" );
			}
		}
		catch( IOException e )
		{
			System.out.println("外部コマンドエラー");
		}
		
		String msg = new String( sb_msg );
		
		
		// -------- レスポンスを送信 --------
		
		
		try {

			DataOutputStream out = new DataOutputStream( sock_.getOutputStream() );
			
			// 送信文字列
			StringBuilder sb = new StringBuilder();
			sb.append( "HTTP/1.1 200 OK\r\n" );
			sb.append( "Connection: close\r\n" );
			sb.append( "Content-Type: text/html; charset=Shift_JIS\r\n" );
			sb.append( "\r\n");
			sb.append( msg );
			sb.append( "\r\n" );
			String str = new String( sb );
			byte[] b = str.getBytes();

			// 送信
			out.write( b, 0, b.length );
			sock_.close();

		}
		catch ( Exception e )
		{
			e.printStackTrace();
		}
	}

}

ServerSocket で,特定のポートをリッスンしている。

コマンドラインから

java Sv 80

と実行すれば,80番ポートへのHTTPリクエストを監視し始める。


ブラウザで www を利用する際のデフォルトは80番だから,
ブラウザのURLボックスに http://localhost/ を入力するだけで http://localhost:80/ と解釈され,
上記のサーバアプリが反応する。

(2)リクエスト処理を移譲する

(1)のコードは,ユーザのリクエストを受け付けたら,リクエスト文字列を response.bat に渡している。

サーバのリクエストをよりによってBATファイルが受け付けるというあたり,いかにもお手製サーバ感が漂う。


内容は,これだけ。

response.bat

	@cscript.exe /nologo returnFile.js %1

「%1」は,BATが受け取る第一引数。


このバッチは,他のバッチ(returnFile.js)にリクエストを受け流しているだけなのだ。

格好よく言えばリクエストディスパッチャという事になる。



※どうして受け流すだけの部分を作るのか?と言ったら,それは拡張性のため。

「何でもいいからとにかくリクエストを受け付け,それをよそに割り振る」という部分を作っておくと,あとで変更が容易。

(3)リクエストを処理して,レスポンスを作る

(2)からリクエスト文字列を移譲される部分。

ユーザがブラウザからサーバへ送るHTTPリクエストの1行目は,ドメインのルートへのリクエストならば

	GET / HTTP/1.1

のようになるが,ここで注目したいのは真ん中の「/」の部分。

この部分を読み取り,クライアントの要求通りのファイルの内容をレスポンスとして返せばよい。

もし要求されたファイルのパスがCGIだったら,適切なエンジンに処理を移譲する。

※HTTPリクエストは LiveHTTPHeadersなどで閲覧できる。

LiveHTTPHeadersでHTTPヘッダ情報を確認する
http://www.atmarkit.co.jp/fsecurity/r...


このリクエスト処理の部分をWSH/JScriptで作ってみる。

returnFile.js

/*

	サーバソケットが受けつけたリクエストを処理して応答するスクリプト

*/

// rubyパス
var ruby_path = "D:\\dev\\ruby\\bin\\ruby.exe";



if( WScript.Arguments.length == 0 )
{
	WScript.Echo("引数が指定されていません。");
	WScript.Quit();
}


// ---------- リクエストされたファイル名を調査 ----------


var arg = WScript.Arguments.Unnamed(0);
var filename = "";
var user_arg = "";
if( arg.length == 0 )
{
	// ドメインのトップがリクエストされた場合
	filename = "index.html";
}
else
{
	// パスが指定された場合
	var t_arg = arg.split( "?" );
	filename = t_arg[0];
	
	// GET引数
	if( t_arg.length > 1 )
	{
		user_arg = t_arg[1].replace( new RegExp( "&", "g" ), " " );
	}
}

// ---------- レスポンス ----------


// CGIか
if( filename.match( new RegExp("(\\.rb|\\.rbx)$") ) )
{
	var ws = WScript.CreateObject( "WScript.Shell" );
	var cmd = ruby_path + " " + filename + " " + user_arg;
		//WScript.Echo( cmd );
	var proc = ws.Exec( cmd );
	WScript.Echo( proc.StdOut.ReadAll() );
	WScript.Quit();
}
else
{
	// 静的ファイルを読み込み
	var fso = WScript.CreateObject("Scripting.FileSystemObject");
	if( fso.FileExists( filename ) )
	{
		var txt = fso.OpenTextFile( filename, 1 );
		WScript.Echo( txt.ReadAll() );
		WScript.Quit();
	}
}


// 処理できなかった場合
WScript.Echo( filename + "は見つかりませんでした。" );


これでOK。

CGIはとりあえずRubyのみ対応しているが,他の言語にも対応させたければ,リクエストの末尾の拡張子ごとに処理を振り分ければよい。

ruby.exeのパスは各自のインストールディレクトリを使う。



これで,セキュリティを全く考慮しないWebサーバが完成した。

必要なのは3ファイルだった。


(4)動かしてみよう :静的ページ

他のサーバアプリは切っておくこと。

前述のとおり,コンソールから

java Sv 80

で起動。(終了はCtrl + C)


ブラウザから「http://localhost/」へアクセスしてみよう。

index.htmlは見つかりませんでした。 

と表示されれば,動作は成功。


Sv.class と同じフォルダに,index.htmlを作ってみよう。


index.html

<h3>Welcome!</h3>

作った通りにブラウザ上に描画される。


index.htmlと同じフォルダ上に,新たにhtmlやフォルダを作成すれば,そのとおりのURLを打ち込むことでブラウザからもアクセスできる。

もちろんIPアドレスを指定すれば,自分以外の人々もアクセスしてこれる。

(5)動かしてみよう :動的ページ

Rubyも動く。ローカルで動くスクリプトと同じようなものでよい。


hoge.rb

print "<h3>Welcome !</h3>"


ちゃんとしたCGIもできる。


boo.rbx

require "cgi"

# 変数を初期化
ARGV.replace(["x=0&y=0"]) if ARGV.empty?
cgi = CGI.new

# GETパラメータを取得して計算
x = cgi["x"].to_i
y = cgi["y"].to_i
sum = x + y

# 出力
print "sum = " + sum.to_s

/boo.rbx?x=1&y=2 とかでアクセスすると,足し算の結果が画面に出力される。


※RubyでCGIを作る際の参考:

Rubyはじめました:RubyでCGI.フォームデータを受けとる
http://blog.tofu-kun.org/070903003706...


[Ruby] CGIのオフラインモードを回避するには
http://d.hatena.ne.jp/takehikom/20080...
CGI.newの前にARGVを初期化しておけばよい。


One Click Installer
http://pub.cozmixng.org/~the-rwiki/rw...


とにかく文字列を返すようなプログラムがあれば,何でもCGIになるのだ。

最悪BATでcgiをしたってよいし,適当なexeでもよい。

補足

なんだこれだけの事か,と思わせられれば成功。

実際,全てこれだけの事なのだ。


80番ポートに来たリクエスト文字列を見て,

  • 静的な要求ならば,要求通りの場所のファイルを読み込む。
  • 動的な要求ならば,同一マシン内の別のプログラムに処理を委ねる。

ソケットプログラミングのサンプルとしては,よくチャットサーバなんかが取り上げられるが,今回のほうが題材としておもしろいだろう。

特にCGIを好き勝手に実現できるあたり。



他の言語の同様のコード:

C言語で
http://research.nii.ac.jp/~ichiro/sys...

Common Lispで
http://d.hatena.ne.jp/yuushimizu/2008...

Javaで
http://codezine.jp/article/detail/170


なおアクセス権限などを実装しておらず,ブラウザから全てのファイルにアクセスできる状態なので,起動したら早めに終了させましょう。