カスタムソケットタイプの作成


データをソケットに送る前やソケットから受け取ったあとで、そのデータを処理することが必要な場合がよくあります。前後にデータを処理して java.net.Socket ソケットを使う代わりに、別のタイプのソケットを使うこともできます。

このページでは、カスタムソケットタイプの作成手順を説明し、また、データを圧縮して送信する CompressionSocket という名前のカスタムソケットの実装例を示します。

CompressionSocket クラスとそれに関連するクラスは、「カスタム RMI ソケットファクトリの作成」のチュートリアルでも使用されています。そのため、この例のソースファイルはすべて examples.rmisocfac パッケージにあります。

このチュートリアルでは、CompressionSocket クラスとそれに関連するクラスの作成は、次の 4 つのステップに分かれます。 各ステップは、他のカスタムソケットタイプを作成する場合にも当てはまります。

  1. java.io.FilterOutputStream を継承してソケットの出力ストリームを作成する。必要に応じて、FilterOutputStream メソッドをオーバーライドする
  2. java.io.FilterInputStream を継承してソケットの入力ストリームを作成する。必要に応じて、FilterInputStream メソッドをオーバーライドする
  3. java.net.Socket のサブクラスを作成し、適切なコンストラクタを実装してから、getInputStreamgetOutputStreamclose メソッドをオーバーライドする
  4. java.net.ServerSocket のサブクラスを作成し、コンストラクタを実装してから、目的のタイプのソケットを作成するために accept メソッドをオーバーライドする

ステップ 1:
FilterOutputStream を継承してソケットの出力ストリームを作成する

データを圧縮するソケットを作成するために、FilterOutputStream クラスを継承する CompressionOutputStream という名前のクラスを作成します。ただし、どんな場合でも FilterOutputStream の継承が適切だとは限りません。一般的に、実装しようとするソケットのタイプにもっとも適したタイプの出力ストリームを継承すべきです。この例では、FilterOutputStream が最適です。

この例では、最大 62 種の共通文字を 6 ビットに符号化し、その他の文字を 18 ビットに符号化する簡単なアルゴリズムを使います (JavaTM プログラミング言語では、文字は通常 16 ビット)。62 種の共通文字を 6 ビットの数値 (0 から 61) にマップするルックアップテーブルを使います。 符号化した文字には、ルックアップの結果に応じて 6 ビットの符号化か 18 ビットの符号化かを示す定数を付けます。最後に、符号化した文字をストリームに書き込みます。このアルゴリズムでは、出現する文字はすべて ASCII で、各文字の高位のバイトは使用しないと仮定します。

注: 実際のアプリケーションでのデータ圧縮に使用することはお勧めしません。このアルゴリズムは簡単な使用例を示す目的で、実用を目的としていません

カスタムソケットの作成方法を示すサンプルコードを記述する前に、入力ストリームと出力ストリームのクラス間で共有される情報を含むインタフェースを記述する必要があります。 このため、このアルゴリズムでは、共通文字のルックアップテーブル codeTable、および 3 つの定数をメンバとします。この例ではインタフェースを使いますが、インタフェースが必要でない場合もあるので注意してください。

interface CompressionConstants {
 
    /** Constants for 6-bit code values. */

    /** No operation: used to pad words on flush. */
    static final int NOP     = 0;  

    /** Introduces raw byte format. */
    static final int RAW     = 1;  

    /** Format indicator for characters found in lookup table. */
    static final int BASE    = 2;  

    /** A character's code is it's index in the lookup table. */
    static final String codeTable =
        "abcdefghijklmnopqrstuvwxyz" +
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ ,.!?\"'()";
}
	

CompressionConstants インタフェースのソースコードを見るには、ここをクリックしてください。

これで、入力ストリームと出力ストリームの間の共有情報のインタフェースが定義できました。 次に、ステップ 1 の残りの部分として、ソケットの出力のストリームを作成するために java.io.FilterOutputStream を継承するクラスを記述します。 必要に応じて FilterOutputStream メソッドをオーバーライドします。

次は、CompressionOutputStream.java クラスのソースコードです。圧縮アルゴリズムについては、ソースコードのコメントの中で説明します。実装については、コードのあとで説明します。

import java.io.*;
 
class CompressionOutputStream extends FilterOutputStream
    implements CompressionConstants
{

    /*
     * Constructor calls constructor of superclass.
     */
    public CompressionOutputStream(OutputStream out) {
        super(out);
    }
 
    /* 
     * Buffer of 6-bit codes to pack into next 32-bit
     * word.  Five 6-bit codes fit into 4 words. 
     */
    int buf[] = new int[5];
 
    /*
     * Index of valid codes waiting in buf. 
     */
    int bufPos = 0;
 

    /*  
     * This method writes one byte to the socket stream. 
     */ 
    public void write(int b) throws IOException {
        // force argument to one byte
        b &= 0xFF;                      
 
        // Look up pos in codeTable to get its encoding. 
        int pos = codeTable.indexOf((char)b);

        if (pos != -1)
            // If pos is in the codeTable, write 
            // BASE + pos into buf. By adding BASE 
            // to pos, we know that the characters in
            // the codeTable will always have a code 
            // between 2 and 63 inclusive. This allows 
            // us to use RAW (RAW is equal to 1) to signify 
            // that the next two groups of 6-bits are necessary
            // for decompression of the next character. 

            writeCode(BASE + pos);

        else {
            // Otherwise, write RAW into buf to signify that
            // the Character is being sent in 12 bits.
            writeCode(RAW);

            // Write the last 4 bits of b into the buf.
            writeCode(b >> 4);

            // Truncate b to contain data in only the first 4
            // bits and write the first 4 bits of b into buf.
            writeCode(b & 0xF);
        }
    }
 
    /* 
     * This method writes up to len bytes to the socket stream. 
     */
    public void write(byte b[], int off, int len) 
        throws IOException 
    {
        // This implementation is quite inefficient because 
        // it has to call the other write method for every 
        // byte in the array.  It could be optimized for 
        // performance by doing all the processing in this 
        // method.

        for (int i = 0; i < len; i++)
            write(b[off + i]);
    }
 
   /* 
    * Clears buffer of all data (zeroes it out). 
    */ 
   public void flush() throws IOException {
        while (bufPos > 0)
            writeCode(NOP);
    }
 
    /* 
     * This method actually puts the data into the output stream
     * after packing the data from all 5 bytes in buf into one
     * word. Remember, each byte has, at most, 6 significant bits.
     */
    private void writeCode(int c) throws IOException {
        buf[bufPos++] = c;

        // write next word when we have 5 codes
        if (bufPos == 5) {      
            int pack = (buf[0] << 24) | (buf[1] << 18) | 
                       (buf[2] << 12) | (buf[3] << 6) | buf[4];
            out.write((pack >>> 24) & 0xFF);
            out.write((pack >>> 16) & 0xFF);
            out.write((pack >>> 8)  & 0xFF);
            out.write((pack >>> 0)  & 0xFF);
            bufPos = 0;
        }
    }
}
	

まず、CompressionOutputStreamFilterOutputStream のサブクラスです。次に、ルックアップテーブルと定数にアクセスできるよう CompressionConstants インタフェースを実装します。

データを圧縮するため、FilterOutputStreamwrite メソッドを CompressionOutputStream でオーバーライドします。また、次のメソッドに注目してください。

public void write(int b)

このメソッドは、1 回呼び出されるごとに 1 文字を書き込みます。 そのとき圧縮定数 (RAW または BASE) を使って各文字に符号化形式を付け、必要なら文字を 2 つの 4 ビット部分に分割します。

また、次のメソッドに注目してください。

public void write(byte b[], int off, int len)

このメソッドは、len で指定した数の文字を書き込むために使用します。このメソッドは 1 回に 1 文字を書き込む write メソッドを len 回呼び出します。

writeCode メソッドは、6 ビットコードを 1 単語 (最大 5 文字を符号化可能) にパックし、その単語を出力ストリームに書き込みます。

CompressionOutputStream のソースコードを見るには、ここをクリックしてください。


ステップ 2:
FilterInputStream を継承してソケットの入力ストリームを作成する

データを圧縮する出力ストリームが完成しました。 次に、圧縮したデータを解凍する入力ストリームを実装します。ソースコードと説明からわかるように、CompressionInputStream の作成方法は CompressionOutputStream の作成方法とよく似ています。

また復号化処理は、符号化処理の逆のアルゴリズムになります。

import java.io.*

