2014年11月18日火曜日

JavaのゆるいFast CGI Client

JavaのFast CGI Clientを探してたのですが、意外といいのが見つからなかったのでゆるく実装。(fastcgi.comはJavaはServer (=Application) 側だけかも、fcgi4jはエラーもIssuesは放置みたい、jfastcgiはJava(servlet)べったりの実装でコア機能だけを使いづらく。)

なぜいまさらFast CGI?というのは、レガシー接続に便利かなというのと、まあ軽そうだし。SCGIなんかに比べると、無駄にややこしいけど、まあ古いものなので。

なるべくFastCGI Specificationに沿ったお勉強的な。本気で使っちゃいけません。

利用イメージ


int requestID = 123;

FastCGIConnection fcgic = new FastCGIConnection("localhost", 9000);

Map params = new HashMap() {{
    put("QUERY_STRING", "qstr1=hello&qstr2=world");
    put("REQUEST_METHOD", "POST");
    put("SCRIPT_FILENAME", "/usr/share/nginx/html/test.php");
  }};
fcgic.sendParams(requestID, params);

fcgic.sendStdin(requestID, "post1=hello&post2=world".getBytes());

ByteArrayOutputStream stdoutStream = new ByteArrayOutputStream();
ByteArrayOutputStream stderrStream = new ByteArrayOutputStream();
fcgic.recvStdoutStderrAndWaitEndRequest(stdoutStream, stderrStream);
System.out.println("STDOUT: [" + stdoutStream.toString("UTF-8") + "]");
System.out.println("STDERR: [" + stderrStream.toString("UTF-8") + "]");

requestIDは、一連のやりとりの識別子。multiplex (一つのコネクションで複数のやりとりをする) のときに使われる。

FastCGIConnectionの中身

なかなか古風な実装で、遅そうですが。(以下は、Bloggerさんのおかげで、ソースは崩れててコピペでは使えません。)
import java.net.*;
import java.io.*;
import java.util.*;

/**
 * simple fastCGI client
 *  - RESPONDER only
 *  - no multiplex
 *  - sync
 *  - no charset consideration
 * 
 * FastCGI Specification
 * http://www.fastcgi.com/devkit/doc/fcgi-spec.html
 *
 * ver 0.5
 */

public class FastCGIConnection{

  //
  // 8. Types and Constants
  //

  final static int FCGI_VERSION_1 = 1;

  final static int FCGI_BEGIN_REQUEST = 1;
  final static int FCGI_ABORT_REQUEST = 2;
  final static int FCGI_END_REQUEST = 3;
  final static int FCGI_PARAMS = 4;
  final static int FCGI_STDIN = 5;
  final static int FCGI_STDOUT = 6;
  final static int FCGI_STDERR = 7;
  final static int FCGI_DATA = 8;
  final static int FCGI_GET_VALUES = 9;
  final static int FCGI_GET_VALUES_RESULT = 10;
  final static int FCGI_UNKNOWN_TYPE = 11;
  final static int FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE;

  final static int FCGI_KEEP_CONN = 1;

  final static int FCGI_RESPONDER = 1;
  final static int FCGI_AUTHORIZER = 2;
  final static int FCGI_FILTER = 3;

  public static void main(String[] args){
    System.out.println("FastCGI client sample");

    try{
      FastCGIConnection fcgic = new FastCGIConnection("localhost", 9000);

      final String method = "POST";
      final String scriptFileName = "/usr/share/nginx/html/test.php";
      final String queryString = "get1=hello&get2=world";
      final String requestBodyString = "post1=hello&post2=world";
      final byte[] requestBody = requestBodyString.getBytes();
      final String requestBodyLength = String.valueOf(requestBody.length);

      int requestID = 1;

      // B. Typical Protocol Message Flow

      // {FCGI_BEGIN_REQUEST,   1, {FCGI_RESPONDER, 0}}
      fcgic.sendBeginRequest(requestID, false);

      // {FCGI_PARAMS,          1, "..."}
      Map params = new HashMap() {{
          put("QUERY_STRING", queryString);
          put("REQUEST_METHOD", method);
          put("SCRIPT_FILENAME", scriptFileName);
          put("CONTENT_TYPE", "application/x-www-form-urlencoded");
          put("CONTENT_LENGTH", requestBodyLength);
        }};

      fcgic.sendParams(requestID, params);

      if(params.size() > 0){
        // {FCGI_PARAMS,          1, ""}
        params = new HashMap();
        fcgic.sendParams(requestID, params);
      }

      // {FCGI_STDIN,           1, "..."}
      fcgic.sendStdin(requestID, requestBody);

      // {FCGI_STDIN,           1, ""}
      fcgic.sendStdin(requestID, "".getBytes());

      // {FCGI_STDOUT,      1, "Content-type: text/html\r\n\r\n\n ... "}
      // {FCGI_END_REQUEST, 1, {0, FCGI_REQUEST_COMPLETE}}

      ByteArrayOutputStream stdoutStream = new ByteArrayOutputStream();
      ByteArrayOutputStream stderrStream = new ByteArrayOutputStream();

      fcgic.recvStdoutStderrAndWaitEndRequest(stdoutStream, stderrStream);

      System.out.println("STDOUT: [" + stdoutStream.toString("UTF-8") + "]");
      System.out.println("STDERR: [" + stderrStream.toString("UTF-8") + "]");

    }catch(Exception e){
       System.err.println("error: " + e.toString());
    }
  }

