Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

Real-Time Communication with Streams Tutorial for iOS

$
0
0

Real-Time Communication with Streams Tutorial for iOS

Note: Updated by Luke Parham for for Xcode 9 beta / iOS 11 / Swift 4. Original post by Cesare Rocchi.

From the dawn of time, man has dreamt of better and better ways of communicating with his brethren far and wide. From carrier pigeons to radio waves, we’re forever trying to communicate more clearly and effectively.

In this modern age, one technology has emerged as an essential tool in our quest for mutual understanding: the humble network socket.

Existing somewhere in layer 4 of our modern networking infrastructure, sockets are at the core of any online communication from texting to online gaming.

Why Sockets?

You may be wondering, “Why do we need to go lower-level than URLSession in the first place?”. If you’re not wondering that then go ahead and pretend you were…

Great question! The thing about communicating with URLSession is that it’s based on the HTTP networking protocol. With HTTP, communication happens in a request-response style. This means that the majority of the networking code in most apps follows the same pattern:

  1. Request some JSON from a server.
  2. Receive and use said JSON in a callback or delegate method.

But what about when you want the server to be able to tell your app about something? Doing so doesn’t really map to HTTP very well. Of course, you can make it work by continually pinging the server and seeing if it has updates, aka polling, or you can get a little more crafty and use a technique like long-polling, but these techniques can feel a little unnatural and each has its own pitfalls. At the end of the day, why limit yourself to this request-response paradigm if it’s not the right tool for the job?

In this streams tutorial, you’ll learn how you can drop down a level of abstraction and use sockets directly to create a real-time chatroom application.

streams tutorial

Instead of each client having to check the server for new messages, it’ll use input and output streams that remain open for the duration of the chat session.

Getting Started

To begin, download the starter materials which include both the chat app and a simple server written in Go. You won’t have to worry about writing any Go code yourself, but you will need to get this server up and running in order to write a client for it.

Getting This Server Up and Running

The included server was written in Go and then compiled for you. If you’re not the kind of person who trusts a precompiled executable you found on the web, I’ve included the source code, so feel free to compile it yourself.

To run the pre-compiled server, open your terminal, navigate to the starter materials directory and enter this command, followed by your password when prompted:

sudo ./server

After putting your password in, you should see Listening on 127.0.0.1:80. Your chat server is ready to go! You can now skip to the next section.

If you want to compile the server yourself, you’ll need to install Go with Homebrew.

If you don’t have Homebrew either then you’ll have to install that first. Open the terminal, and paste in the following line:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Then, use this command to install Go:

brew install go

Once that’s finished, navigate to the directory of the starter materials and build the server with the build command.

go build server.go

Finally, you can start your server, using the command listed at the start of this section.

Looking at the Existing App

Next, open the DogeChat project and build and run it to get a look at what’s already been built for you.

streams tutorial

As shown above, DogeChat is currently set up to allow a user to enter their username and then go into a chat room. Unfortunately, the last person to work on it really wasn’t sure how to write a chat app so they wrote all the UI and basic plumbing, but left it for you to implement the actual networking layer.

Creating a Chat Room

To get started on the actual coding, navigate to ChatRoomViewController.swift. Here, you can see that you’ve got a view controller that is ready and able to receive strings as messages from the input bar, as well as display messages via a table view with custom cells that can be configured with Message objects.

Since you’ve already got a ChatRoomViewController, it only makes sense that you’d create a ChatRoom class to take care of the heavy lifting.

Before getting started on writing a new class, I like to make a quick list of what its responsibilities will be. For this class, we’ll want it to take care of the following:

  1. Opening a connection to the chat room server
  2. Allowing a user to join the chat room by providing a username
  3. Allowing a user to send and receive messages
  4. Closing the connection when you’re done

Now that you know what you want, hit ⌘+n to create a new file. Choose Cocoa Touch Class and then name it ChatRoom.

Creating Input & Output Streams

Next, go ahead and replace what’s in that file with

import UIKit

class ChatRoom: NSObject {
  //1
  var inputStream: InputStream!
  var outputStream: OutputStream!

  //2
  var username = ""

  //3
  let maxReadLength = 4096

}

Here, you’ve defined the ChatRoom class and also declared the properties you’ll need in order to communicate effectively.

  1. First, you have your input and output streams. Using this pair of classes together allows you to create a socket based connection between your app and the chat server. Naturally, you’ll send messages via the output stream and receive them via the input stream.
  2. Next, you have your username variable where you’ll store the name of the current user.
  3. And finally you have the maxReadLength. This variable puts a cap on how much data can be sent in any single message.

Next, go over to ChatRoomViewController.swift and add a chat room property to the list of properties at the top.

let chatRoom = ChatRoom()

Now that you’ve got the basic structure of your class set up, it’s time to knock out the first thing in your checklist, opening a connection between the app and the server.

Opening a Connection

Head back over to ChatRoom.swift and below the property definitions, add the following method:

func setupNetworkCommunication() {
  // 1
  var readStream: Unmanaged<CFReadStream>?
  var writeStream: Unmanaged<CFWriteStream>?

  // 2
  CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault,
                                     "localhost" as CFString,
                                     80,
                                     &readStream,
                                     &writeStream)
}

Here’s what’s happening:

  1. Here, you’ve set up two uninitialized socket streams that won’t be automatically memory managed for you.
  2. Then you bind your read and write socket streams together and connect them to the socket of the host, in this case on port 80.

    The function takes four arguments. The first is the type of allocator you want to use when initializing your streams. You should use kCFAllocatorDefault whenever possible, though there are other options if you run into a situation where you need something that acts a little differently.

    Next, you specify the hostname. In this case you’re just connecting to the local machine, but if you had a specific IP address for a remote server, you could also use that here.

    Then, you specify that you’re connecting via port 80, which is the port we’ve set our server up to listen on.

    Finally, you pass in the pointers to your read and write streams so the function can initialize them with the connected read and write streams it will create internally.

Now that you’ve got initialized streams, you can store retained references to them by adding the following lines:

inputStream = readStream!.takeRetainedValue()
outputStream = writeStream!.takeRetainedValue()

Calling takeRetainedValue() on an unmanaged object allows you to simultaneously grab a retained reference and burn an unbalanced retain so the memory isn’t leaked later. Now you’ll be able to use the input and output streams when you need them.

Next, in order for your app to react to networking events properly, these streams need to be added to a run loop. Do so by adding these two lines to the end of setupNetworkCommunication.

inputStream.schedule(in: .current, forMode: .commonModes)
outputStream.schedule(in: .current, forMode: .commonModes)

Finally, you’re ready to open the flood gates! To get the party started, add (again to the bottom of setupNetworkCommunication):

inputStream.open()
outputStream.open()

And that’s all there is to it. To finish up, head over to ChatRoomViewController.swift and add the following line to the viewWillAppear(_:) method.

chatRoom.setupNetworkCommunication()

You now have an open connection between your client app and the server running on localhost. You can go ahead and build and run if you want, but you’ll see the same thing you saw before since you haven’t actually tried to do anything with your connection.

streams tutorial

Joining the Chat

Now that you’ve set up your connections to the server, it’s time to actually start communicating something! The first thing you’ll want to say is who exactly you think you are. Later, you’ll also want to start sending messages to people.

This brings up an important point: since you have two kinds of messages, you’ll need to think up a way to differentiate them.

The Communication Protocol

One advantage of dropping down to the TCP level is that you can define your own “protocol” for deciding whether a message is valid or not. With HTTP, you need to think about all those pesky verbs like GET, PUT, and PATCH. You need to construct URLs and use the appropriate headers and all kinds of stuff.

Here we just have two kinds of messages. You can send,

iam:Luke

To enter the room and inform the world of your name.

And you can say,

msg:Hey, how goes it mang?

To send a message to everyone else in the room.

This is pure and simple.

This is also blatantly insecure, so maybe don’t use it as-is at work. ;]

Now that you know what the server is expecting, you can write a method on your ChatRoom class to allow a user to enter the chat room. The only argument it needs is their desired username.

To implement it, add the following method below the setup method you just wrote.

func joinChat(username: String) {
  //1
  let data = "iam:\(username)".data(using: .ascii)!
  //2
  self.username = username

  //3
  _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
}
  1. First, you construct your message using the simple chat room protocol.
  2. Then, you save off the name that gets passed in so you can use it when sending chat messages later.
  3. Finally, you write your message to the output stream. This may look a little more complex than you’d have assumed, but the write(_:maxLength:) method takes a reference to an unsafe pointer to bytes as its first argument. The withUnsafeBytes(of:_:) method provides you with a convenient way to work with an unsafe pointer version of some data within the safe confines of a closure.

Now that your method is ready, head over to ChatRoomViewController.swift and add a call to join the chat at the bottom of viewWillAppear(_:).

chatRoom.joinChat(username: username)

Now, build and run, enter your name, and then hit enter to see…

streams tutorial

The same thing?!

streams tutorial

Now, hold on, I can explain. Go ahead and go to your Terminal application. Right under Listening on 127.0.0.1:80, you should see Luke has joined, or something along those lines if your name happens not to be Luke.

This is good news, but you’d definitely rather see some indication of success on the phone’s screen…

Reacting to Incoming Messages

Luckily, the server takes incoming messages like the join message you just sent, and then sends them to everyone in the room, including you. As fortune would also have it, your app is already set up to show any type of incoming message as a cell in the ChatRoomViewController‘s table of messages.

All you need to do is use the inputStream to catch these messages, turn them into Message objects, and pass them off and let the table do its thing.

In order to react to incoming messages, the first thing you’ll need to do is have your chat room become the input stream’s delegate. First, go to the bottom of ChatRoom.swift and add the following extension.

extension ChatRoom: StreamDelegate {

}

Now that you’ve said you conform to the StreamDelegate protocol, you can claim to be the inputStream‘s delegate.

Go up and add the following line to setupNetworkCommunication().
and add the following directly before the calls to schedule(_:forMode:).

inputStream.delegate = self

Next, add this implementation of stream(_:handle:) to the extension.

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch eventCode {
    case Stream.Event.hasBytesAvailable:
      print("new message received")
    case Stream.Event.endEncountered:
      print("new message received")
    case Stream.Event.errorOccurred:
      print("error occurred")
    case Stream.Event.hasSpaceAvailable:
      print("has space available")
    default:
      print("some other event...")
      break
    }
}

Here, you’ve really just set yourself up to do something with the incoming events that can occur in relation to a Stream. The one you’re really interested in is Stream.Event.hasBytesAvailable since that means there’s an incoming message waiting to be read.

Next, you’ll write a method to handle these incoming messages. Below this function, add:

private func readAvailableBytes(stream: InputStream) {
  //1
  let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength)

  //2
  while stream.hasBytesAvailable {
    //3
    let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength)

    //4
    if numberOfBytesRead < 0 {
      if let _ = stream.streamError {
        break
      }
    }

    //Construct the Message object

  }
}
  1. First, you set up a buffer, into which you can read the incoming bytes.
  2. Next, you loop for as long as the input stream has bytes to be read.
  3. At each point you'll call read(_:maxLength:) which will read bytes from the stream and put them into the buffer you pass in.
  4. If the call to read returns a negative value, some error occurred and you bail.

This method needs to be called in the case the input stream has bytes available, so go up to the Stream.Event.hasBytesAvailable case in the switch statement inside stream(_:handle:) and call the method you're working on below the print statement.

readAvailableBytes(stream: aStream as! InputStream)

At this point, you've got a sweet buffer full of bytes! Before you finish this method you'll need to write another helper to turn the buffer into a Message object.

Put the following method definition below readAvailableBytes(_:).

private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>,
                                    length: Int) -> Message? {
  //1
  guard let stringArray = String(bytesNoCopy: buffer,
                                 length: length,
                                 encoding: .ascii,
                                 freeWhenDone: true)?.components(separatedBy: ":"),
    let name = stringArray.first,
    let message = stringArray.last else {
      return nil
  }
  //2
  let messageSender:MessageSender = (name == self.username) ? .ourself : .someoneElse
  //3
  return Message(message: message, messageSender: messageSender, username: name)
}
  1. First, you initialize a String using the buffer and length that's passed in.
    You just treat the text as ASCII, tell the String to free the buffer of bytes when it's done with them, and then split the incoming message on the : character so you can get the sender's name and the actual message as separate strings.
  2. Next, you figure out if you or someone else sent the message based on the name. In a real app you'd want to use some kind of unique token, but for now this is good enough.
  3. Lastly, you construct a Message with the parts you've gathered and return it.

To use your Message construction method, add the following if-let to the end of readAvailableBytes(_:).

if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) {
  //Notify interested parties

}

At this point, you're all set to pass the Message off to someone, but who?

Creating the ChatRoomDelegate Protocol

Well, you really want to tell the ChatRoomViewController.swift about the new message, but you don't have a reference to it. Since it holds a strong reference to the ChatRoom, you don't want to explicitly create a circular dependency and make a ChatRoomViewController property.

This is the perfect time to set up a delegate protocol. The ChatRoom doesn't care what kind of object wants to know about new messages, it just wants to tell someone.

Head to the top of ChatRoom.swift and add the simple protocol definition.

protocol ChatRoomDelegate: class {
  func receivedMessage(message: Message)
}

Next, add a weak optional property to hold a reference to whoever decides to become the ChatRoom's delegate.

weak var delegate: ChatRoomDelegate?

Now, you can go back and really complete readAvailableBytes(_:) by adding the following inside the if-let.

delegate?.receivedMessage(message: message)

To finish things off, go back to ChatRoomViewController.swift add the following extension that conforms to this protocol right below the MessageInputDelegate extension.

extension ChatRoomViewController: ChatRoomDelegate {
  func receivedMessage(message: Message) {
    insertNewMessageCell(message)
  }
}

Like I said earlier, the rest of the plumbing has already been set up for you, so insertNewMessageCell(_:) will take your message and take care of adding the appropriate cell to the table.

Now, go and assign the view controller to be the chatRoom's delegate by adding the following line right after the call to super in viewWillAppear(_:).

chatRoom.delegate = self

Once again, build and run, and then enter your name into the text field and hit enter.

streams tutorial

🎉 The chat room now successfully shows a cell stating that you've entered the room. You've officially sent a message to and received a message from a socket-based TCP server.

Sending Messages

Now that you've got the ChatRoom class set up to send and receive messages, it's time to allow users to send actual text back and forth.

Go back over to ChatRoom.swift and add the following method to the bottom of the class definition.

func sendMessage(message: String) {
  let data = "msg:\(message)".data(using: .ascii)!

  _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
}

This method is just like the joinChat(_:) method you wrote earlier, except it prepends msg to the text you send through to denote it as an actual message.

Since you want to send off messages when the inputBar tells the ChatRoomViewController that the user has hit Send, go back over to ChatRoomViewController.swift and find the MessageInputDelegate extension.

Here, you'll see an empty method called sendWasTapped(_:) that gets called at just such a time. To actually send the message, just pass it along to the chatRoom.

chatRoom.sendMessage(message: message)

And that's actually all there is to it! Since the server will receive this message and then forward it back to everyone, the ChatRoom will be notified of a new message the same way it is when you join the room.

Go ahead and build and run and try things out for yourself.

streams tutorial

If you want to see someone chatting in return, go to a new terminal window and enter:

telnet localhost 80

This will allow you to connect to the TCP server on the command line. Now you can issue the same commands the app uses to chat from there.

iam:gregg

Then, send a message.

msg:Ay mang, wut's good?

Congrats, you've successfully written a chat client!

Cleaning Up After Yourself

If you've ever done any programming with files, you should know that good citizens close files when they're done with them. Well turns out, like everything else in unix, an open socket connection is represented through a file handle, which means you need to close it when you're done, just like any other file.

To do so, add the following method after your definition of sendMessage(_:).

func stopChatSession() {
  inputStream.close()
  outputStream.close()
}

As you might have guessed, this closes the stream and makes it so information can't be sent or received. These calls also remove the streams from the run loop you scheduled them on earlier.

To finish things up, add this method call to the Stream.Event.endEncountered case in the switch statement.

stopChatSession()

Then, go back to ChatRoomViewController.swift and add the same line to viewWillDisappear(_:).

stopChatSession()

And with that, you're done. Profectu tuo laetamur! 👏

Where To Go From Here?

To download the completed chat app, click here.

Now that you've mastered (or at least seen a simple example of) the basics of networking with sockets, there are a few places to go to expand your horizons.

UDP Sockets

This streams tutorial is an example of communicating using TCP, which opens up a connection and guarantees packets will be delivered if possible. Alternatively, you can also use UDP, or datagram sockets to communicate. These sockets have no such guarantees, which means they're a lot faster and have less overhead. They're useful for applications like gaming. Ever experienced lag? That means you've got a bad connection and a lot of the UDP packets you should be receiving are getting dropped.

WebSockets

Another alternative to using HTTP for an application like this is a technology called WebSockets. Unlike traditional TCP sockets, WebSockets do at least maintain a relationship with HTTP and can be useful to achieve the same real-time communication goals as traditional sockets, all from the comfort and safety of the browser. Of course, WebSockets can be used with an iOS app as well, and we have just the tutorial if you're interested in learning more.

Beej's Guide To Network Programming

Finally, if you really want to dive deeper into networking, check out the free online book Beej's Guide to Network Programming. Questionable nickname choices aside, this book provides a really thorough and well-written explanation of socket programming. If you're afraid of C then this book may be a little intimidating, but then again, maybe today's the day you face your fears. ;]

I hope you enjoyed this streams tutorial, and as always, feel free to let me know if you have any questions or comments below!

The post Real-Time Communication with Streams Tutorial for iOS appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4384

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>