3

Tomcat 6.0.36 と JRE 1.5.0 を使用しており、Windows 7 で開発作業を行っています。

私が行っているいくつかの作業の概念実証として、Java コードから、XML をソケット経由でサーブレットに HTTP 投稿しています。その後、サーブレットは xml をエコー バックします。最初の実装では、両端の入力ストリームを XML ドキュメント ファクトリに渡して、ネットワーク経由で送信された xml を抽出していました。これはサーブレットでは問題なく機能しましたが、クライアント側では失敗しました。クライアント側で失敗したことが判明しました。応答全体が到着する前にドキュメント ファクトリがタイムアウトし、例外がスローされるまで応答の読み取りがブロックされたためです。(以下で説明するように、ドキュメント ファクトリを使用しなくても同じブロッキングの問題が発生するため、ドキュメント ファクトリの動作は意味がありません。)

このブロッキングの問題を解決するために、クライアント側のコードとサーブレットのより単純なバージョンを考え出しました。この単純なバージョンでは、式からドキュメント ビルダーを削除しました。両側のコードは、それぞれの入力ストリームからテキストを読み取るだけです。

残念ながら、私はまだ応答でこのブロックの問題を抱えており、以下で説明するように、単純に response.flushBuffer() を呼び出すだけでは解決されませんでした。Google 検索では、関連するトピックが 1 つしか見つかりませんでしたが ( Tomcat は応答バッファーをフラッシュしません)、これはまったく同じ問題ではありませんでした。

私は自分のコードを含め、以下に正確な問題を説明しました。

これが私のサーブレット コードです (覚えておいてください、これは最低限の概念実証コードであり、運用コードではありません)。

