Creating a Game with Bonjour - Client and Server Setup

In the first part of this series, I wrote about the basics of networking. In this article, we will start working with Bonjour and the CocoaAsyncSocket library by creating the foundation of our game.


Overview

In this article, we zoom in on establishing a connection between two devices running an instance of the game we are about to create. Even though this may sound trivial, there are quite a few components involved to make this work. Before we get our hands dirty, let me walk you through the process step by step.

In the previous article, I wrote about the client-server model. Before we create the game itself, we need to implement the client-server model in the application. Two devices each running a different instance of the game won't magically find each other on the network and start a game. One instance needs to act as the server and make it known to other instances on the network that they can join.

A common approach to solve this problem is by allowing players to host or join a game. The device of a player hosting a game acts as a server, while the device of a player joining a game acts as a client connecting to a server. The server provides a service and it can announce this on the network using Bonjour. The device of the player looking for a session to join searches the network for services also using Bonjour. When a player joins a game, the service is resolved and the client device attempts to establish a connection between the two devices so that the game can start.

If you are confused by the client-server model, then I recommend revisiting the first part of this series in which the client-server model is described in more detail.

Bonjour

What role does Bonjour play in this setup? Bonjour is in charge of publishing and discovering services on the network. Bonjour is also used for the discovery of printers or other devices connected to the network. It is important to remember that Bonjour is not responsible for establishing a connection between the server and the client. We make use of the CocoaAsyncSocket library to accomplish this task.

CocoaAsyncSocket

The CocoaAsyncSocket library comes into play when we have to deal with sockets, ports, and connections. It also helps us with sending data from one end of the connection to the other end, in both directions. Even though Bonjour is not responsible for establishing a connection between two processes, it does provide us with information that we need to establish the connection. As I mentioned earlier in this series, Bonjour and the CocoaAsyncSocket are a powerful combination as you will see in this article.

Testing

Testing is a key aspect of software development especially when networking is involved. To test the networking component of our application, you will need to run two instances of it. You could do this by running one instance on the iOS Simulator and a second instance on a physical device. One instance will serve as the server by hosting a game, whereas the other instance will serve as the client by searching the network for games to join.


1. Project Setup

Open Xcode and create a new project based on the Single View Application template (figure 1). Name the project Four in a Row, set Devices to iPhone, and double-check that ARC (Automatic Reference Counting) is enabled for the project (figure 2). We won't be using storyboards in this tutorial.

Creating a Game with Bonjour - Client and Server - Project Setup
Project Setup
Creating a Game with Bonjour - Client and Server - Project Setup
Project Setup

2. Adding CocoaAsyncSocket

Adding the CocoaAsyncSocket library is easy if you choose to use CocoaPods as I explained in a prior post. However, even without CocoaPods, adding the CocoaAsyncSocket library to your project isn't rocket science.

Step 1

Download the latest version of the library from GitHub and expand the archive. Locate a folder named GCD and drag both GCDAsyncSocket.h and GCDAsyncSocket.m into your Xcode project. Make sure to copy the files to the Xcode project and add them to the Four in a Row target (figure 3).

Creating a Game with Bonjour - Client and Server - Adding the CocoaAsyncSocket Library
Adding the CocoaAsyncSocket Library

Step 2

The CocoaAsyncSocket library depends on the CFNetwork and Security frameworks, which means that we need to link our project against both frameworks. Open your project in the Project Navigator on the left, select the Four in a Row target from the list of targets, choose the Build Phases tab at the top, and open the Link Binary with Libraries drawer. Click the plus button and link your Xcode project against the CFNetwork and Security frameworks (figure 4).

Creating a Game with Bonjour - Client and Server - Linking the Project against the CFNetwork and Security Frameworks
Linking the Project against the CFNetwork and Security Frameworks

Step 3

Before continuing, add an import statement to the project's precompiled header file to import the header file that we just added to our Xcode project. This makes sure that we can use the GCDAsyncSocket class throughout our project.

You may have noticed that we didn't add GCDAsyncUdpSocket.h and GCDAsyncUdpSocket.m to our project. As the file names indiciate, these files declare and implement the GCDAsyncUdpSocket class, which is in charge of working with the UDP protocol. Even though we will only be working with the TCP protocol in this series, keep in mind that the CocoaAsyncSocket library also supports the UDP protocol.


3. Creating the Host Game View Controller

Step 1

