46

Real-Time Communication with Streams Tutorial for iOS [FREE]

 4 years ago
source link: https://www.tuicool.com/articles/FZBbimA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Update note : Brody Eller updated this tutorial for Xcode 10, iOS 12, and Swift 5. Cesare Rocchi wrote the original.

From the dawn of time, man has dreamed of better ways to communicate 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 wonder, “Why do I 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 something? That doesn’t really work very well with HTTP.

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 iOS streams tutorial, you’ll learn how to drop down a level of abstraction and use sockets directly to create a real-time chat room app.

IntroVideo.gif

Instead of each client having to check the server for new messages, your chat room app will use input and output streams that remain open for the duration of the chat session.

Getting Started

To begin, download the starter materials by using the Download Materials button at the top or bottom of the tutorial. The starter materials 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’ll need to get this server up and running in order to write a client for it.

Getting Your Server to Run

The server in the starter materials uses Go and is pre-compiled for you. If you’re not the kind of person who trusts a pre-compiled executable you found on the web, you can use the source code from the starter materials to compile it yourself.

To run the pre-compiled server, open Terminal, navigate to the starter materials directory and enter this command:

sudo ./server

When the prompt appears, enter your password. After putting your password in, you should see: Listening on 127.0.0.1:80 .

Note : You must run the server with privilege — thus the sudo command — because it listens on port 80. All port numbers less than 1024 are privileged ports, requiring root access to bind to them.

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 it before you can start. Open 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 build .

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 built for you.

vid1_1-1.gif

As shown above, DogeChat is currently set up to allow a user to enter a username and then go into a chat room.

Unfortunately, the last person to work on it wasn’t sure how to write a chat app, so all you get is the UI and basic plumbing; you must implement the networking layer.

Creating a Chat Room

To get started on the actual coding, navigate to ChatRoomViewController.swift . Here you can see you have a view controller that’s ready and able to receive strings as messages from the input bar. It can also display messages via a table view with custom cells you configure with Message objects.

Since you already have a ChatRoomViewController , it makes sense to create a ChatRoom class to take care of the heavy lifting.

Before you start to write a new class, make a quick list of what its responsibilities will be. You’ll want this class to take care of the following tasks:

  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, press Command-N to create a new file. Choose Swift File and name it ChatRoom .

Creating Input and Output Streams

Next, replace the code in ChatRoom.swift 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 declared the properties you’ll need to communicate.

username
maxReadLength

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 set up the basic structure of your class, 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. First, you set up two uninitialized socket streams without automatic memory management.
  2. Then you bind your read and write socket streams together and connect them to the socket of the host, which is on port 80 in this case.

    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 connecting to the local machine; 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 the server listens 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 that it creates internally.

Now that you’ve got initialized streams, you can store retained references to them by adding the following lines at the end of setupNetworkCommunication() :

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 can use the input and output streams when you need them.

Next, you need to add these streams to a run loop so that your app will react to networking events properly. Do so by adding these two lines to the end of setupNetworkCommunication() :

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

Finally, you’re ready to open the flood gates! To get the party started, add 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 viewWillAppear(_:) :

chatRoom.setupNetworkCommunication()

You now have an open connection between your client app and the server running on localhost.

You can build and run your app 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 yet.

vid1_1-1.gif

Joining the Chat

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

This brings up an important point: Since you have two kinds of messages, you’ll need to find 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 you 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, man?

to send a message to everyone else in the room.

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

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

To implement it, add the following method below the setup method you just wrote inside ChatRoom.swift :

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

  //3
  _ = data.withUnsafeBytes {
    guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
      print("Error joining chat")
      return
    }
    //4
    outputStream.write(pointer, maxLength: data.count)
  }
}
withUnsafeBytes(_:)
write(_:maxLength:)

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

chatRoom.joinChat(username: username)

Now, build and run your app. Enter your name, and then tap return to see…

vid1_1-1.gif

The same thing?!

63qEFba.png!web

Now, hold on, there’s a good explanation. Go to your terminal. Under Listening on 127.0.0.1:80 , you should see Brody has joined , or something similar if your name happens not to be Brody.

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

Reacting to Incoming Messages

The server sends incoming messages like the join message you just sent to everyone in the room, including you. As fortune would 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 inputStream to catch these messages, turn them into Message objects, and pass them off to 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.

To do this, go to the bottom of ChatRoom.swift and add the following extension.

extension ChatRoom: StreamDelegate {
}

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

Add the following line to setupNetworkCommunication() directly before the calls to schedule(in: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 .hasBytesAvailable:
      print("new message received")
    case .endEncountered:
      print("new message received")
    case .errorOccurred:
      print("error occurred")
    case .hasSpaceAvailable:
      print("has space available")
    default:
      print("some other event...")
    }
}

Handling the Incoming Messages

Here, you’ve set yourself up to do something with the incoming events related to Stream . The event you’re really interested in is .hasBytesAvailable , since it indicates there’s an incoming message to read.

Next, you’ll write a method to handle these incoming messages. Below the method you just added, 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, let error = stream.streamError {
      print(error)
      break
    }

    // Construct the Message object
  }
}
read(_:maxLength:)

You need to call this method in the case where the input stream has bytes available, so go to .hasBytesAvailable 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(stream:) .

private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>,
                                    length: Int) -> Message? {
  //1
  guard 
    let stringArray = String(
      bytesNoCopy: buffer,
      length: length,
      encoding: .utf8,
      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 treat the text as UTF-8, tell 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 treat the sender's name and the message as separate strings.
  2. Next, you figure out if you or someone else sent the message based on the name. In a production 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 the while loop in readAvailableBytes(stream:) , right underneath the last comment:

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

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

Creating the ChatRoomDelegate Protocol

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

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

At the top of ChatRoom.swift , add the simple protocol definition:

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

Next, to the top of the ChatRoom class, add a weak optional property to hold a reference to whomever decides to become the ChatRoom 's delegate:

weak var delegate: ChatRoomDelegate?

Now, you can go back to ChatRoom.swift and complete readAvailableBytes(stream:) by adding the following inside the if-let for message , underneath the last comment in the method:

delegate?.received(message: message)

To finish, go back to ChatRoomViewController.swift and add the following extension, which conforms to this protocol, right below MessageInputDelegate :

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

The starter project includes the rest of the plumbing for you, so insertNewMessageCell(_:) will take your message and add the appropriate cell to the table.

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

chatRoom.delegate = self

Once again, build and run your app and enter your name into the text field, then tap return.

vid2.gif

:tada: 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 set up ChatRoom to send and receive messages, it's time to allow users to send actual text back and forth.

In ChatRoom.swift , add the following method to the bottom of the class definition:

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

  _ = data.withUnsafeBytes {
    guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
      print("Error joining chat")
      return
    }
    outputStream.write(pointer, maxLength: data.count)
  }
}

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

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

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

chatRoom.send(message: message)

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

Build and run your app, then go ahead and try the messaging out for yourself.

vid3.gif

If you want to see someone chatting back, go to a new Terminal window and enter:

nc 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?

vid4_1.gif

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 Unix represents an open socket connection, like everything else, through a file handle. That means you need to close it when you're done, just like any other file.

To do so, in ChatRoom.swift add the following method after your definition of send(message:) :

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

As you might have guessed, this closes the streams and makes it so you can't send or receive information. 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 .endEncountered case in the switch statement inside stream(_:handle:) :

stopChatSession()

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

chatRoom.stopChatSession()

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

Where to Go From Here?

Use the Download Materials button at the top or bottom of this tutorial to download the completed project.

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 iOS streams tutorial is an example of communicating using TCP , which opens up a connection and guarantees packets will arrive at their destination if possible.

Alternatively, you can also use UDP , or datagram sockets to communicate. These sockets have no guarantees that packets will arrive, which means they're a lot faster and have less overhead.

They're useful for applications like gaming. Ever experienced lag? That means you have a bad connection and a lot of the UDP packets you should receive 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 achieve the same real-time communication goals as traditional sockets, all from the comfort and safety of the browser.

Of course, you can use WebSockets with an iOS app as well, and we havejust 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 .

This book provides a 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 iOS streams tutorial. As always, feel free to let me know if you have any questions or comments below!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK