LINUX.ORG.RU

Как правильно вычитывать строки из потока?

 


0

4

Под потоком подразумевается сокет, последовательный порт и т.д..

Есть куча протоколов общения, которые подразумевают общение строками, длина которых заранее не известна. Насколько я знаю, есть 2 подхода. Первый подход заключается в чтении по одному байту до тех пор, пока не встретится конец строки. Второй подход заключается в чтении какого-то произвольного числа байт в некий буфер и уже проход по буферу.

Первый вариант хорош своей простотой, но плох большим количеством вызовов read()/recv(). Второй вариант хорош тем, что число вызовов read()/recv() будет минимально, но плох тем, что можно вычитать более одной строки и тогда вокруг данных после конца строки начинается пляска с бубном.

Как вообще принято реализовывать подобные вещи?

★★

Эм, подхода действительно два но не те что ты указал. Точнее ты угадал один. read()/recv() по 1 байту - это не подход, это максимум временный костыль для черновика/прототипа.

Подхода два: либо перед началом строки передаётся длина и тогда её читать чуть проще, либо строка нуль-терминированная и без буферизации не вздумай в проде её читать. Хотя даже в первом варианте, с длиной заранее, буферизация желательна.

Единственное исключение: тебе надо прочитать какой-то заголовок, а потом отдать сокет другой проге, и нет никакой возможности передать ей уже предпрочитанный буфер с началом её данных. Тогда придётся по 1 байту, но это печаль.

но плох тем, что можно вычитать более одной строки и тогда вокруг данных после конца строки начинается пляска с бубном

Не начинается. Там всё элементарно и общепринято. Если тебя пугает - значит надо набираться опыта.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

перед началом строки передаётся длина

Скажи это http, AT, NMEA и куче других протоколов, которые мне сходу не пришли на ум ;)

Если тебя пугает

Не пугает, просто не нравится рост сложности.

u5er ★★
() автор топика
Ответ на: комментарий от u5er

Не пугает, просто не нравится рост сложности.

Буферизация (и именно своя с явно доступным буфером) в любом случае пригодится, намного удобнее когда ты можешь парсить данные из уже готового буфера, и удалять из него уже «обработанное» в правильный момент, а не одновременно с чтением.

firkax ★★★★★
()
Ответ на: комментарий от firkax

Я так и делаю и распарсенное раскидываю по подстрокам.

На самом деле, в обоих случаях получается свой буфер. Задача прочитать строку. Делать это по одному байту или пакетом сути не меняет - на выходе всё равно будет строка. Вопрос лишь в способе.

u5er ★★
() автор топика

академически это принято называть PAD «packet assembler/disassembler» - штука у которого на одной стороне колбаса байтового потока, а из другой вылезают нарезанные пакеты (в данном случае строки). И наоборот :-)

а как делать - уже на вкус и цвет. В event-driven генеряться события «о ! пакет нарезался». В многопоточных пакет складывается в исходящую очередь. В монолитах заполняется промежуточный буфер и выставляются флаги (код результата), как выше упомянутый fdopen.

опять-же зависит от того как организовано чтение из приёмника. То есть кто главный заводила цикла приложения. Данные вычитываешь по мере их поступления или только когда обрабатываешь..

MKuznetsov ★★★★★
()
Ответ на: комментарий от a1ba

В сорцах андроида есть компонент reference-ril, у которого есть простая читалка AT команд. Вот он и есть твой разбор из последовательного порта, по строкам.

a1ba ★★
()
Ответ на: комментарий от rupert

Зачем так извращаться. Стандартный getline уже в строчку читает.

зачем-то так извращались :-) не знаю..возможно то была отрыжка UML-проектирования. «реализуем прямой позитивный сценарий, вот прямо по начертанной свыше схеме, а все ветвления и отклонения обмазываем исключениями»

MKuznetsov ★★★★★
()

Как вообще принято реализовывать подобные вещи?

Если говорить про разбор протоколов, то один из подходов — т.н. sans-IO. Где разбор протокола, как можно догадаться по названию, отделяется от IO. Парсер становится автоматом, который на вход принимает байт (или буфер, для оптимизации), а на выходе выдаёт события/инструкции типа: НАЧАЛ_ЧИТАТЬ_СТРОКУ, ЗАКОНЧИЛ_ЧИТАТЬ_СТРОКУ или ДАЙ_ИЩО_БАЙТОВ

Это даёт свободу в способе добычи байтов: читать из fd, из FILE, из memory-mapped файла, синхронно или асинхронно.

utf8nowhere ★★★
()
Последнее исправление: utf8nowhere (всего исправлений: 1)
Ответ на: комментарий от u5er

Единственное отличие в том, что парсер сам вызывает функцию чтения при необходимости.

Такое наверное сложно применять с явной асинхронностью (крашеными функциями)

utf8nowhere ★★★
()
Ответ на: комментарий от utf8nowhere

Ну я сделал так. У меня отдельный поток (обработчик) «стоит на приёме». Если приходит сообщение само по себе, то это событие. Обработчик парсит сообщение и запускает обработчик соответствующего события.

Если нужно послать команду, то просто пишу данные в дескриптор и жду уведомления от обработчика. Когда приходит ответ на команду, то обработчик понимает это, даёт сигнал «пользователю» и сам встаёт на паузу в ожидании сигнала чтоб не мешать «пользователю». «Пользователь» полностью вычитывает ответ на свою команду и даёт сигнал обработчику. Получив сигнал, обработчик снова «встаёт» на приём.

Как-то так.

u5er ★★
() автор топика

Вот мой код, кстати

#define LINE_READER_BUFSIZE   4096


struct lineReader_s {
	
	int fd;
	
	char buf[LINE_READER_BUFSIZE];
	size_t readerOffset;
	size_t parserOffset;
	
	unsigned char flag_r;
	unsigned char flag_done;
};


void lineReaderInit( struct lineReader_s* line, int fd ){
	line->fd = fd;
	line->readerOffset = 0;
	line->parserOffset = 0;
	line->flag_r = 0x00;
	line->flag_done = 0x00;
}

static void _readLine_parser( struct lineReader_s* parser ){
	
	//Перебираем строку с того места, где остановились в прошлый раз.
	for( ; parser->parserOffset < parser->readerOffset; parser->parserOffset++ ){
		
		//Если встречаем возврат каретки, то взводим флаг.
		if( parser->buf[parser->parserOffset] == '\r' ){
			parser->flag_r = 0xff;
			continue;
		}
		
		//Если встречаем символ новой строки...
		if( parser->buf[parser->parserOffset] == '\n' ){
			
			//...и при этом флаг взведён, то это означает конец строки.
			//Забиваем \r\n нулями, взводим флаг и выходим.
			if( parser->flag_r ){
				parser->buf[parser->parserOffset    ] = 0x00;
				parser->buf[parser->parserOffset - 1] = 0x00;
				parser->parserOffset++;
				parser->flag_done = 0xff;
				return;
			}
			
			continue;
		}
		
		//Если встречаем обычный символ, то убираем флаг.
		parser->flag_r = 0x00;
	}
	
	//Если мы тут оказались, значит последовательность \r\n не была
	//встречена, а значит мы не достигли конца строки.
}

/*
reutrn:
	-1 on error
	0 if no line parsed
	1 if line parsed
*/

int readLine( struct lineReader_s* line ){
	
	int res;
	
	if( line->flag_done ){
		line->flag_done = 0x00;
		
		if( line->readerOffset == line->parserOffset ){
			//весь буфер пройден парсером
			line->readerOffset = 0;
			line->parserOffset = 0;
		}else{
			//в буфере есть данные, до которых парсер не дошёл
			//перемещаем их в начало буфера
			memcpy( line->buf, line->buf + line->parserOffset, line->readerOffset - line->parserOffset );
			line->readerOffset -= line->parserOffset;
			line->parserOffset = 0;
		}
	}
	
	for(;;){
		
		//Этот кусочек кода занимается выборкой строк.
		//Тут обслуживается счётчик parserOffset.
		_readLine_parser( line );
		if( line->flag_done ) return 1;
		
		//Этот кусочек кода занимается чтением потока данных.
		//TODO: EINTR
		res = read( line->fd, line->buf + line->readerOffset, LINE_READER_BUFSIZE - line->readerOffset );
		if( res < 0 ){
			perror( "line read" );
			return -1;
		}
		
		//тут обслуживается счётчик readerOffset
		if( !res ) return 0;
		line->readerOffset += res;
	}
}

Довольно много писанины по сравнению с «просто read()», поэтому и создал тему для поиска идей получше.

u5er ★★
() автор топика
Ответ на: комментарий от u5er

код самозатупаный какойто

покури сырцы иные

имхо понятней:

for( ; parser->parserOffset < parser->readerOffset; parser->parserOffset++ )switch(parser->buf[parser->parserOffset]){
    case '\n'://Если встречаем символ новой строки...
        if( !parser->flag_r )continue;
        //...и при этом флаг взведён, то это означает конец строки.
        //Забиваем \r\n нулями, взводим флаг и выходим.
        parser->buf[parser->parserOffset    ] = 0x00;
        parser->buf[parser->parserOffset - 1] = 0x00;
        parser->parserOffset++;
        parser->flag_done = 0xff;
        return;
    case '\r':parser->flag_r = 0xff;continue;//Если встречаем возврат каретки, то взводим флаг.
    default:parser->flag_r = 0x00;//Если встречаем обычный символ, то убираем флаг.
}

но вообще кури логику и сырцы ибо вот

qulinxao3 ★★
()

Если нет требований к производительности - читай по 1 байту, чем проще, тем лучше. Но это прям совсем тупой вариант, пожалуй так я делать не порекомендую почти никогда кроме простых программ на выброс.

Если есть требования к производительности - читай в буфер и потом из буфера. Проще всего использовать libc для этого, впрочем и вручную сделать несложно.

vbr ★★★★
()
Последнее исправление: vbr (всего исправлений: 1)
Ответ на: комментарий от u5er

это мощно. видимо ты видишь разницу между 0, 0x0 и 0x00. :) пишешь ради зауми, или что?

parser->flag_r = 0x00;

флаг это вообще булево значение, и делается 1 битом, а не 8мью как у тебя. это вспринмается не как флаг, а как какой-то набор битов.

parser->flag_done = 0xff;

присваивай своему флагу 0 и 1, и не мучь глаза читателя.

alysnix ★★★
()

Как вообще принято реализовывать подобные вещи?

Я обычно внимательно читаю задание. Потом еще раз. Потом определяюсь с тем, как данные приходят (посимвольно или строками или просто кусками) и что должно быть получено - максимальное быстродействие, максимально гибкая реализация, etc.

Потом делаю «чтобы работало» как можно проще - отладить протокол. А потом оптимизирую в целевом направлении. Ну т.е. 2-3 раза приходится переделывать, сразу ни разу не получалось сделать прям хорошо и на все случаи жизни.

aiqu6Ait ★★★★
()
Ответ на: комментарий от alysnix

У меня давно такая привычка. Если 0, то это просто число, если 0х00, то это что-то служебное. Хотя фактически разницы там нет.

Дёргать по одному биту в данной задаче не хочу, т.к. флага всего 2. Если их будет 100500, то можно будет использовать битовые поля, например. Но побитовый доступ всё же немного накладный.

u5er ★★
() автор топика
Ответ на: комментарий от u5er

Исключительно для упрощения сделал

канонически, то есть по взрослому, такая вот мутота делается след образом

берется тред - ридер, который спит на ожидании сигнала появления данных в канале. когда данные появляются, он просыпается читает все, что есть, в кольцевой буфер, и…

