Разбор данных после отправки между устройствами iOS через NSStream
У меня установлено приложение для отправки данных между двумя устройствами iOS с использованием NSStream
через TCP соединение.
Отправленные данные состоят из двух частей:
- Целое число, указывающее размер объекта сообщения
- Объект сообщения, некоторые
NSStrings
иNSData
объект, закодированный с помощью NSKeyedArchiver
Проблема:
- Когда размер NSData составляет около 1,5 МБ, я получаю непонятное архивное исключение, когда пытаюсь его декодировать. При чтении 4 байтов, следующих за тем, где должно быть сообщение, появляется большое число, а не размер следующего сообщения.
- Когда NSData объект размером около 80 КБ, два сообщения успешно декодируются, и тогда я получаю непонятное архивное исключение.
Кажется, что данные в какой-то момент выходят из строя... хотя вся цель TCP - поддерживать их в порядке. Итак, я должен быть проблемой!
Соответствующий код
сервер
sendData:
передается объект Message, который был закодирован с помощью NSKeyedArchiver. Он вызывается за 100 сообщений за короткий промежуток времени
// dataQueue is an NSMutableArray
- (void) sendData:(NSData *)data
{
int size = data.length;
NSData *msgSize = [NSData dataWithBytes:&size length:sizeof(int)];
if (self.outputStream.hasSpaceAvailable && (self.dataQueue.count == 0)) {
[self.dataQueue addObject:data];
[self.outputStream write:msgSize.bytes maxLength:msgSize.length];
}
else {
[self.dataQueue addObject:msgSize];
[self.dataQueue addObject:data];
}
}
//called by NSStreamDelegate method when space is available
- (void) hasSpaceAvailable
{
if (self.dataQueue.count > 0) {
NSData *tmp = [self.dataQueue objectAtIndex:0];
[self.outputStream write:tmp.bytes maxLength:tmp.length];
[self.dataQueue removeObjectAtIndex:0];
}
}
клиент
streamHasBytes:
собирает фрагменты сообщения и добавляет их в self.buffer. Когда длина self.buffer становится больше, чем длина сообщения, указанная в первых 4 байтах self.buffer, объект Message анализируется...
//Called by NSStreamDelegate method when bytes are available
- (void) streamHasBytes:(NSInputStream *)stream
{
NSInteger bytesRead;
uint8_t buffer[32768];
bytesRead= [stream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1 || bytesRead == 0) //...err
@synchronized(self) { //added to test concurrency
[self.buffer appendBytes:buffer length:bytesRead];
}
[self checkForMessage];
}
- (void) checkForMessage
{
@synchronized(self) { //added to test concurrency
int msgLength = *(const int *)self.buffer.bytes;
if (self.buffer.length < msgLength) return;
//remove the integer from self.buffer
[self.buffer replaceBytesInRange:NSMakeRange(0, sizeof(int)) withBytes:NULL length:0];
//copy the actual message from self.buffer
NSData *msgData = [NSData dataWithBytes:self.buffer.bytes length:msgLength];
//remove the message from self.buffer
[self.buffer replaceBytesInRange:NSMakeRange(0, msgLength) withBytes:NULL length:0];
Message *theMsg = [NSKeyedUnarchiver unarchiveObjectWithData:msgData];
[self.delegate didReceiveMessage:theMsg];
}
}
РЕДАКТИРОВАТЬ:
Сейчас я замечаю, что в случае, когда объект NSData в первом сообщении составляет около 1,5 МБ, для общего размера сообщения около 1,6 МБ, только около 1,3 МБ получено клиентом... Это объясняет непонятное Архив ошибок. Почему все данные не будут доставлены?
2 ответа
Оказывается, что в некоторых случаях фактически отправлялась только часть данных, которые, как я предполагал, отправлялись. NSOutputStream
"s write:maxLength:
Метод возвращает количество байтов, которые были фактически записаны в поток. Итак hasSpaceAvailable
метод выше может быть исправлен с
NSInteger i = [self.outputStream write:tmp.bytes maxLength:tmp.length];
if (i < tmp.length) {
//send the difference
}
Я делаю что-то практически то же самое, но я пошел немного другим методом. Я решил получить размер заархивированного dataToSend, а затем преобразовать переменную int size в кусок mutableData. Затем я добавляю dataToSend к блоку dataSized, создавая один блок данных, который вы можете обработать и "объединить" для отправки вместе. Таким образом, все остается вместе. Вещи должны оставаться вместе, и они остаются. Вы должны настроить буферы соответственно. У меня есть данные размером от 400 до 450 байт, поэтому у меня есть 512-байтовый буфер для dataToSend, тогда сервер и клиенты имеют 4096 (4K) буфер для накопления отправленных данных. Но он никогда не проходит 1536 байт, когда 4 игрока обмениваются данными со скоростью 30 кадров в секунду...
Каждый игрок звонит:
- (void)sendData:(CAKNodeSpec *)sentSpec {
NSMutableData *archivedData = [NSMutableData dataWithCapacity:512];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:archivedData];
[sentSpec encodeWithCoder:archiver];
[archiver finishEncoding];
[self.gameClient outputData:archivedData];
// gameClient is custom NSStreamDelegate with IN/OUT Streams
}
В gameClient и gameConnect для игрового сервера
- (void)outputData:(NSMutableData *)dataToSend {
if (self.outputBuffer != nil) {
unsigned int size = (unsigned int)dataToSend.length;
// maintains 32-bit/64-bit architecture compatibility & silence warnings
NSMutableData *dataSized = [NSMutableData dataWithBytes:&size length:sizeof(unsigned int)];
[dataSized appendBytes:dataToSend.bytes length:size];
[self.outputBuffer appendData:dataSized];
[self startOutput];
}
}
- (void)startOutput {
NSInteger actuallyWritten = [self.outputStream write:self.outputBuffer.bytes maxLength:self.outputBuffer.length];
if (actuallyWritten > 0) {
[self.outputBuffer replaceBytesInRange:NSMakeRange(0, (NSUInteger) actuallyWritten) withBytes:NULL length:0];
} else {
[self closeStreams];
}
}
следуя классической модели коммутатора
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)streamEvent {
assert(aStream == self.inputStream || aStream == self.outputStream);
switch(streamEvent) {
case NSStreamEventOpenCompleted: {
if (aStream == self.inputStream) {
self.inputBuffer = [[NSMutableData alloc] init];
self.dataQueue = [[NSMutableArray alloc] init];
} else {
self.outputBuffer = [[NSMutableData alloc] init];
}
} break;
case NSStreamEventHasSpaceAvailable: {
if ([self.outputBuffer length] != 0) {
[self startOutput];
}
} break;
case NSStreamEventHasBytesAvailable: {
uint8_t buffer[4096];
NSInteger actuallyRead = [self.inputStream read:buffer maxLength:4096];
if (actuallyRead > 0) {
[self.inputBuffer appendBytes:buffer length:(NSUInteger)actuallyRead];
NSLog(@"Read: %ld", (long)actuallyRead);
[self chopaChunk];
NSMutableData *specData;
while ((specData = [self.dataQueue shift])) {
[self.delegate handleData:specData]; // unarchives
}
} else {
NSLog(@"empty buffer!");
}
} break;
case NSStreamEventErrorOccurred:
case NSStreamEventEndEncountered: {
[self closeStreams];
} break;
default:
break;
}
}
Это реальная разница:
- (void)chopaChunk {
unsigned int dataSize = *(const unsigned int *)self.inputBuffer.bytes;
while (self.inputBuffer.length >= (sizeof(unsigned int) + dataSize)) {
//remove the integer from self.inputBuffer
[self.inputBuffer replaceBytesInRange:NSMakeRange(0, sizeof(unsigned int)) withBytes:NULL length:0];
//copy the actual message from self.inputBuffer
NSMutableData *specData = [NSMutableData dataWithBytes:self.inputBuffer.bytes length:dataSize];
[self.dataQueue addObject:specData];
//remove the message from self.inputBuffer
[self.inputBuffer replaceBytesInRange:NSMakeRange(0, dataSize) withBytes:NULL length:0];
if (self.inputBuffer.length > 0) {
dataSize = *(const unsigned int *)self.inputBuffer.bytes;
// I just keep going adding, I guess you would add multiple 80K pieces here
}
}
}
Следуя вашим указаниям, я решил проверить, является ли inputBuffer>= dataSize + (sizeof(unsigned int)), таким образом, как производитель колбасы, мы просто скручиваем кусочки по мере их поступления!:D
Поток представляет собой данные для outputBuffer для outputStream <- send -> для inputStream для inputBuffer обратно к данным.
Я думаю, что если вы перейдете к использованию сцепленных данных вместо того, чтобы пытаться отправить два разных объекта, вы будете чувствовать себя лучше, чем пытаться выяснить, кто есть кто, и когда вы получите то, что... и> = вместо <, и просто добавить блоки mutableData в Ваше dataQueue... ваши варианты использования могут отличаться...