StreamReaderとキャリッジリターン

StreamReaderのReadLine/ReadLineAsyncメソッド、まぁ便利です。
改行コードがLF/CR/CR+LFどれであっても、いい感じに読み込んでくれます。

が、…このメソッド、CRが来るだけでは処理を返してくれません!!!!
CRを読み込んだ後、他の文字が来るか、もしくはもうEOFだということがわかるまで、なんと処理を返さず待機するのです!

なんでやねん!CR読み込んだ時点で1行確定でしょ!!すぐに返してよ!!!!

この動作、テキストファイル等、すでにすべてのデータが揃っているストリームに対しては問題無いですね。
ですが、例えばネットワークなどでCRを区切りとしている系のAPIだと、応答が返ってきているはずなのに何も来ない…とか、1回分遅れて応答が帰ってくるように見える…ということが発生します。
CRの次の文字が来るまで返さないので、CRが来ただけでは、最新の1行分の応答がStreamReaderにバッファされたままになるのです。

先日、PJLinkのC#ラッパーを書いていてこの問題にぶち当たりました。
PJLinkとは、プロジェクタ等をLAN経由で操作するための標準仕様ですが、コマンドや応答の終端をCRとすることになっているのです。
つまり、
→コマンド1を送る
(←コマンド1の応答が返ってくるけどStreamReaderにバッファされるだけ
→コマンド2を送る
←コマンド1の応答がやっと返される(そしてコマンド2の応答はStreamReaderにバッファされる)
みたいな。

しかたがないので、改行コードがCRな1行を読み込んだら、すぐに返すクラスを書いてみた。
長いけどこんなん。

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace ShComp
{
    /// <summary>
    /// 改行コードがCRの場合に、1行を読み込んですぐに応答を返すメソッドを提供します。
    /// </summary>
    class CRLineReader : IDisposable
    {
        private StreamReader _reader;
        private bool _isEof;
        private int _bufferStart;
        private int _bufferEnd;
        private char[] _buffer;

        public CRLineReader(Stream stream, int capacity = 136)
        {
            _reader = new StreamReader(stream);
            _buffer = new char[capacity];
        }

        /// <summary>
        /// CR区切りの1行を非同期に読み込みます。CR以外の改行コードは、改行コードとみなしません。
        /// </summary>
        public async Task<string> ReadLineAsync()
        {
            if (_isEof) return null;

            var sb = new StringBuilder();

            if (_bufferEnd > _bufferStart)
            {
                if (AppendFromBuffer(sb))
                {
                    return sb.ToString();
                }
            }

            _bufferStart = 0;

            while (true)
            {
                _bufferEnd = await _reader.ReadAsync(_buffer, 0, _buffer.Length);

                if (_bufferEnd == 0)
                {
                    _isEof = true;
                    return sb.ToString();
                }

                if (AppendFromBuffer(sb))
                {
                    return sb.ToString();
                }
            }
        }

        private bool AppendFromBuffer(StringBuilder sb)
        {
            for (int i = _bufferStart; i < _bufferEnd; i++)
            {
                if (_buffer[i] == 'r')
                {
                    sb.Append(_buffer, _bufferStart, i - _bufferStart);
                    _bufferStart = i + 1;
                    return true;
                }
            }

            sb.Append(_buffer, _bufferStart, _bufferEnd - _bufferStart);
            return false;
        }

        public void Dispose()
        {
            _reader.Dispose();
        }
    }
}

バッファサイズが136なのは、PJLinkの応答の最大長が136だから。
PJLink、ASCIIな文字しか使わないから、StreamReader介さず直接Streamから読み込んでもいい気がするけど、まぁ。

カテゴリー: プログラミング タグ: , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です