« ◇ubuntuにminimServer導入 | トップページ | ◇ハサミはPLUSのフッ素加工に限る »

◇Javaで簡易Httpサーバを作りLinuxでサービス化する

この記事では次の2つの異なるレベルの問題を扱います。

  • Javaで簡易Httpサーバを作る
  • Java作成したサーバをLinuxでサービス化する

HTTPS、マルチスレッドの単純サンプルを ◆HTTPS試験サーバJava編に作成しました。

  Javaで簡易Httpサーバを作る

Javaで標準で用意されるcom.sun.net.httpserver.HttpHandlerクラスを用いて単純なHTTPサーバを作成します。

「開発環境に簡単なhtmlドキュメントサーバが欲しいのだけど簡単には入れられない」といった場合にこんなおもちゃでも重宝します。

HttpHandlerに関する詳しい説明は他に譲りここでは一応動くプログラムを示すだけにします。

このプログラムは起動時にポートとドキュメントの配置位置、index.htmlを解釈するかどうかを指定するようになっています。
urlでフォルダが指定されると、その中にあるindex.html,index.htmを探し、存在すればそちらにジャンプし、無ければ当該フォルダの内容がリスト表示されるようになっています。

import java.io.File;
import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class MiniHttpServer implements HttpHandler {
   static final boolean D=true;
   static final byte[] NOT_FOUND    = ("<html><head><title>404 - Not Found</title>"
                                    + "</head><body>404 - Not Found"
                                    + "(directory must ends with '/')</body></html>")
                                     .getBytes();
   static final byte[] SERVER_ERROR = ("<html><head><title>500 - Error</title></head>"
                                    + "<body>500 - Error</body></html>")
                                     .getBytes();
   static String      base_dir;
   static boolean     index_jump;
   static PrintStream pr=System.err;
   static void help(String msg_,int code){
      pr.println(msg_);
      pr.println("Usage: java -jar MiniHttpServ.jar port htdocs_dir [index_jump]");
      pr.println("   Ex) java -jar MiniHttpServ.jar 8001 /var/www/htdocs index_jump");
      System.exit(code);
      }
   public static void main(String[] args) throws Exception {
      MiniHttpServer miniHttpServer = new MiniHttpServer();
      if( args.length<2 ) help("needs port and htdocs_dir",1);
      base_dir  = args[1];
      if( args.length>=3 ) index_jump= args[2].equals("index_jump");
      int        _port   = Integer.parseInt(args[0]);
      HttpServer _server = HttpServer.create(new InetSocketAddress(_port), 0);
      _server.createContext("/", miniHttpServer);
      if(D)pr.println("miniHttpSever started, wating at port "+_port);
      _server.start();
      }
   @Override
   public void handle(HttpExchange http_) throws IOException {
      if(D)pr.println(http_.getRequestMethod()+" "+http_.getRequestURI());
      OutputStream _out = http_.getResponseBody();
      try{
         String _pathName=base_dir+http_.getRequestURI();
         File   _file    =new File(_pathName);
         boolean _dirStyle= _pathName.endsWith("/");
         byte[] _buf=null;
         if(_file.isFile()&&!_dirStyle) {
            // htmlファイル等読み込み
            try(InputStream _is = new FileInputStream(_file)){
               _buf = new byte[(int)_file.length()];
               _is.read(_buf);
               }
            catch(Exception _ex){throw _ex;}
            }
          else if(_file.isDirectory() && _dirStyle ){
            // フォルダの場合ファイルリストを出す
            StringBuilder _sb = new StringBuilder();
            _sb.append("<html><title></title><body><a href=\"../\">../</a><br>\n");
            String _indexFile=null;
            for(File f : _file.listFiles()) {
               String name = f.isDirectory()?f.getName()+"/" : f.getName();
               _sb.append("<a href=\"").append(name).append("\">")
                  .append(name).append("</a><br>\n");
               if( name.startsWith("index.htm")) _indexFile=name;
               }
            if(index_jump&&_indexFile!=null){
               // インデックスファイルを含む場合は強制的に移動
               _sb.append("<script>");
               _sb.append("window.location.href = '"+_indexFile+"';");
               _sb.append("</script>");
               }
            _sb.append("</body></html>");
            _buf = _sb.toString().getBytes();
            }
         if( _buf!=null ){
            http_.sendResponseHeaders(200, _buf.length);
            _out.write(_buf);
            if(D)pr.println("ok/200");
            }
         else {
            if(D)pr.println("not found/404 "+http_.getRequestURI());
            http_.sendResponseHeaders(404, NOT_FOUND.length);
            _out.write(NOT_FOUND);
            }
         }
      catch (Exception _ex) {
         if(D)pr.println("exception : "+_ex+"\nserver-error/500");
         http_.sendResponseHeaders(500, SERVER_ERROR.length);
         _out.write(SERVER_ERROR);
         _ex.printStackTrace();
         }
      finally {
         _out.close();
         http_.close();
         }
      }
   }

先頭のDをfalseにするとログトレースを出しません。
簡易プログラムであり、残念ながらシングルスレッドで動きます。あくまで臨時のwebドキュメントサーバとして動かすものです。


ビルドし試験起動するバッチファイルを示します。

:: shift-jis (UTF-8では動かない)
:: CR-LF
javac -encoding utf-8 MiniHttpServer.java
if ERRORLEVEL 1 goto ERR
jar -cmf MiniHttpServer.mf MiniHttpServer.jar MiniHttpServer.class
del *.class
java -jar MiniHttpServer.jar 8001 C:/xampp/htdocs index_jump
::MiniHttpServer.jar 8001 C:/xampp/htdocs index_jump
:ERR
pause

