Tutorial: Networking and Bonjour on iPhone

n this tutorial, we are going to explore a simple chat application for the iPhone. It allows you to host your own chat room and advertise it on your local Wi-Fi network (in which case your app acts as a chat “server”) or find and join chat rooms hosted by other people on your network (acting as a chat “client”). Both announcement and discovery of chat rooms are implemented using Apple’s Bonjour protocol. The goal of this app is to show you how to use various networking-related frameworks available in the iPhone SDK version 2.x. The UI is minimal (consisting of only 3 simple views) – just enough to be able to play with the core functionality of the app without having to deal with complex UIKit code.

Source code for the tutorial

The app is called “Chatty.” Its source code is located here – it is being released under the MIT license, which means that you are free to modify and reuse it at will. Before we begin with the tutorial, download and open the zip file, and double-click Chatty.xcodeproj.

In order to see how Chatty works, you’ll need to run at least 2 instances of the app on your local network. The easiest way to get there is to use iPhone Simulator on your computer in conjunction with an iPhone or iPod Touch that is connected to the same Wi-Fi network.

Instead of going through the process of constructing this app step-by-step, we are going to examine various parts of completed source code. You don’t need to read the whole article – feel free to look through whatever sections interest you the most. Majority of the source code is pretty well commented, and the purpose of the tutorial is to cover “what to do” and “why do it this way”, as opposed to “how to do it” – that’s what the comments in the code are for.

To keep things interesting, at the very end of this article I’ll present you with a little puzzle that has to do with figuring out how to improve the way messages are sent over the network.

Lets begin with a quick overview of the basics.

Overview: Networking frameworks

The lowest level (closest to the metal, so to speak) networking framework available in the iPhone SDK is the BSD socket library. Most developers probably won’t need something this powerful. Many common tasks (sending and receiving data, connecting to HTTP servers etc) require quite a bit of coding in C if implemented using straight-up BSD sockets. Apple decided to hide some of the complexity by introducing another, higher level, framework called CFNetwork. Even though we are still in the C (as opposed to Objective-C) territory here, it has some clear advantages, with run-loop integration being one of them (more on this later). As a rule of thumb, anything that start with “CF”, which stands for “Core Foundation”, is in C. Objective-C and CocoaTouch comes into play with classes whose names start with “NS” (Next Step). A lot of components have both “CF” and “NS” implementations: CFReadStream vs NSInputStream, CFNetService vs NSNetService. Typically, “NS” versions are of higher level and easier to use, but that often means that the corresponding “CF” version is more flexible and feature-rich.

Overview: Sockets vs Streams

Socket represents a unique communication endpoint on the network. When your app needs to exchange data with another app, it creates a socket and uses it to connect to the other app’s socket. You can both send and receive data through the same socket. Each socket has an IP address and a port number (between 1 and 65535) associated with it. IP address uniquely identifies each computer on a given network and port number uniquely identifies a network socket on that computer.

Stream is a one-way channel through which data is transmitted serially. There are 2 types of streams: the ones into which you can write data, and the ones from which you can read. By itself, stream is just a buffer that temporarily holds data before or after its transmission. In order to actually deliver data somewhere meaningful, streams need to be tied to something (like a file, a memory location etc). In this tutorial, we’ll use streams that are paired up with sockets to allow our app to send data over network.

Overview: Bonjour

Bonjour is a protocol that allows devices or applications to find each other on the network. More precisely, it provides a way for an application to tell others what IP address and port they can connect to in order to communicate with it. In Bonjour terminology, such announcement is called publishing a service. Other apps can then look for services by browsing. Once an app finds a service that it would like to talk to, it resolves the service to find out what IP address and port number it needs to establish a socket connection to.

In the SDK, Bonjour can be used via NSNetServices and CFNetServices APIs.

Overview: Synchronous vs Asynchronous operations

Majority of interactions on a network involve waiting for something to happen: socket connections take time to get established; waiting for your peer to send you some data; waiting for your data to be delivered to the other side, and so on and so forth. If your app has only one thread of execution, you can’t really afford sitting inside of a fictional waitForDataToArrive() method call, because that means that all other important things, such as handling of various events, processing UI-related tasks etc, will most likely stall and your whole app will appear unresponsive and slow.

Here, you have 2 solutions: launch more threads or utilize the one thread in a more efficient manner. In this particular example, we’ll go with the latter, only because it allows us to use a clever piece of code called a run loop and learn to do things in an asynchronous (or non-blocking) manner, without wasting valuable resources, like extra threads and such. In some cases, you have to use more than one thread to accomplish something – but on a small device such as the iPhone, it’s better to keep your resource usage to a minimum.

Overview: Run loops

A run loop is literally a loop that runs in a thread – think of a more complex version of while(true){ processNextEvent(); }. Its job is to process events that arrive from elsewhere. Events represent things that happen in an app: “User touched screen”, “Network connection established”, “Timer has fired”, “Device’s orientation has changed” etc. You write the code to handle some of those and the OS takes care of everything else, like scheduling, receiving and sending them. You can also specify which additional sources of events, like sockets or streams, the run loop should process. This way, one thread is able to process many different things in an efficient manner, without wasting time on waiting for something specific to happen.

Of course, there is more that goes on behind the scenes and you can always find out the details by digging into the documentation that comes with the SDK.

Structure of the app

But enough with the theory, it’s time to get our hands dirty. Lets open up Chatty and see what it is made of.

All classes that comprise the app are separated into 3 categories (layers): “UI”, “Business Logic” and “Networking” (Each category is represented by a “File group” in the XCode project). We use delegates and protocols in order to wire all of the pieces together.

Business Logic deals with 2 different implementations of chat rooms: local and remote. Whenever we want to host our own chat, we’ll create a local chat room, which will automatically launch a server and announce it via Bonjour. Remote chat room, on the other hand, allows us to connect to another instance of the app on the same network and join the chat room hosted there.

The 3 Networking classes encapsulate everything that is needed to make Chatty work over a network.

Server class:

  • Create a server
  • Announce the server via Bonjour

Connection class:

  • Resolve Bonjour services
  • Establish connections to other servers
  • Exchange data via socket streams

ServerBrowser class:

  • Browse for other servers via Bonjour

Control flow in the app

Since we have quite a few classes interacting with each other in various ways, it might be helpful to step back and look at the “big picture” before we dive into the details. The diagram below shows how various layers of our application work with one another:

All user interactions get processed in the UI layer. Each time user wants to send a chat message (broadcastChatMessage:fromUser:), Business Logic layer decides whether to simply forward the message to the server (in case of the Remote chat room) or send a copy of the message to each client that’s connected (for Local chat room). Whenever a network message is received via a connection, Business Logic layer is notified, and, in addition to distributing a copy of the chat message to all connected clients (for Local chat room), it in turn passes the message on to the UI, which displays it to the user.

Socket+Streams+Buffers=Connection

The lowest level networking class that we deal with is called Connection. It encapsulates several things necessary for the inter-app communication:

  • 2 socket streams, one for writing and one for reading
  • 2 data buffers, one for each socket stream
  • various control flags and values

Here is how it all works together:

Lets go through various parts of that diagram. As I mentioned earlier, implementing network communication using sockets alone requires quite a bit of coding. Some of that work has already been done for us and we shall reuse as much of it as possible. That’s why we are using streams to help us along. Since streams are one-way channels, we will require 2 of those for each socket, in order to be able to both read and write data. We initialize the streams in the connect and setupSocketStreams methods of the Connection class. In Chatty, a socket can be created in 2 ways:

1. By connecting to another host by specifying its IP address (or host name) and port, or
2. By accepting a connection request from another app, in which case the socket is created automatically by the OS and is passed to us in the form of a native socket handle.

Regardless of how the connection is actually made, we use same exact code to initialize our streams, which is why setupSocketStreams method ends up being called from connect in each branch of the if-else tree there.

Next up: What are data buffers and why do we need those? Lets use an analogy (popular with some politicians) to understand how asynchronous data exchange works: think of a network connection as being a tube. You stuff bytes in one end, and, after spending some time traveling, they come out on the other side. This tube doesn’t have infinite capacity – it can hold only so much stuff at once. And the tube can’t deliver data instantaneously, either. This means that you can send up to certain number of bytes per each second through that connection. What if your application needs to send a message that’s bigger than what the connection can take in at once? What are you going to do with the data that didn’t fit into the tube? That’s right, you will temporarily hold it in the outgoing data buffer. The write stream will tell you when the pipe frees up and is able to take more bytes – that’s when the kCFStreamEventCanAcceptBytes event gets sent and we can check to see if there is any more data that needs to be transmitted (see methods writeStreamHandleEvent: and writeOutgoingBufferToStream).

