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から読み込んでもいい気がするけど、まぁ。