Hello. I use my iOS application to download big text files from the server over TCP. This is the code I use:
func openNetworkCommunication()
{
if(self.inputStream != nil && self.outputStream != nil)
{
closeNetworkCommunication()
}
let ipAddress = delegate?.ipAddressString as! CFStringRef
let portNumber = delegate?.ipPort
var readStream: Unmanaged<CFReadStream>?
var writeStream: Unmanaged<CFWriteStream>?
CFStreamCreatePairWithSocketToHost(nil, ipAddress, portNumber!, &readStream, &writeStream)
self.inputStream = readStream!.takeRetainedValue()
self.outputStream = writeStream!.takeRetainedValue()
self.outputBuffer = delegate?.messageToServer.dataUsingEncoding(NSUTF8StringEncoding) as? NSMutableData
self.inputStream?.delegate = self
self.outputStream?.delegate = self
self.inputStream?.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
self.outputStream?.scheduleInRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
self.inputStream?.open()
self.outputStream?.open()
}
func closeNetworkCommunication()
{
self.inputStream?.close()
self.outputStream?.close()
self.inputStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
self.outputStream?.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
self.inputStream?.delegate = nil
self.outputStream?.delegate = nil
self.inputStream = nil;
self.outputStream = nil;
self.outputBuffer = nil;
}
func stream(theStream : NSStream, handleEvent streamEvent : NSStreamEvent)
{
switch (streamEvent)
{
case NSStreamEvent.None:
println("NSStreamEventNone")
case NSStreamEvent.OpenCompleted:
println("NSStreamEventOpenCompleted")
case NSStreamEvent.HasBytesAvailable:
println("NSStreamEvent.HasBytesAvailable")
var buffer = [UInt8](count: 4096, repeatedValue: 0)
if ( theStream == self.inputStream)
{
while (self.inputStream!.hasBytesAvailable)
{
var len = inputStream!.read(&buffer, maxLength: buffer.count)
if len < 0
{
self.error = self.inputStream!.streamError
println("Input stream has less than 0 bytes")
closeNetworkCommunication()
}
else if len == 0
{
println("Input stream has 0 bytes")
closeNetworkCommunication()
}
if(len > 0)
{
var messageFromServer = NSString(bytes: &buffer, length: buffer.count, encoding: NSUTF8StringEncoding)
delegate?.messageReceived(messageFromServer!)
}
}
}
case NSStreamEvent.HasSpaceAvailable:
println("NSStreamEventHasSpaceAvailable")
if self.outputBuffer?.length != 0
{
var bytesWritten : Int = self.outputStream!.write(UnsafePointer<UInt8>(self.outputBuffer.bytes), maxLength: self.outputBuffer.length)
if bytesWritten <= 0
{
self.error = self.outputStream?.streamError
}
else
{
self.outputBuffer?.replaceBytesInRange(NSMakeRange(0, bytesWritten), withBytes: nil, length: 0)
}
}
case NSStreamEvent.ErrorOccurred:
println("NSStreamEventErrorOccurred");
case NSStreamEvent.EndEncountered:
println("NSStreamEventEndEncountered")
theStream.close()
theStream.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)
default:
println("Unknown event")
}
}
I have two big problems:
1) After the text has been transmitted to the iOS client, the connection remains open. If I try to do closeNetworkCommunication() in NSStreamEvent.HasBytesAvailable, then not the whole text is downloaded -- it is truncated in the middle. Is there a way to close the connection only after the full text has been passed?
2) Sometimes, the text passed to the iOS application includes a piece from the beginning of the text. For example:
Original:
AAAAAAAAAAAAAA
BBBBBBBBBBBBBB
Passed:
AAAAAAAAAAAAAA
BBBBBBBBBBBBBBAAAA
I am checking what is transmitted by the server and it is the original text... Does the code in the client fills the buffer with the remaining text to have the full size buffer? And how to fix it?
Many thanks!
You should factor out you creation of
dataToFind
because it’s effectively a global constant. Check out Swift’s static property feature.
Why are you searching backwards (
NSDataSearchOptions.Backwards
)? You want to search from the beginning of the buffer so that you find the first block. If you search backwards, you’ll find the end of the last block, and the data before that may contain multiple blocks.
Also,
-subdataWithRange:
will give you just the terminator. What you want it to get the data up to the terminator. So, you should have:
let subData = self.inputBuffer!.subdataWithRange(NSMakeRange(0, indexOfData.location))
or:
let subData = self.inputBuffer!.subdataWithRange(NSMakeRange(0, indexOfData.location + indexOfData.length))
depending on whether you want
subData
to include the terminator or not.
Also, keep in mind that the newly arrived chunk of data might contain two blocks, so this code needs to go into a loop. You can either remove that data from the buffer each time around the loop, or adjust
searchRange
each time around the loop and then remove all the data in one hit at the end.
The following code shows an example of that first approach.
class BlankLineUnframer {
private var inputBuffer: NSMutableData
init() {
self.inputBuffer = NSMutableData(capacity: 65536)!
}
private static let terminator = "\n\n".dataUsingEncoding(NSUTF8StringEncoding)!
func blocksFromData(newData: NSData) -> [String] {
var result: [String] = []
self.inputBuffer.appendData(newData)
repeat {
// Search for the terminator.
let terminatorRange: NSRange = self.inputBuffer.rangeOfData(
BlankLineUnframer.terminator,
options: [],
range: NSMakeRange(0, self.inputBuffer.length)
)
if terminatorRange.location == NSNotFound {
// +++ here you should check for self.inputBuffer being ridiculously large
// If it's not present, we're done.
break
}
// The block is everything up to the terminator. Extract it as a string
// and add it to our result array.
let blockRange = NSMakeRange(0, terminatorRange.location)
let blockData = self.inputBuffer.subdataWithRange(blockRange)
let blockString = String(data: blockData, encoding: NSUTF8StringEncoding)
// +++ here you should check for blockString being nil
result.append(blockString!)
// Remove the terminator and everything before it (the block) from the buffer.
let rangeToEndOfTerminator = NSMakeRange(0, terminatorRange.location + terminatorRange.length)
self.inputBuffer.replaceBytesInRange(rangeToEndOfTerminator, withBytes: nil, length: 0)
} while (true)
return result
}
}
IMPORTANT See how I’ve put this code into a separate ‘unframer’ class? This is important because it allows you to test the code in isolation. Parsing data that comes off the network is tricky because the edge cases will only show up in the field. Worse yet, before this is network facting, any bugs in your parse may introduce security vulnerabilities. So, you should test your parser in a unit test rather than in your live code.
ps A lot of the declarations in your code are
var
rather than
let
for no apparent reason. In Swift you should default to
let
.
Share and Enjoy
—
Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"