When bytes arrive to us from another app, we also temporarily store them in a buffer. In order to understand why, lets take a look at how the messages are actually sent and received.

Format of the network messages

Lets define the rules of how different instances of Chatty actually communicate with one another. Some key assumptions:

1. Whenever a user wishes to send a message, we store the text of the message and the user’s name in a new instance of NSDictionary. We call this a chat message.
2. We also have a notion of a network message, which is a sequence of bytes that encode exactly one chat message.
3. We need an ability to send and receive network messages separately from one another.

Why do we need to differentiate between a “chat message” and a “network message”, you ask? That’s because most networking frameworks deal not with objects (like NSDictionary, NSNumber, NSString etc), but with sequences of bytes. Before an object can be sent over the wire, it has to be converted into an array of bytes. Luckily, Cocoa provides us with a pair of classes, called NSKeyedArchiver and NSKeyedUnarchiver, that make this task easy. To see how these classes can be used, take a look at the sendNetworkPacket: and readFromStreamIntoIncomingBuffer methods in Connection.m.

That takes care of assumptions number 1 and 2. What about the 3rd one? Depending upon how much user had to say in their chat message, length of the corresponding network message can vary. When we start sending our byte array via the write stream, how does the other side know how many bytes it is supposed to read before it can actually decode that network message into a chat message and display it to the user? In other words, how do we separate network messages from one another? In general, there are 3 ways to do that:

1. Make all messages have the same length. All we need to do in this case is read same exact number of bytes for each message.
2. Append a marker to each message that will indicate where the message ends, therefore signaling the reader when to stop reading.
3. Before transmitting the actual message, send some kind of a header that includes enough information to deduce how long the actual message is. The other side will then read this header, which will tell it how many more bytes it needs to extract before it gets to the end of the message.

Each of these approaches has advantages and disadvantages. I’ll let you think through them on your own. For Chatty, we are using method number 3, which utilizes message headers. Here is what one network message will look like:

In this case each message consists of 2 parts: header and body. Header has a constant length (4 bytes = 1 integer) and it tells us how long the body is. It’s fairly easy to send such message:

1. Convert NSDictionary object into a byte array and measure its length.
2. Write the integer (4 bytes) that represents the length of our byte array to the stream.
3. Write the actual byte array to the stream.

Reading of the message is a bit more complicated:

1. Read first 4 bytes and interpret those as an integer. Call that variable packetBodySize.
2. Read packetBodySize number of bytes. Stop when enough data has been received.
3. Turn received sequence of bytes into an NSDictionary object.

The complexity of the reading routine comes from the fact that the underlying networking layer might not deliver all of the bytes that comprise one network message in one shot, in which case it will be transmitted in chunks, spoon-feeding us a few bytes at a time (in theory). In that situation, we have to read as much as we can and return again later, when the stream signals us that more bytes have arrived. That’s why you see those ‘if’ statements that check how much data we have actually received so far. On the other hand, it is also possible that more than one network message will be delivered to us at once. That’s the reason why a big while( YES ) loop is dominating our readFromStreamIntoIncomingBuffer method. This also explains the need to have a separate buffer for incoming data: in case an incomplete message arrives, we need to temporarily store it somewhere until the rest is received and the puzzle can be completed, so to speak.

Creating a server

In order to allow others to interact with our instance of Chatty, we need to create a socket that will listen for connections (listeningSocket variable of the Server class) and instruct the networking subsystem of the operating system to notify us every time somebody tries to connect to that socket (we want the serverAcceptCallback function to be called for each new incoming connection). This is accomplished by the createServer method of the Server class.

How do other applications know how to connect to us? Each endpoint on the network must be identified by an IP address and port number. The address is determined by our network card – whenever iPhone or iPod Touch connects to the Wi-Fi network, it gets assigned an address automatically (or it can be set manually by the user). This means that we don’t have to worry about picking a value for the address (that’s what INADDR_ANY is for). But what about the other crucial piece of information – which port should we choose to listen on? Some servers must use pre-determined values in order to operate correctly (mail servers, web servers etc). We don’t have such strict requirements and we can let the OS assign a port that is not occupied by any other application. To do that, we will use port number 0. The only caveat here is that we will need to tell other apps which exact port we are listening on in order for them to connect to us. We find out our actual port number in part 3 of the createServer method.

