Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-11-09

Socket Programming in Swift: Part 6 - select and recv

Updated on 2016-08-11 for Swift 3 Xcode 8 beta 3 & use in playground.

In the previous post we have progressed to the point where a new thread was started to receive the actual data that is being send to the application.

While the accept call can in fact 'hang' when the other side messes something up, that is an unlikely scenario. The accept call is part of the TCP/IP stack that is implemented in the OS. The recv call however depends on "the other application" doing things right. Which under some circumstances cannot always be assumed.

Hence we will use this opportunity to introduce the select call.

It is easiest to see in code:

(PS: This code does compile in playground assuming the fdSet is present in a resource called "SwifterSockets". But it is unlikely that it is usable as is. For example the 'processData' function should be defined someplace else, not inside this function. If it must be kept inside this function, it would probably be better not to spin off the 'processData' into its own thread.)

func receiveAndDispatch(socket: Int32) {
    
    let dataProcessingQueue = DispatchQueue.main
    
    let bufferSize = 100*1024 // Application dependant
    var requestBuffer: Array<UInt8> = Array(repeating: 0, count: bufferSize)
    var requestLength: Int = 0

    func requestIsComplete() -> Bool {
        // This function should find out if all expected data was received and return 'true' if it did.
        return true
    }
    
    func processData(data: Data) {
        // This function should do something with the received data
    }
    
    // =========================================================================================
    // This loop stays active as long as there is data left to receive, or until an error occurs
    // =========================================================================================
    
    RECEIVER_LOOP: while true {
        
        
        // =====================================================================================
        // Use the select API to wait for anything to happen on our client socket only within
        // the timeout period
        // =====================================================================================
        
        let numOfFd:Int32 = socket + 1
        var readSet:fd_set = fd_set(fds_bits: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
        var timeout:timeval = timeval(tv_sec: 10, tv_usec: 0)
        
        SwifterSockets.fdSet(socket, set: &readSet)
        let status = select(numOfFd, &readSet, nil, nil, &timeout)
        
        // Because we only specified 1 FD, we do not need to check on which FD the event was received
        
        
        // =====================================================================================
        // In case of a timeout, close the connection
        // =====================================================================================
        
        if status == 0 {
            
            let message = "Timeout during select."
            print(message)
            close(socket)
            break RECEIVER_LOOP
        }
        
        
        // =====================================================================================
        // In case of an error, close the connection
        // =====================================================================================
        
        if status == -1 {
            
            let errString = String(utf8String: strerror(errno)) ?? "Unknown error code"
            let message = "Error during select, message = \(errno) (\(errString))"
            print(message)
            close(socket)
            break RECEIVER_LOOP
        }
        
        
        // =====================================================================================
        // Use the recv API to see what happened
        // =====================================================================================
        
        let bytesRead = recv(
            socket,
            &requestBuffer[requestLength],
            bufferSize,
            0)
        
        
        // =====================================================================================
        // In case of an error, close the connection
        // =====================================================================================
        
        if bytesRead == -1 {
            
            let errString = String(utf8String: strerror(errno)) ?? "Unknown error code"
            let message = "Recv error = \(errno) (\(errString))"
            print(message)
            
            // The connection might still support a transfer, it could be tried to get a message to the client. Not in this example though.
            
            close(socket)
            break RECEIVER_LOOP
        }
        
        
        // =====================================================================================
        // If the client closed the connection, close our end too
        // =====================================================================================
        
        if bytesRead == 0 {
            
            let message = "Client closed connection"
            print(message)
            close(socket)
            break RECEIVER_LOOP
        }
        
        
        // =====================================================================================
        // If the request is completely received, dispatch it to the dispatchQueue
        // =====================================================================================
        
        let message = "Received \(bytesRead) bytes from the client"
        print(message)
        
        requestLength = requestLength + bytesRead
        
        if requestIsComplete() {
            
            let receivedData = Data(bytes: requestBuffer[0 ... requestLength])
            dataProcessingQueue.async() { processData(data: receivedData) }
            
            close(socket)
            break RECEIVER_LOOP
        }
    }

}

A whole lot of stuff going on here. First off, the select call waits until the time-out to see if there is any activity on the given file descriptor. Yes, file descriptor. However a socket descriptor is a kind of file descriptor thus we can use them interchangeably.

The select call needs a set of file descriptors that specify which file descriptors must be monitored. The C examples you will find all use FD_SET, FD_CLR etc to do the heavy lifting. These are C-macro's. However in Swift these are not available. I have implemented replacement functions that mimic these macro's. You can find them here.

When select returns there are three possibilities: something happened, timeout or error. If something was received the select call will tell us on which socket it was. But since in the above example only one file descriptor is specified, the code does not have to check for this. But it does check if there was an error or a timeout.

Once the recv call receives data a check is needed to see if all data was received. Even for the shortest of data transfers (> 1 byte) it is not guaranteed that all data is received in a single call to recv. Every application will have a different way of knowing when the data is complete. For example if the app is expecting a JSON message it can count the number of '{' and '}'. When the numbers are equal that will signify the completion of the message. (Assuming only JSON objects are allowed at the top level)

When the message is complete the RECEIVER_LOOP will start processing the data on a different thread while terminating itself.

2016-01-09: Part 7 - Client side considerations

Check out my Port Spy app in the App Store. A utility that helps you debug your socket based application and includes its own source code. So you can see first hand how to implement socket based io in Swift. And you will be helping this blog!

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment