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なので、ハードコーディングされているのを変えて試したりして遊べるかも。

--
以上

4 件のコメント:


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

    返信削除

  2. Such a nice & useful post. Really happy to see such post. I have come to know about many new ideas. I will try my best to implement some of them. Thanks.
    Still Hunting Method
    Hunting psych tips Survival Tips Travel Touring Tips

    返信削除


  3. it is really helpful. the information in this article is satisfied me. And the information it is very interesting. Thanks for this kind of article.
    travel trekking tips
    see the link Tent Camping 101 Exploring Smithriver

    返信削除