MiniHttpServ.mfには実行可能jarを作るための指定があり、次のようになっています。MiniHttpServerの後ろに改行(CR-LF)が入っていることに留意してください。

Main-Class: MiniHttpServer

実行可能jarはWindowsでは単体で実行出来ます。
Linuxではjava -jarで指定する必要があります。
Windowsでもコマンドウィンドウの終了でうまく終了できないことがありますので、ここではjava -jarを使用しています。

  JavaプログラムをLinuxでサーバとして動かす

Javaはシンプルな文法を持ち書きやすいものとなっています。
例えばPythonの複雑極まりない変数スコープやself地獄や、変数名の書き間違い時に別の変数定義となるといった恐ろしい仕様は持ちません。

しかし、折角ビルドされた物を動かすのはとてつもなく大変なのです。

ここでは前項で作成した簡易httpサーバをLinuxでsystemctlで操作するサービスとして定義します。
(ubuntu,centOs7,ASW-ubutuで動作確認しました)

通常のサービスと同様に/etc/systemd/stsytem/に.serviceを置きます。
しかし!

  • .serviceで直接javaを起動するのではなく
  • 開始用、終了用それぞれシェルスクリプト経由となる

ここでは.jarとシェルスクリプトは/project/miniHttpServer/に置きました。

 start.sh

起動用のシェルスクリプト(/project/miniHttpServer/start.sh)を示します。
やっていることはjavaを起動し、後で停止するためのpidをファイルに落とすことです。
プログラム引数であるポート番号やドキュメントフォルダの指定もここで行います。

#!/bin/bash
# start.sh
# LF (CR-LFでは動かない)
cd /project/miniHttpServer/
java -jar MiniHttpServer.jar 8001 /var/www/htdocs index_jump
pid=$!
touch $pid.pid # PIDファイルを作成する

実行パーミションが必要です。

 stop.sh

停止用のシェルスクリプト(/project/miniHttpServer/stop.sh)を示します。
start.shで作成したpidを元に停止処理を行います。

#!/bin/bash
# stop.sh
# LF (CR-LFでは動かない)
cd /project/miniHttpServer/
pidFile=`ls *.pid`
pid=${pidFile%.*}  # {pid}.pidファイルからpidを取得
echo $pid stop server 
kill $pid # kill
rm -rf $pidFile # pidファイル削除
実行パーミションが必要です。

 miniHttpServ.service

.service(/etc/systems/system/miniHttpServ.service)を示します。

[Unit]
Description = mini HTTP service

[Service]
StartLimitBurst=0
User = root
WorkingDirectory= /project/miniHttpServer/
ExecStart = /project/miniHttpServer/start.sh
ExecStop = /project/miniHttpServer/stop.sh
Restart = always
Type = simple

[Install]
WantedBy = multi-user.target

サービスの起動にはsystemctlコマンドを使います。

$ sudo systemctl start   miniHttpServer # 開始
$ sudo systemctl status  miniHttpServer # 状態表示
$ sudo systemctl stop    miniHttpServer # 停止
$ sudo systemctl restart miniHttpServer # リスタート
$ sudo systemctl enable  miniHttpServer # システム起動時に自動開始
$ sudo systemctl disable miniHttpServer # 自動開始解除

 サービスにはしないが実行可能ファイルにする

実行可能jarではあってもLinuxではjava -jar無しで実行することはできません。

.jarをシェルスクリプトに含有させることで、実行可能ファイルを作ることができます。

$ echo '#!/usr/bin/java -jar' >  miniHttpServer  # 基本スクリプト
$ cat miniHttpServer.jar      >> miniHttpServer  # 後ろにファイル追加
$ chmod a+x miniHttpServer                       # 実行パーミション付加
$ miniHttpServer 8001 /var/www/htdocs index_jump # 実行

残念ながら、この実行可能ファイルを用いて「安定した」サービス化は出来ませんでした。

  ダウンロード

次の場所にファイルセットを置きました。右クリックで「対象をファイルに保存」でダウンロードしてください。

minihttpserver.zip

内容は次のものです。

A03_build.bat(win)MiniHttpServer.jarを作成する
A05_test.bat(win)MiniHttpServer.jarを作成し実行する
A05_test.sh(linux)MiniHttpServer.jarを実行する
MiniHttpServer.jar(linux)実行形式jar
MiniHttpServer.java(win)javaソース
MiniHttpServer.mf(win)実行形式jar作成用マニフェスト
miniHttpServer.service(linux)サービス定義
start.sh(linux)サービス用starスクリプト
stop.sh(linux)サービス用stopスクリプト

適当なフォルダに展開し、次の修正を行ってください。

  • 展開フォルダの名前を
    • miniHttpServer.service
    • start.sh
    • stop.sh
    に設定
  • ポート番号、http保存フォルダ、index.htmlに関するオプションを
    • A05_test.bat
    • A05_test.sh
    • star.sh
    に設定

miniHttpServer.serviceは/etc/systemd/system/下に移して下さい。

異常時の対応

何等かの障害により一度立ち上げたプログラムがうまく終了しないことがあり, 以降の起動で次のエラーになることがあります。

Exception in thread "main" java.net.BindException: Address already in use: bind

Windowsの場合はウィンドウマネージャでjavaを探しタスクを終了させます。

Linuxの場合はps -ea|grep javaでjavaプロセスを探し、対応すると思われるプロセスをkill -9で殺します。

|

« ◇ubuntuにminimServer導入 | トップページ | ◇ハサミはPLUSのフッ素加工に限る »