Whenever a user launches our application, she has two options to choose from: (1) hosting a game, or (2) joining a game that is hosted by another player. The interface is pretty straightforward as you can imagine. Open MTViewController.xib, add two buttons to the view controller's view, and give each button an appropriate title (figure 5).

Creating a Game with Bonjour - Client and Server - Creating the User Interface
Creating the User Interface

Step 2

Add two actions, hostGame: and joinGame:, to the view controller's implementation file and connect each action with the appropriate button in MTViewController.xib.

Step 3

When the user taps the button titled Host a Game, it will present the user with a modal view. Under the hood, the application will publish a service using Bonjour and the CocoaAsyncSocket library. When another player joins the game, the user hosting the game will be notified and the game can start. Create a UIViewController subclass and name it MTHostGameViewController (figure 6). Tell Xcode to also create a XIB file for the new view controller class (figure 6).

Creating a Game with Bonjour - Client and Server - Creating the Host Game View Controller
Creating the Host Game View Controller

Step 4

Add an import statement for the new view controller to MTViewController.m and implement the hostGame: action as shown below. When the user taps the top button, an instance of the MTHostGameViewController class is created, set as the root view controller of a navigation controller, and presented modally.

Step 5

Open MTHostGameViewController.m and implement the viewDidLoad method as shown below. All we do is call setupView, a helper method. In the setupView method, we add a button to the navigation bar to let the user cancel hosting a game and dismiss the view controller. As the implementation of the cancel: action shows, cancelling the hosting of a game is something that we will implement later in this tutorial. Build and run the application for the first time to see if everything works as expected.


4. Publishing a Service

When the user opens the host game view controller, the view controller should automatically publish a service that other instances of the application on the network can resolve. The class that we will be using for this purpose is NSNetService. As the documentation of NSNetService states, an instance of the NSNetService class represents a network service. Remember that a service isn't limited to a process or an application. Printers or other devices connected to the network can also make their services known by using Bonjour. It is this versatility that makes Bonjour great for consumers.

Instead of overwhelming you with more theory, let me show you what it takes to publish a service using Bonjour and the CocoaAsyncSocket library. Amend the viewDidLoad method as shown below.

Before we take a look at the startBroadcast method, we need to do some housekeeping. Add a class extension at the top of the host game view controller implementation file and declare two private properties, service, of type NSNetService, and socket, of type GCDAsyncSocket. In the class extension, we also conform the host game view controller to the NSNetServiceDelegate and GCDAsyncSocketDelegate protocols.

The service property represents the network service that we will be publishing using Bonjour. The socket property is of type GCDAsyncSocket and provides an interface for interacting with the socket that we will be using to listen for incoming connections. With the class extension in place, let's take a look at the implementation of the startBroadcast method.

We initialize an instance of the GCDAsyncSocket class and pass the view controller as the socket's delegate. The second argument of the initializer is a GCD (Grand Central Dispatch) queue, the dispatch queue of the application's main thread in this example. The CocoaAsyncSocket library has a queued architecture, which makes it extremely flexible and powerful. Even though the integration with GCD is an important addition to the CocoaAsyncSocket library, I won't cover this integration in this series.

The second step is to tell the socket to accept incoming connections by sending it a message of acceptOnPort:error:. We pass 0 as the port number, which means that it is up to the operating system to supply us with a port (number) that is available. This is generally the safest solution as we don't always know whether a particular port is in use or not. By letting the system choose a port on our behalf, we can be certain that the port (number) we get back is available. If the call is successful, that is, returning YES and not throwing an error, we can initialize the network service.

The order in which all this takes place is important. The network service that we initialize needs to know the port number on which to listen for incoming connections. To initialize the network service, we pass (1) a domain, which is always local. for the local domain, (2) the network service type, which is a string that uniquely identifies the network service (not the instance of our application), (3) the name by which the network service is identified on the network, and (4) the port on which the network service is published.

The type that we pass as the second argument of initWithDomain:type:name:port: needs to specify both the service type and the transport layer protocol (TCP in this example). The underscore prefixing the service type and the transport layer protocol is also important. The details can be found in the documentation.

With the network service ready to use, we set the delegate of the service and publish it. Publishing the service simply means advertising the network service on the network so that clients can discover it.

If we were to set all this up using the CFNetwork instead of the CocoaAsyncSocket library, we would have to write quite a bit of complex C code. As you can see, the CocoaAsyncSocket library makes this process a lot easier by providing a convenient Objective-C API.


5. Responding to Events