In part 4, we register our listening socket as a source of events for the application’s run loop. This will let the OS know to execute serverAcceptCallback whenever a new connection request comes in.

Whenever serverAcceptCallback gets called, we try to create a new Connection object (see handleNewNativeSocket method in Server.m) and tie it in with the socket that was created by the OS in response to the new connection request. After that, our freshly established connection gets passed on to the delegate.

Announcing the server via Bonjour

Strictly speaking, Bonjour is not the only way for apps to find each other on a network. But it turns out to be one of the easiest, and that’s why we are using it in this particular example. Publishing a service involves creating an instance of the NSNetService class, just like we do in the publishService method located in Server.m

When you search for services on the network, you need to specify what type you are interested in. Chatty uses “_chatty._tcp.” service type to differentiate itself from other Bonjour services that might be published on the same network. Additionally, each service must have a name that’s unique among all published services of the same type. Publishing will fail if there is already another service on the network with the same name. The last important piece of information is the port on which our server is listening.

Just like with the sockets, we want to perform Bonjour operations asynchronously in order to avoid blocking our main thread for long periods of time. That’s why, before actually publishing the service, we need to schedule it in the run loop. This allows us to handle various events, such as “Publishing succeeded”, “Publishing failed” etc. In this case, we are only interested in hearing about things that went wrong, and that’s why we are assigning self to be netService’s delegate and implementing the netService:didNotPublish: method. All we want to do in case of such an error is to propagate it further up the delegate chain.

Browsing for servers via Bonjour

Finding other Chatty apps on the network is a relatively easy task. Take a look at ServerBrowser.m. Whenever start method gets called, we create and initialize an object of type NSNetServiceBrowser and kick off the search for “_chatty._tcp.” services . Every time a new service is found or a service that was found earlier disappears from the network, our delegate gets called. This allows us to maintain an accurate list of services and tell our delegate to refresh the list when appropriate.

Resolving services via Bonjour

Whenever the user picks one of the chat rooms from the list, the app has to figure out how to connect to that particular server. All we have at this point is an NSNetService object (see ChattyViewController.m, method named joinChatRoom:). This is where we need to resolve the service. We accomplish this by calling resolveWithTimeout: on that object and wait until either netService:didNotResolve or netServiceDidResolveAddress get called (see Connection.m). And since that operation is happening on the background, we are free to do other things in the meantime, like switching to the chat room view.

As soon as netServiceDidResolveAddress gets called, we can proceed with establishing a connection to that server, which consists of creating a socket by calling CFStreamCreatePairWithSocketToHost() from within the connect method and setting up streams in setupSocketStreams.

The End: What’s next?

I would argue that a chat application is pretty close to being the “hello world” of networking programs. And while it’s very simple, it is also easily extensible. What can you do with Chatty to make it bigger/better/faster/more usable? Here are some random ideas for you:

  • If you decide to send messages between applications that might be running on platforms other than iPhone, consider encoding messages using XML instead of the binary property list format that Chatty is using right now.
  • If you want to squeeze every last bit of performance out of the message sending/receiving code, consider introducing a separate thread that will handle all of the networking stuff. You will also need to reduce the size of network messages by changing the message format to something slimmer.
  • You can write a simple game on top of this example by replacing the chat view with something more like a playing field. At first, you probably don’t even need to get dirty with the networking code itself – just replacing LocalRoom/RemoteRoom delegates might be enough (for board games or card games).
  • If you need more features, such as support for UDP protocol, for example, take a look at the cocoaasyncsocket project on Google Code.

One more thing: A puzzle

Before we go, here is a fun little exercise for those of you that like puzzles (or, as software developers call them, “last minute requirement changes”):

Right now, each chat message that gets sent from client to the server contains name of the user that the message is coming from, which then gets displayed right next to the message itself (i.e. when John types “Hello world!”, everybody else see it as “John: Hello world!” on the screen). This is a bit redundant: if John sends 100 messages to the same chat server, every single one of those will also carry the string “John” in it. Your goal is to change the code so that you wouldn’t send user’s name more than once after you connect to a server, and it would still be displayed in the chat view, just like it is now. Think of it as an optimization aimed at reducing the bandwidth used by the app.

This shouldn’t be too hard to do if you understand how Chatty works. But if you would like some hints on how to get started on the solution, take a look at puzzle.txt, which is included in the source code archive.

You may also like -


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值