дальше варианты - если ему решили доложить интеллекту - он ждет появления конца строки, и увидев его, сигналит треду - потребителю, что тот должен срочно забрать строку из буфера.

поскольку буфер кольцевой, и у нас все по взрослому, там не надо практически никакой синхронизации тредов, поскольку пишут и читают они по разным адресам их кольцевого буфера.

только роль ридера в такой схеме - никакая. в принципе и консьюмер строк может забирать байты из канала, и разбираться с концом строки.

но это вопрос уже чисто архитектурный.

alysnix ★★★
()
Ответ на: комментарий от alysnix

берется тред - ридер, который спит на ожидании сигнала появления данных в канале.

Вот любите вы треды форкать по поводу и без. А если одновременных сессий много обрабатывать надо - тоже по треду на каждую? А как только появляется non-blocking IO становится не так принципиально часть это main loop или в отдельном треде делается. И сильно оптимизированный буфер реально нужен только если вероятность получить partial message довольно высока, а если они раз в пятилетку случаются хвосты можно вообще в обычных строках хранить и изначально читать в массив на стеке. И обрабатывать тоже можно прям в главном треде (ie только реально тяжелую обработку отправлять в background thread) - ещё и на context switches сэкономите.

канонически, то есть по взрослому

В общем так себе ваше «взрослое» решение.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

читаем меня до конца

только роль ридера в такой схеме - никакая. в принципе и консьюмер строк может забирать байты из канала, и разбираться с концом строки.

alysnix ★★★
()
Ответ на: комментарий от bugfixer

А если одновременных сессий много обрабатывать надо - тоже по треду на каждую?

много сессий сразу - это задачи другого уровня. и решаются они канонически, то есть по взрослому, пулом тредов, исполняющих некие задания, например диспетчеризации входящих строк.

но такой наворот автору явно не надобен, ибо вопросы были бы другие

alysnix ★★★
()
Ответ на: комментарий от alysnix

много сессий сразу - это задачи другого уровня. и решаются они канонически, то есть по взрослому, пулом тредов

Опять 25. Зайдём с другой стороны: что делать будем если сессий больше чем размер пула? Ответ - да нафиг он не впился пока тяжёлых запросов не появляется. Вот только к вопросам вычитывания и парсинга входящих мессаг это никакого отношения не имеет.

Но наверное в одном я таки буду с вами солидарен: читать побайтно - это действительно пионерство, и так никто не делает.

bugfixer ★★★★★
()
Ответ на: комментарий от alysnix

если у вас есть проблема, сформулируйте ее

Формулирую: ваши вышеприведённые тезисы полны BS, и так как вы советуете никто не делает. Идеальным решением было бы ваше хотя бы однократное публичное признание своей неправоты, но как показывает практика этого не происходит вообще никогда.

bugfixer ★★★★★
()

Есть куча протоколов общения, которые подразумевают общение строками, длина которых заранее не известна

читать в буфер (если это уже не происходит под капотом), а что еще делать?

В целом все подобные протоколы - говно, придуманное людьми, которые не предполагали, что их поделки будут парситься компьютерами.

Включая HTTP, да. В первую очередь HTTP.

Lrrr ★★★★★
()

Если разделитель - один байт - то никакой пляски с бубном нет - просто ищешь разделитель в считанном буффере.
Если надо избежать блокировки - читаешь первый байт в блокирующем режиме, а дальше в неблокирующем (MSG_DONTWAIT например) - тогда ситуации когда отправлена одна строчка, а пытаются считать больше не будет.
если разлелитель больше байта - тут начинается пробоема, что метка может оказаться на границе считанного буффера и в этом случае надо искать и в текущем буфере и в предыдущем

mittorn ★★★★★
()
Ответ на: комментарий от mittorn

надо искать и в текущем буфере и в предыдущем

Я избежал этой проблемы, выделив буфер достаточного размера. Есть счётчик смещения, по которому можно будет продолжить чтение с того места, на котором остановилась предыдущая операция чтения.

u5er ★★
() автор топика