We don't need to implement every method of the NSNetServiceDelegate protocol. The most important ones are listed below. In these delegate methods, we simply log a message to the console to make sure that we can keep track of what is happening.

The GCDAsyncSocketDelegate protocol has quite a few delegate methods as you can see in the documentation. To not overwhelm you, we will implement one method at a time. The first method that is of interest to us is socket:didAcceptNewSocket:, which is invoked when the listening socket (the server socket) accepts a connection (a client connection). Because we only allow one connection at a time in our application, we discard the old (listening) socket and store a reference to the new socket in the socket property. To be able to use the incoming client connection, it is key to keep a reference to the new socket that is passed to us in this delegate method.

As the documentation states, the delegate and delegate queue of the new socket are the same as the delegate and delegate queue of the old socket. What many people often forget is that we need to tell the new socket to start reading data and set the timeout to -1 (no timeout). Behind the scenes, the CocoaAsyncSocket library creates a read and a write stream for us, but we should tell the socket to monitor the read stream for incoming data.

The second delegate method that we can implement at this time is a method that is invoked when the connection is broken. All we do in this method is cleaning things up.

Because networking can be a bit messy from time to time, you may have noticed that I have inserted a few log statements here and there. Logging is your best friend when creating applications that involve networking. Don't hesitate to throw in a few log statements if you are not sure what is happening under the hood.

Build and run your application in the iOS Simulator (or on a physical device) and click the top button to host a game. If all went well, you should see a message in Xcode's Console that indicates that the network service was successfully published. How easy was that? In the next step, we will create the second piece of the puzzle, discovering and connecting to a network service.

If you are running the application in the iOS Simulator and you have a firewall enabled on your Mac, then you should see a warning from the operating system asking for your permission to allow incoming connections for the application running in the iOS Simulator (figure 7). To make sure that everything works, it is important to accept incoming connections for our application.

Creating a Game with Bonjour - Client and Server - Telling the Firewall to Allow Incoming Connections
Telling the Firewall to Allow Incoming Connections

6. Discovering Services

For the second piece of the puzzle, we need to create another view controller class, a UITableViewController subclass to be precise (figure 8). Name the new class MTJoinGameViewController. There is no need to create a XIB file for the new class.

Creating a Game with Bonjour - Client and Server - Creating the Join Game View Controller
Creating the Join Game View Controller

The next steps are similar to what we did earlier. The implementation of the joinGame: action is almost identical to the hostGame: action. Don't forget to add an import statement for the MTJoinGameViewController class.

As we did in the MTHostGameViewController class, we invoke setupView in the view controller's viewDidLoad method.

The only difference is that we stop browsing for services in the cancel: action prior to dismissing the view controller. We will implement the stopBrowsing methods in a moment.

Browsing Services

To browse for services on the local network, we use the NSNetServiceBrowser class. Before putting the NSNetServiceBrowser class to use, we need to create a few private properties. Add a class extension to the MTJoinGameViewController class and declare three properties as shown below. The first property, socket of type GCDAsyncSocket, will store a reference to the socket that will be created when a network service resolves successfully. The services property (NSMutableArray) will store all the services that the service browser discovers on the network. Every time the service browser finds a new service, it will notify us and we can add it to that mutable array. This array will also serve as the data source of the view controller's table view. The third property, serviceBrowser, is of type NSNetServiceBrowser and will search the network for network services that are of interest to us. Also note that the MTJoinGameViewController conforms to three protocols. This will become clear when we implement the methods of each of these protocols.

Amend the view controller's viewDidLoad method as shown below. We invoke another helper method in which we initialize the service browser so that the application can begin browsing the network for services that we are interested in. Let's take a look at the startBrowsing method.

In the startBrowsing method, we prepare the table view's data source, the mutable array that stores the services discovered on the network. We also initialize an instance of the NSNetServiceBrowser class, set its delegate, and tell it to start searching for services. It is key that the network service type that you pass as the first argument of searchForServicesOfType:inDomain: is identical to the type that we passed in the MTHostGameViewController class. It is a good idea to make this a constant to prevent any typos.

By invoking searchForServicesOfType:inDomain:, the service browser starts searching the network for services of the specified type. Every time the service browser finds a service of interest, it notifies its delegate by sending it a message of netServiceBrowser:didFindService:moreComing:. The network service (NSNetService) that is discovered is passed as the second parameter and we add that object to our array of services. The last parameter of this delegate method, moreComing, tells us if we can expect more services. This flag is useful if you don't want to prematurely update your application's user interface, such as updating a table view. If the moreComing flag is set to NO, we sort the array of services and update the table view.

