cafemike
cafemike

Reputation: 660

Scheduling socket communication for Swift

I am fairly new to Swift programming, please forgive the dummy questions I have got to ask below.

In my app, I am trying to schedule to call a function, which will receive some data from my server and will be called every second. The communication needs to be achieved via a TCP socket. After doing some research, it seems to me that I need to have a way to properly use threads to call that function. So here comes my questions:

  1. Where should I make the connection to the server? (Should I make the connection in the viewDidLoad() function of my first view controller?)
  2. Where should I create a thread to schedule the function call? If I create the thread inside my first view controller, would the thread die after I switching to another view controller?
  3. What QoS level I should use for that thread? The app is rendering the data received from the server every second, so I assume this task would have a very high priority.

I tried looking for tutorials and examples about threads and socket communications, but I could not find information that is applicable to my app. So any help or insight regarding the design would be greatly appreciated!

Thanks in advance!

Upvotes: 4

Views: 1202

Answers (2)

cafemike
cafemike

Reputation: 660

After some digging, I have found the way to implement stream programming on the Swift side (using native Swift capabilities). It turns out that the way in Swift is very much like the way in Objective C.

The two functions required to enable stream programming in Swift are func connect(), which I had to write by myself, and func stream(_ aStream: Stream, handle eventCode: Stream.Event), which is provided as a method in StreamDelegate.

Here is what func connect() looks like:

func connect() {
    Stream.getStreamsToHost(withName: <serverIP>, port: <serverPort>, inputStream: &inputStream, outputStream: &)

    guard let inputStream = inputStream, let outputStream = outputStream else {
        print(" ->\tNetworkControllerError: Cannot open inputstream/outputstream.")
        return
    }

    // Set delegate
    inputStream.delegate = self
    outputStream.delegate = self

    let socketWorkQueue = DispatchQueue(label: "socketWorkQueue", attributes: .concurrent)
    CFReadStreamSetDispatchQueue(inputStream, socketWorkQueue)
    CFWriteStreamSetDispatchQueue(outputStream, socketWorkQueue)

    inputStream.open()
    outputStream.open()
}

And here is what the func stream(_ aStream: Stream, handle eventCode: Stream.Event) looks like:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {

    if aStream === inputStream {
        switch eventCode {
        case Stream.Event.errorOccurred:
            streamEventQueue.async {
                // do something here
            }
            break
        case Stream.Event.openCompleted:
            streamEventQueue.async {
                // do something here
            }
            break
        case Stream.Event.hasBytesAvailable:
            streamEventQueue.async {
                // do something here
                let output = read()
            }
            break
        case Stream.Event.endEncountered:
            streamEventQueue.async {
                // do something here
            }
            break
        default:
            break
        }
    }
    else if aStream === outputStream {
        switch eventCode {
        case Stream.Event.errorOccurred:
            streamEventQueue.async {
                // do something here
            }
            break
        case Stream.Event.openCompleted:
            streamEventQueue.async {
                // do something here
            }
            break
        default:
            break
        }
    }
}

So the stream() function should be the only place in my app to handle state transitions caused by network connection/disconnection. Note that I also used a serial event queue to process the stream event. This is to prevent any potential race condition which might lead to state corruption.

For those who are not very familiar with stream programming, this is a server-client communication pattern which keeps the connection/socket alive. The connection is required to stay alive mostly due to there is a constant communication between the server and the client. Communication patterns like RESTful API would burden the server significantly by clients' requests and hence not recommended.

Upvotes: 0

KrishnaCA
KrishnaCA

Reputation: 5695

According to me, the following can be done for the scenarios presented in the question

  1. If you are going to make connection to your server as soon as you launch the app. I believe it's better to do that in AppDelegate in didFinishLaunchingWithOptions given below

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    }
    
  2. It depends on how you are going to switch to other ViewController. In the process of switching if you are going to deallocate your current ViewController, then your thread will die.

  3. Considering that you're making a socket based app and you will be receiving data from server every second. Then, NSURLSession might not be of much help to you. For socket communication, normally NSInputStream and NSOutputStream will be used. The following example from objective-C may help you to get started:

     - (void)connectWithHostFromUrl: (NSURL *)hostUrl {
    
            CFReadStreamRef readStream;
            CFWriteStreamRef writeStream;
            CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)[hostUrl host], 80, &readStream, &writeStream);
    
           _inputStream = (__bridge_transfer NSInputStream *)readStream;
           [_inputStream setDelegate:self];
           [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
           [_inputStream open];
    
           _outputStream = (__bridge_transfer NSOutputStream *)writeStream;
           [_outputStream setDelegate:self];
           [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
           [_outputStream open];
      }
    
     // Delegate methods
    
    - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    
            switch (eventCode) {
                 case NSStreamEventNone:
                 {
                  // handle it according to your need
                 }
                      break;
                 case NSStreamEventOpenCompleted:
                 {
                  // handle it according to your need
                 }
                      break;
                 case NSStreamEventHasBytesAvailable:
                 {
                     if (_receivedData == nil) {
                          _receivedData = [NSMutableData new];
                     }
                     uint8_t buffer[1024];
                     NSInputStream *inputStream = (NSInputStream *)aStream;
                     NSInteger bytesReceived = [inputStream read:buffer maxLength:1024];
                    if (bytesReceived > 0) {
                        [_receivedData appendBytes:(const void *)buffer length:bytesReceived];
                    }
                 }
                      break;
                 case NSStreamEventHasSpaceAvailable:
                 {
                      if (_dataToSend != nil) {
                        // _dataToSend is NSMutableData/NSData object
                          NSOutputStream *outputStream = (NSOutputStream *)aStream;
                          const uint8_t *mutableBytes = (const uint8_t *)[_dataToSend mutableBytes];
                          NSInteger length = [_dataToSend length]/sizeof(uint8_t);
                          [outputStream write:(const uint8_t *)mutableBytes maxLength:length];
                      }
                  }
                      break;
                  case NSStreamEventErrorOccurred:
                  {
                    // handle it according to your need
                  }
                      break;
                  case NSStreamEventEndEncountered:
                  {
                    // handle it according to your need
                  }
                      break;
                  default:
                      break;
           }
    }
    

Since there are a lot of cases to handle here. Most of the time, I suggest using a tested third party library like SocketRocket.

Please suggest edits to make this answer better :)

Upvotes: 1

Related Questions