import java.io.InputStreamReader;
import java.io.LineNumberReader;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public final class EchoXmlServlet extends HttpServlet {

    public void init(ServletConfig config) throws ServletException {
        System.out.println("EchoXmlServlet loaded.");
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException {
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException {

        try {
            processRequest(request, response);
        }
        catch(Exception e) {
            e.printStackTrace();
            throw new ServletException(e);
        }

        System.out.println("Response sent.");

        return;
    }

    private final void processRequest(HttpServletRequest request, final HttpServletResponse response) throws Exception {

        String line = null;

        StringBuilder sb = new StringBuilder();
        LineNumberReader lineReader = new LineNumberReader(new InputStreamReader(request.getInputStream(), "UTF-8"));

        while((line = lineReader.readLine()) != null) {

            System.out.println("line: " + line);

            sb.append(line);
            sb.append("\n");
        }

        sb.append("An additional line to see when it turns up on the client.");

        System.out.println(sb);

        response.setHeader("Content-Type", "text/xml;charset=UTF-8");
        response.getOutputStream().write(sb.toString().getBytes("UTF-8"));

        // Some things that were tried.
        //response.getOutputStream().print(sb.toString());
        //response.getOutputStream().print("\r\n");
        //response.getOutputStream().flush();
        //response.flushBuffer();
    }

    public void destroy() {
    }

}

これが私のクライアント側のコードです。

import java.io.BufferedOutputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.net.Socket;

public final class SimpleSender {

    private String host;
    private String path;
    private int port;

    public SimpleSender(String host, String path, int port) {

        this.host = host;
        this.path = path;
        this.port = port;

    }

    public void execute() {

        Socket connection = null;
        String line;

        try {
            byte[] xmlBytes = getXmlBytes();
            byte[] headerBytes = getHeaderBytes(xmlBytes.length);

            connection = new Socket(this.host, this.port);

            OutputStream outputStream = new BufferedOutputStream(connection.getOutputStream());
            outputStream.write(headerBytes);
            outputStream.write(xmlBytes);
            outputStream.flush();

            LineNumberReader lineReader
                = new LineNumberReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));

            while((line = lineReader.readLine()) != null) {
                System.out.println("line: " + line);
            }

            System.out.println("The response is read.");
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        finally {
            try {
                connection.close();
            }
            catch(Exception e) {}
        }
    }

    private byte[] getXmlBytes() throws Exception {

        StringBuffer sb = null;

        sb = new StringBuffer()
            .append("<my-xml>\n")
            .append("Hello to myself.\n")
            .append("</my-xml>\n");

        return sb.toString().getBytes("UTF-8");
    }

    private byte[] getHeaderBytes(int contentLength) throws Exception {

        StringBuffer sb = null;

        sb = new StringBuffer()
            .append("POST ")
            .append(this.path)
            .append(" HTTP/1.1\r\n")
            .append("Host: ")
            .append(this.host)
            .append("\r\n")
            .append("Content-Type: text/xml;charset=UTF-8\r\n")
            .append("Content-Length: ")
            .append(contentLength)
            .append("\r\n")
            .append("\r\n");

        return sb.toString().getBytes("UTF-8");
    }

}

SimpleSender.execute() の呼び出しを介してリクエストがサーブレットに送信されると、リクエストを受信するサーブレット内のコードは問題なく xml を読み取ります。私のサーブレット コードも、問題なく processRequest() と doPost() から終了します。これは、サーバー上の即時の (つまり、出力行間にブロックがない) 出力です。

line: <my-xml>
line: Hello to myself.
line: </my-xml>
<my-xml>
Hello to myself.
</my-xml>
An additional line to see when it turns up on the client.
Response sent.

上記の出力は、まさに期待どおりです。

ただし、クライアント側では、コードは次のブロックを出力します。

HELLO FROM MAIN
line: HTTP/1.1 200 OK
line: Server: Apache-Coyote/1.1
line: Content-Type: text/xml;charset=UTF-8
line: Content-Length: 74
line: Date: Sun, 18 Nov 2012 23:58:43 GMT
line:
line: <my-xml>
line: Hello to myself.
line: </my-xml>

約 20 秒のブロッキングの後 (時間計測しました)、クライアント側で次の行が出力されます。

line: An additional line to see when it turns up on the client.
The response is read.
GOODBYE FROM MAIN

クライアント側でブロッキングが発生している間、サーバー側の出力全体が完全に表示されることに注意してください。

そこから、サーバー側でフラッシュして、この問題を修正しようとしました。response.flushBuffer() と response.getOutputStream().flush() という 2 つのフラッシュ方法を個別に試しました。フラッシュの両方の方法で、クライアント側でまだブロックがありましたが (ただし、応答の別の部分で)、他の問題もありました。これがクライアントがブロックした場所です。

HELLO FROM MAIN
line: HTTP/1.1 200 OK
line: Server: Apache-Coyote/1.1
line: Content-Type: text/xml;charset=UTF-8
line: Transfer-Encoding: chunked
line: Date: Mon, 19 Nov 2012 00:21:53 GMT
line:
line: 4a
line: <my-xml>
line: Hello to myself.
line: </my-xml>
line: An additional line to see when it turns up on the client.
line: 0
line: 

20秒ほどブロックした後、クライアント側で以下が出力され、

The response is read.
GOODBYE FROM MAIN

この出力には、クライアント側で 3 つの問題があります。まず、応答の読み取りはまだブロックされています。応答の別の部分の後でブロックしているだけです。次に、予期しない文字 ("4a"、"0") が返されました。最後に、ヘッダーが変更されました。Content-Length ヘッダーを失い、「Transfer-encoding: chunked」ヘッダーを取得しました。

したがって、フラッシュがないと、最終行を送信して応答を終了する前に、応答がブロックされます。ただし、フラッシュを使用すると、応答は引き続きブロックされますが、不要な文字が取得され、不要なヘッダーが変更されます。

Tomcat では、コネクタにデフォルトの定義があり、

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

connectionTimeout は 20 秒に設定されています。これを 10 秒に変更すると、クライアント側のコードが 20 秒ではなく 10 秒ブロックされました。そのため、Tomcat によって管理されている接続タイムアウトが原因で、応答が完全にフラッシュされて終了したようです。

応答が終了したことを示すために、サーブレット コードで何か追加する必要がありますか?

最終行と終了インジケータを送信する前に、私の応答がブロックされている理由について誰か提案がありますか?

フラッシュが不要な文字を送信する理由と、フラッシュ後に応答がまだブロックされている理由について、誰か提案がありますか?

時間があれば、この投稿に含まれているコードを実行してみて同じ問題が発生するかどうか教えていただけますか?

編集 - Guido の最初の返信に応じて

グイド、

お返事ありがとうございます。

readLine を使用してメッセージの本文を読み取っているため、クライアントがブロックされています。本文が改行で終わっていないため、readLine がハングする

いいえ、そうではないと思います。まず、コードの元のバージョンでは、クライアント側でもサーバー側でもライン リーダーを使用していませんでした。両側で、ストリームを xml ドキュメント ファクトリに渡して、ストリームから読み取らせました。サーバーでは、これは正常に機能しました。クライアントでは、タイムアウトしました。(クライアントでは、ストリームをドキュメント ファクトリに渡す前に、ヘッダーの最後まで読んでいました。)

次に、ライン リーダーを使用しないようにクライアント コードを変更しても、ブロックが発生します。これは、ライン リーダーを使用しないバージョンの SimpleSender.execute() です。

public void execute() {

    Socket connection = null;
    int byteCount = 0;

    try {
        byte[] xmlBytes = getXmlBytes();
        byte[] headerBytes = getHeaderBytes(xmlBytes.length);

        connection = new Socket(this.host, this.port);

        OutputStream outputStream = new BufferedOutputStream(connection.getOutputStream());
        outputStream.write(headerBytes);
        outputStream.write(xmlBytes);
        outputStream.flush();

        while(connection.getInputStream().read(new byte[1]) >= 0) {
            ++byteCount;
        }

        System.out.println("The response is read: " + byteCount);
    }
    catch(Exception e) {
        e.printStackTrace();
    }
    finally {
        try {
            connection.close();
        }
        catch(Exception e) {}
    }

    return;
}

上記のコードブロックは、

HELLO FROM MAIN

20 秒後に終了し、

The response is read: 235
GOODBYE FROM MAIN

上記は、クライアント側でラインリーダーを使用することに問題がないことを最終的に示していると思います。

sb.append("An additional line to see when it turns up on the client.\n");

上記の行に return を追加すると、ブロックが 1 行遅れるだけです。OPの前にこれをテストしましたが、もう一度テストしました。

If you want to do your own HTTP parser, you have to read through the headers until you get two blank lines.

はい、私はそれを知っていますが、この不自然な単純な例では、それは議論の余地があります. クライアントでは、返された HTTP メッセージ、ヘッダーなどを単純に出力しています。

Then you need to scan the headers to see if you had a Content-Length header. If there is no Content-Length then you are done. If there is a Content-Length you need to parse it for the length, then read exactly that number of additional bytes from the stream. This allows HTTP to transport both text data and also binary data which has no line feeds.

はい、すべて真実ですが、この不自然な単純な例では関係ありません。

I recommend you replace the guts of your client code HTTP writer/parse with a pre-written client library that handles these details for you.

私は完全に同意します。私は実際には、ストリームの処理を xml ドキュメント ファクトリに渡すことを望んでいました。ブロックの問題に対処する方法として、Apache commons-httpclient も調べました。新しいバージョン (httpcomponents) では、ポスト リターンのストリームを処理することは開発者に任されているため (私が知る限り)、それは役に立ちませんでした。別のライブラリを提案できれば、私は確かに興味があります。

私はあなたの意見に同意しませんでしたが、あなたの返信に感謝します。私の意見の相違による不快感や否定的な暗示は意味しません. 私は明らかに何か間違ったことをしている、またはすべきことをしていませんが、ラインリーダーが問題だとは思いません。さらに、私がフラッシュした場合、それらのファンキーなキャラクターはどこから来るのでしょうか? クライアント側でライン リーダーが使用されていない場合にブロッキングが発生するのはなぜですか?

また、Jetty で問題を再現しました。したがって、これは明らかに Tomcat の問題ではなく、「私」の問題です。私は何か間違ったことをしていますが、それが何であるかわかりません。

4

2 に答える 2

1

笑 わかりました、私は何か間違ったことをしていました(省略による)。私の問題の解決策は? 次のヘッダーを http リクエストに追加します。

Connection: close

それは簡単です。それがなければ、接続は生き続けていました。私のコードはサーバーが終了したことを示すことに依存していましたが、サーバーは開いている接続を閉じるのではなく、まだリッスンしていました。

ヘッダーにより、サーバーは応答の書き込みを終了すると接続を閉じます (これは、doPost(...) への呼び出しが返されたときに示されると思います)。

補遺

flush() が呼び出されたときのファンキーな文字について...

Connection: close を使用しているため、私のサーバー コードは、flush() を呼び出しません。ただし、送り返されるコンテンツが十分に大きい場合 (Tomcat コネクタのバッファ サイズよりも大きいと思われます)、ファンキーな文字が返され、ヘッダー 'Transfer-Encoding: chunked' が応答に表示されます。

これを修正するために、応答を書き込む前に、サーバー側で明示的に response.setContentLength(...) を呼び出します。これを行うと、Content-Length ヘッダーが Transfer-Encoding: チャンクの代わりに応答に含まれ、ファンキーな文字が表示されません。

コードが機能するようになったので、これ以上時間を費やすつもりはありませんが、ファンキーな文字がチャンク区切り文字だったのではないかと思います。コンテンツの長さを明示的に設定すると、チャンク区切り文字は不要になりました。

于 2012-11-19T06:51:27.703 に答える