class CompressionInputStream extends FilterInputStream
    implements CompressionConstants
{
    /*
     * Constructor calls constructor of superclass
     */
    public CompressionInputStream(InputStream in) {
        super(in);
    }
 
    /* 
     * Buffer of unpacked 6-bit codes 
     * from last 32 bits read.
     */
    int buf[] = new int[5];
 
    /*
     * Position of next code to read in buffer (5 signifies end). 
     */ 
    int bufPos = 5;
 
    /*
     * Reads in format code and decompresses character
     * accordingly.  
     */
    public int read() throws IOException {
        try {
            int code;

            // Read in and ignore empty bytes (NOP's) as 
            // long as they arrive. 
            do {
                code = readCode();
            } while (code == NOP);      
 
            if (code >= BASE) {
                // Retrieve index of character in codeTable 
                // if the code is in the correct range.

                return codeTable.charAt(code - BASE);
            } else if (code == RAW) {
                // read in the lower 4 bits and the 
                // higher 4 bits, and return the 
                // reconstructed character
                int high = readCode();
                int low = readCode();
                return (high << 4) | low;
            } else 
                throw new IOException("unknown compression code: " 
                                      + code);
        } catch (EOFException e) {
            // Return the end of file code
            return -1;
        }
    }
 
    /* 
     * This method reads up to len bytes from the input stream.
     * Returns if read blocks before len bytes are read. 
     */ 
    public int read(byte b[], int off, int len) 
        throws IOException 
    {
        if (len <= 0) {
            return 0;
        }
 
        // Read in a word and return -1 if no more data.
        int c = read();
        if (c == -1) {
            return -1;
        }

        // Save c in buffer b
        b[off] = (byte)c;
 
        int i = 1;
        // Try to read up to len bytes or until no
        // more bytes can be read without blocking.
        try {
            for (; (i < len) && (in.available() > 0); i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                if (b != null) {
                    b[off + i] = (byte)c;
                }
            }
        } catch (IOException ee) {
        }
        return i;
    }

    /*
     * If there is no more data to decode left
     * in buf, read the next four bytes from the 
     * wire. Then store each group of 6 bits in an
     * element of buf.  Return one element of buf.
     */
    private int readCode() throws IOException {
        // As soon as all the data in buf has been read
        // (when bufPos == 5) read in another four bytes.
        if (bufPos == 5) {
            int b1 = in.read();
            int b2 = in.read();
            int b3 = in.read();
            int b4 = in.read();

            // make sure none of the bytes signify the
            // end of the data in the stream
            if ((b1 | b2 | b3 | b4) < 0)
                throw new EOFException();
            // Assign each group of 6 bits to an 
            // element of buf.
            int pack = (b1 << 24) | (b2 << 16) | 
                       (b3 << 8) | b4;
            buf[0] = (pack >>> 24) & 0x3F;
            buf[1] = (pack >>> 18) & 0x3F;
            buf[2] = (pack >>> 12) & 0x3F;
            buf[3] = (pack >>>  6) & 0x3F;
            buf[4] = (pack >>>  0) & 0x3F;
            bufPos = 0;
        }
        return buf[bufPos++];
    }
}

入力ストリームを記述するときは、ストリームからデータを取り込むメソッドが必要です。したがって、コンストラクタに加えて、FilterOutputStream の 2 つの read メソッドがオーバーライドされます。また、次のメソッドに注目してください。

public int read()

このメソッドは、1 回呼び出されるごとに 1 文字を読み出し、writeCode メソッドによってパックされた 6 ビットコードを復号化します。

また、次のメソッドに注目してください。

public int read(byte b[], int off, int len)

このメソッドは、最大 len バイトを読み取って配列 b に書き込みます。 これは、1 回の呼び出しで 1 文字を読み取る read メソッドを最大 len 回呼び出すことによって行います。このメソッドは、len バイトの読み取りが終了するか、ファイルの最後まで読み取ったときに復帰します。 また、ブロッキングなしにデータが読み取れなくなった場合にも、すぐに復帰します。

readCode メソッドは CompressionOutputStreamwriteCode メソッドに対応しており、ストリームからデータを読み取って 4 バイトのグループを 5 つの 6 ビットコードに復元します。この 6 ビットコードは、read によって復号化されます。

CompressionInputStream.java のソースコードを見るには、ここをクリックしてください。