It is also possible that a previously discovered service stopped for some reason and is no longer available. When this happens, the netServiceBrowser:didRemoveService:moreComing: delegate method is invoked. It works in much the same way as the previous delegate method. Instead of adding the network service that is passed as the second argument, we remove it from the array of services and update the table view accordingly.

Two other delegate methods are of interest to us. When the service browser stops searching or is unable to start searching, the netServiceBrowserDidStopSearch: and netServiceBrowser:didNotSearch: delegate methods are invoked, respectively. All we do in these methods is clean up what we started in the stopBrowsing helper method as shown below.

The implementation of stopBrowsing isn't difficult. All we do is instruct the service browser to stop browsing and clean everything up.

Before we continue our journey, let's see all this in action. First, however, implement the table view data source protocol as shown below to make sure that the table view is populated with the services that the service browser finds on the network. As you can see in the tableView:cellForRowAtIndexPath: method, we display the name property of the network service, which is the name that we passed in the MTHostGameViewController when we initialized the network service. Because we didn't pass a name, it automatically uses the name of the device.

Don't forget to statically declare the cell reuse identifier that we use in the tableView:cellForRowAtIndexPath: table view data source method.

To test what we have built so far, you need to run two instances of the application. On one instance you tap the Host a Game button and on the other instance you tap the Join a Game button. On the latter instance, you should see a list with all the instances that are publishing the service that we are interested in (figure 9). Bonjour makes discovering network services very easy as you can see.

Creating a Game with Bonjour - Client and Server - Testing the Application
Testing the Application

7. Making the Connection

The last step of the process is making a connection between both devices when a player joins a service (game) by tapping a service in the list of discovered services. Let me show you how this works in practice.

When the user taps a row in the table view, we pull the corresponding network service from the array of services and attempt to resolve the service. This takes place in the tableView:didSelectRowAtIndexPath: table view delegate method. The resolveWithTimeout: method accepts one argument, the maximum number of seconds to resolve the service.

One of two things will happen, succes or failure. If resolving the service is unsuccessful, the netService:didNotResolve: delegate method is invoked. All we do in this method is cleaning everything up.

If the service resolves successfully, we try to create a connection by invoking, connectWithService: and passing the service as an argument. Remember that Bonjour is not responsible for creating the connection. Bonjour only provides us with the information necessary to establish a connection. Resolving a service is not the same as creating the connection. What needs to be done to establish a connection can be found in the implementation of connectWithService:.

We first create a helper variable, _isConnected, and a mutable copy of the addresses of the network service. You might be surprised that a service can have multiple addresses, but this can indeed be the case in some situations. Each element of the array of addresses is an NSData instance containing a sockaddr structure. If the socket property has not yet been initialized (no connection is active), we create a new socket just like we did in the MTHostGameViewcontroller class. We then iterate over the array of addresses and try to connect to one of the addresses in the array of addresses. If this is successful, we set _isConnected to YES. This means that the connection was successfully established.

When the socket did successfully establish a connection, the socket:didConnectToHost: delegate method of the GCDAsyncSocketDelegate protocol is invoked. Apart from logging some information to the console, we tell the socket to start monitoring the read stream for incoming data. I will cover reading and writing in more detail in the next article of this series.

This is also a good time to implement another method of the GCDAsyncSocketDelegate protocol. The socketDidDisconnect:withError: method is invoked when the connection is broken. All we do is logging some information about he socket to the console and clean everything up.

Run two instances of the application to make sure that everything works. It is important that on the device hosting the game the socket:didAcceptNewSocket: delegate method is invoked and on the device joining a game the socket:didConnectToHost: delegate method is invoked. If both delegate methods are called, then we know that we have successfully created a connection between both devices.


Loose Ends

There are still a few loose ends that we need to take care of, but that is something that we will do in the next instalment of this series. I hope you agree that getting up and running with Bonjour and the CocoaAsyncSocket library isn't that complicated. I do want to emphasize that the CocoaAsyncSocket library makes working with sockets and streams incredibly easy by providing an Objective-C API and making sure that we don't have to use the CFNetwork framework ourselves.


Conclusion

In this tutorial, we have laid the foundation of our game in terms of networking. Of course, we still have a lot of ground to cover as we haven't even started implementing the game itself. That is something we do in the next instalment of this series. Stay tuned.

Tags:

Comments

Related Articles