  Socket socket;
  BufferedOutputStream ostream;
  InputStream istream;

  public FastCGIConnection(String host, int port) throws IOException{
    socket = new Socket(host, port);
    ostream = new BufferedOutputStream(socket.getOutputStream());
    istream = socket.getInputStream();
  }

  //
  // 3. Protocol Basics
  //

  //
  // 3.3 Records (All data is carried in "records")
  //
  int sendRecordHeader(int requestID, int recordType, int contentLength)
      throws IOException{

    int intPaddingLength = contentLength % 8;
    if(intPaddingLength != 0) intPaddingLength = (8 - intPaddingLength);

    System.out.println(" Record Header: " + recordType + ", pad=" + intPaddingLength);

    ostream.write(FCGI_VERSION_1);
    ostream.write(recordType);
    ostream.write(requestID >> 8);
    ostream.write(requestID);
    ostream.write(contentLength >> 8);
    ostream.write(contentLength);
    ostream.write(intPaddingLength); // paddingLength
    ostream.write(0); // reserved

    return intPaddingLength;
  }

  //
  // 3.4 Name-Value Pairs
  //
  int calcParamLength(String name, String value){
    if(name == null || value == null) return 0;

    int nameLength = name.length();
    int valueLength = value.length();

    if(nameLength < 0x80){
      if(valueLength < 0x80){
        // FCGI_NameValuePair11
        return nameLength + valueLength + 2;
      }else{
        // FCGI_NameValuePair14
        return nameLength + valueLength + 5;
      }
    }else{
      if(valueLength < 0x80){
        // FCGI_NameValuePair41
        return nameLength + valueLength + 5;
      }else{
        // FCGI_NameValuePair44
        return nameLength + valueLength + 8;
      }
    }
  }
  void sendParam(int requestID, String name, String value) throws IOException{
    if(name == null || value == null) return;

    int nameLength = name.length();
    int valueLength = value.length();

    if(nameLength < 0x80){
      if(valueLength < 0x80){
        // FCGI_NameValuePair11
        ostream.write(nameLength);
        ostream.write(valueLength);
      }else{
        // FCGI_NameValuePair14
        ostream.write(nameLength);
        ostream.write(0x80 | valueLength >> 24);
        ostream.write(valueLength >> 16);
        ostream.write(valueLength >> 8);
        ostream.write(valueLength);
      }
    }else{
      if(valueLength < 0x80){
        // FCGI_NameValuePair41
        ostream.write(0x80 | nameLength >> 24);
        ostream.write(nameLength >> 16);
        ostream.write(nameLength >> 8);
        ostream.write(nameLength);
        ostream.write(valueLength);
      }else{
        // FCGI_NameValuePair44
        ostream.write(0x80 | nameLength >> 24);
        ostream.write(nameLength >> 16);
        ostream.write(nameLength >> 8);
        ostream.write(nameLength);
        ostream.write(0x80 | valueLength >> 24);
        ostream.write(valueLength >> 16);
        ostream.write(valueLength >> 8);
        ostream.write(valueLength);
      }
    }
    ostream.write(name.getBytes());
    ostream.write(value.getBytes());
  }

  //
  // 5. Application Record Types
  //

  //
  // 5.1 FCGI_BEGIN_REQUEST
  //
  public void sendBeginRequest(int requestID, boolean keepalive) throws IOException{

    sendRecordHeader(requestID, FCGI_BEGIN_REQUEST, 8);

    final int role = FCGI_RESPONDER;
    ostream.write(role >> 8);
    ostream.write(role);
    ostream.write(keepalive ? FCGI_KEEP_CONN : 0);

    for(int i = 0; i < 5; i++) ostream.write(0); // padding = 5
  }

  //
  // 5.2 Name-Value Pair Stream: FCGI_PARAMS
  //
  public void sendParams(int requestID, Map params) throws IOException{
    int intParamsLength = 0;

    for(Map.Entry entry : params.entrySet()){
      intParamsLength += calcParamLength(entry.getKey(), entry.getValue());
    }

    System.out.println("FCGI_PARAMS: length=" + intParamsLength);

    int pad = sendRecordHeader(requestID, FCGI_PARAMS, intParamsLength);

    for(Map.Entry entry : params.entrySet()){
      sendParam(requestID, entry.getKey(), entry.getValue());
    }

    for(int i = 0; i < pad; i++) ostream.write(0); // padding
    ostream.flush();
  }

  //
  // 5.3 Byte Streams: FCGI_STDIN
  //
  public void sendStdin(int requestID, byte[] body) throws IOException{
    System.out.println("FCGI_STDIN: length=" + body.length);

    int pad = sendRecordHeader(requestID, FCGI_STDIN, body.length);

    ostream.write(body, 0, body.length);

    for(int i = 0; i < pad; i++) ostream.write(0); // padding
    ostream.flush();
  }