ステップ 3:
Socket のサブクラスを作成し、適切なコンストラクタを実装してから、getInputStreamgetOutputStreamclose メソッドをオーバーライドする

これで、CompressionInputStream クラスと CompressionOutputStream クラスの実装が終わりました。 次に、これらの圧縮ストリームを使って通信するソケットを実装します。このサブクラスは java.net.Socket クラスを継承します。

次は、CompressionSocket クラスのソースコードです。コードに続いて、クラスについて説明します。

import java.io.*;
import java.net.*;
 
class CompressionSocket extends Socket {

    /* InputStream used by socket */
    private InputStream in;
    /* OutputStream used by socket */
    private OutputStream out;

    /* 
     * No-arg constructor for class CompressionSocket  
     */
    public CompressionSocket() { super(); }

    /* 
     * Constructor for class CompressionSocket 
     */
    public CompressionSocket(String host, int port) 
        throws IOException 
    {
        super(host, port);
    }

    /* 
     * Returns a stream of type CompressionInputStream 
     */
    public InputStream getInputStream() 
        throws IOException 
    {
        if (in == null) {
            in = new CompressionInputStream(super.getInputStream());
        }
        return in;
    }

    /* 
     * Returns a stream of type CompressionOutputStream 
     */
    public OutputStream getOutputStream() 
        throws IOException 
    {
        if (out == null) {
            out = new CompressionOutputStream(super.getOutputStream());
        }
        return out;
    }

    /*
     * Flush the CompressionOutputStream before 
     * closing the socket.
     */
    public synchronized void close() throws IOException {
        OutputStream o = getOutputStream();
        o.flush();
        super.close();
    }

}
      

データ圧縮を使って通信するソケットを提供するために Socket クラスを継承しているので、次の操作が必要です。

CompressionSocket コンストラクタは、単にスーパークラス java.net.Socket 内の同等のコンストラクタを呼び出します。

getInputStream メソッドは、まだインスタンスの生成が実行されていない場合に、ソケット用の CompressionInputStream を作成し、ストリームへの参照を返します。同様に、getOutputStream メソッドは、必要に応じて CompressionOutputStream を作成し、CompressionSocket で使用するためにこれを返します。

close メソッドは、ソケットが閉じられる前にすべてのデータが確実に送信されるように、背後の CompressionOutputStream をフラッシュします。

CompressionSocket.java のソースコードを見るには、ここをクリックしてください。


ステップ 4:
ServerSocket のサブクラスを作成し、コンストラクタを実装してから、目的のタイプのソケットを作成するために accept メソッドをオーバーライドする

カスタムソケット作成の最後のステップは、カスタムプロトコルをサポートする ServerSocket のサブクラスの作成です。この例では、サブクラスの名前は CompressionServerSocket です。

次は、CompressionServerSocket クラスのソースコードです。 ソースコードに続いて、クラスについて説明します。

import java.io.*;
import java.net.*;
 
class CompressionServerSocket extends ServerSocket {

    public CompressionServerSocket(int port) 
        throws IOException 
    {
        super(port);
    }

    public Socket accept() throws IOException {
        Socket s = new CompressionSocket();
        implAccept(s);
        return s;
    }
}

CompressionSocket の場合と同様に、圧縮プロトコルを使って通信するサーバソケットを作成するには、コンストラクタを実装し、次に、java.net.Socket タイプのソケットを使用するすべてのメソッドを CompressionSocket タイプのソケットを使用するようにオーバーライドする必要があります。

コンストラクタの実装は簡単で、スーパークラスのコンストラクタを呼び出すだけです。

ServerSocket のクラスメソッドのうちオーバーライドする必要のあるものは、accept メソッドだけです。Socket タイプではなく CompressionSocket タイプのソケットのインスタンスを生成するようにオーバーライドします。

このように簡単な記述ができる理由は、このチュートリアルで説明している圧縮ソケットタイプは、TCP の上のプロトコル層だからです。 TCP は java.net.Socketjava.net.ServerSocket がデフォルトで使用するプロトコルです。したがって、圧縮ソケットは他のメソッドに対するものと同一の方法を使い、また類似した接続確立インタフェースを使用します。

CompressionServerSocket.java のソースコードを見るには、ここ をクリックしてください。


Copyright © 1999 Sun Microsystems, Inc. All Rights Reserved.
コメントの送付先: rmi-comments@java.sun.com 
Sun