  //
  // 5.3 Byte Streams: FCGI_STDOUT, FCGI_STDERR, 5.5 FCGI_END_REQUEST
  //
  public int[] recvStdoutStderrAndWaitEndRequest(OutputStream stdoutStream, OutputStream stderrStream)
      throws IOException{

    int[] appStatAndProtStat= new int[2];

    int version, a;
    long b;
    int recordType = -1;
    int requestID = -1;
    int contentLength = 0;
    int paddingLength = 0;
    while(true){
      if(recordType < 0){
        version = istream.read();
        if(version < 0) break;
        if(version != FCGI_VERSION_1){
          System.err.println("recv record version error: " + version);
          break;
        }
        recordType = istream.read();
        requestID = (istream.read() << 8) + istream.read();
        contentLength = (istream.read() << 8) + istream.read();
        paddingLength = istream.read();
        istream.read(); // reserved
      }

      switch (recordType) {
      case FCGI_STDOUT:
        System.out.println("FCGI_STDOUT: requestID=" + requestID +
            ", contentLength=" + contentLength + ", paddingLength=" + paddingLength);
        byte[] bufstdout = new byte[contentLength];
        a = istream.read(bufstdout, 0, contentLength);
        stdoutStream.write(bufstdout, 0, a);
        contentLength -= a;
        if(contentLength == 0){
          b = istream.skip(paddingLength);
          paddingLength -= b;
          if(paddingLength == 0){
            recordType = -1;
            System.out.println("FCGI_STDOUT: done");
          }
        }
        break;
      case FCGI_STDERR:
        System.out.println("FCGI_STDERR: requestID=" + requestID +
            ", contentLength=" + contentLength + ", paddingLength=" + paddingLength);
        byte[] bufstderr = new byte[contentLength];
        a = istream.read(bufstderr, 0, contentLength);
        stderrStream.write(bufstderr, 0, a);
        contentLength -= a;
        if(contentLength == 0){
          b = istream.skip(paddingLength);
          paddingLength -= b;
          if(paddingLength == 0){
            recordType = -1;
            System.out.println("FCGI_STDERR: done");
          }
        }
        break;
      case FCGI_END_REQUEST:
        appStatAndProtStat[0] = (istream.read() << 24) + (istream.read() << 16) +
            (istream.read() << 8) + istream.read(); // appStatus
        appStatAndProtStat[1] = istream.read();  // protocolStatus
        istream.skip(3);
        recordType = -1;
        System.out.println("FCGI_END_REQUEST: requestID=" + requestID +
            ", appStatus=" + appStatAndProtStat[0] + ", protocolStatus=" + appStatAndProtStat[1]);
        break;
      default:
        System.err.println("recv record type error: " + recordType);
        break;
      }
    }
    return appStatAndProtStat;
  }
}


ubuntu 14.04LTSで試す


ubuntu 14.04LTSでのお試しは、FastCGI Server (Application) にPHP-FPM、それが動いているかの確認のためのFast CGI Client (Web Server) にnginxで。

1. PHP-fpmとnginxをインストール

sudo apt-get install nginx php5-fpm

2. 気にするファイルは

2-1. PHP-FPMの設定から、/etc/php5/fpm/pool.d/www.conf
listen = /var/run/php5-fpm.sock 
↓
listen = 127.0.0.1:9000
こんな感じ で、TCP Socketに変更。JavaはUnix Socket不得意なので。

2-2. nginxの設定から、/etc/nginx/sites-available/defaultの、PHPっぽいところを、
        location ~ \.php$ {
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass 127.0.0.1:9000;
        }

2-3. PHPのプログラムを、/usr/share/nginx/html/test.phpなどに、「<?php echo 'hello fastCGI PHP'; ?>」みたいな感じで。(なぜか半角で書くとblogger上で消えちゃうので全角で書いておきます。試す人は、もちろん半角で。)

3. ブラウザ経由、nginx経由で、確認
PHP-fpmとnginxを起動して、
/etc/init.d/php5-fpm start
/etc/init.d/nginx start
ブラウザで、http://サーバー/test.phpを見ると、「hello fastCGI PHP」 と表示されます。

4. ゆるいJavaのFastCGIクライアントプログラムで試す。 ここで、Javaはopenjdk-7を利用しました。(もちろんこっちはnginxは止めてもOK)
javac FastCGIConnection.java
java FastCGIConnection

期待される結果は、
FastCGI client sample
FCGI_STDOUT: requestID=123, contentLength=80, paddingLength=0
FCGI_STDERR: requestID=123,appStatus=0,protocolStatus=0
STDOUT: [X-Powered-By: PHP/5.5.9-1ubuntu4.5
Content-type: text/html

hello fastCGI PHP]
STDERR: []

mainがsampleなので、ハードコーディングされているのを変えて試したりして遊べるかも。

--
以上

1 件のコメント:


  1. awesome blog you shared with us and its very helpful for my training institute candidates. keep update more techniques.
    Hadoop training in Chennai

    返信削除