Core OS • iOS, OS X • 44:50
iOS and OS X both provide a number of abstractions to simplify and demystify networking and its inherent complexity. This session covers these abstractions in detail—from a TCP connection to NSURLConnection—and explains best design practices to tune for performance and power.
Speaker: Josh Graessley
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Hello, my name is Josh Graessley and I'm going to be going over networking best practices today. Networking is a little bit different from a lot of other things on the system where you're interacting with an environment that you don't necessarily have a lot of control over. And it can be a real challenge to know what you can modify to improve the situation and what you just have to simply live with.
You can't control the networks that your users are going to use your applications on, but you can control how well your applications perform on those networks. We're going to go into detail about how networking actually works so that you'll have a better idea about what you have control over and what you don't have control over.
Networking can be really tricky. When we deal with a network at the lowest layers, we have nothing but packets, and packets can be reordered, packets can become corrupt, packets can get dropped completely. Dealing with that complexity can be a real challenge. Fortunately, the system and the network in general provide a lot of abstractions that hide a lot of that complexity from your application and give you a chance to actually get something accomplished.
The only downside to these abstractions is that they tend to hide the true cost. There are some things that end up leaking through, and if you don't understand how these things work under the covers, then you can run into certain pitfalls. We're going to be going over the best practices to avoid those pitfalls. We'll also talk about the best layer that you can take advantage of.
First we're going to cover network performance. Network performance is something that people usually run into trouble with. We're going to go into details about how TCP works, and we'll also cover HTTP, and we'll cover a lot of the APIs that are available on the system to take advantage of these protocols. We're going to finish off with some mobility and cost issues and also cover some debugging tips and tricks.
First, network performance. There are two metrics to a network's performance: bandwidth and latency. Bandwidth is something that you read about all the time. All the ISPs advertise how many bits per second you can send or receive. Everyone has a pretty good idea about what bandwidth is. In this diagram, we have an iPhone on one side, and we have a server that we're talking to on the other side. We actually have to transit a bunch of different links in order to get to that server.
Our bandwidth is going to be limited by what we're going to call the bottleneck link. In this particular case, the DSL line is the one with the lowest bandwidth, so that's our bottleneck. That's basically dictating the maximum amount of bandwidth that we have available to communicate with that server.
Because it's not the first link in the hop, we can't necessarily find out how much bandwidth is available. There's also an issue that we might be sharing that link with a bunch of other devices, so we can't make assumptions about whether or not that bandwidth is going to stay the same the whole time. The only way to know how much bandwidth is available is to put some load on the network and then actually measure it and see how much bandwidth we got back.
When we're putting load on the network, it's really important to make sure that we use that load for something productive. We don't want to just download a whole bunch of bytes that we throw away because it's going to cost us power and it may end up costing the user some money.
It's also really important to remember that bandwidth may fluctuate over time, so we don't want to perform a test and then make some assumptions that our bandwidth is going to be the same all the way through. Netflix is actually a really good example of an application that handles changes in bandwidth well. Netflix uses HTTP live streaming, which has a bunch of different reference movies at different bit rates.
When Netflix starts, it picks a fairly conservative bit rate video to download, and then if it's playing back slower than it's downloading, it'll actually switch to a higher bit rate video to take advantage of the additional bandwidth. If it's actually downloading slower than it's playing back, it has to switch to a lower bit rate, lower quality video.
Netflix continues to do this over time and it adapts very well. Compression is about the only trick that you have to work around limited bandwidth. Other than that, you're just going to end up having to wait for things to come down. In my mind, the more interesting measure of a network's performance is the latency. The latency is the round trip time between your device and the server you're talking to.
This includes the time it takes to transit all the links to the server and to transit all the way back. This is usually measured in milliseconds. The round trip time includes the amount of time that the packets have to spend in queues as well as the time spent on links.
As the network congestion tends to go up, the queues will be a little bit more full and your packets will end up spending more time stuck in those queues before they get to go across the network. As the load and congestion on the network goes up, the round trip times are going to go up.
Latency is one place where your application, if it's designed properly, can do a lot more than just a few seconds. You can also use the high latency network to hide a high latency network from the user. This is one place where you can make some huge performance improvements. So we're going to go over some techniques. The first and most important thing to do is make sure that all of your networking is performed asynchronously. It's really important that you provide a very responsive user interface while you have any networking operation going on.
One trick you can use is placeholders. If you're going to show a list of users, you can show the whole list of users with placeholders next to them for icons that you don't have at the time. As the icons come in off the network, you can fill in the placeholder. The user gets the whole list very quickly, and they don't have to wait around.
Another really important technique is to use a single connection or the fewest possible connections and have a number of outstanding concurrent requests on those connections. If you're using HTTP, there's a technique called pipelining. If you can take advantage of pipelining, this will handle this for you. We'll go into this in a little bit depth later.
Another thing that's really important to remember is that the network will always add some delay to a request. If you know you need some resource, you can always add some delay to a request. If you know you need some resource, you should go ahead and request that as soon as you know you need that as opposed to waiting longer. All you'll do is add to the amount of time that it will take to get that resource back.
Finally, if you're using caching well, if you can fetch something out of the cache instead of having to go out to the network to get it, that comes back much, much quicker. There's a kind of interesting interaction between bandwidth and latency. If you were to write a fairly naive application that would send a packet, and then when that packet gets to the server, we get an acknowledgment back. When we get that acknowledgment back, we send another packet.
If we have a network with a 60 millisecond round trip time, we can send about 16 of these round trips per second. If a packet has about 1500 bytes in it, that works out to about 200 kilobits a second. Most people have much faster networks. The only way to take full advantage of all the throughput on a network is to have a lot of outstanding in-flight data at any given time.
You can use something called the bandwidth delay product to calculate the amount of information you need to have in flight in order to take full advantage of all the throughput. You can do this by multiplying the throughput. In this case, 10 megabits per second by the round trip time. In this case, 60 milliseconds. And you get about 60 kilobits that need to be in flight at any given time.
Most of the time, your applications won't know how much bandwidth is available. So you don't have to worry about the specific details here. The important thing to take away is that you need to keep your socket send buffer full. You need to keep data flowing all the time.
The system actually resized the socket send buffer and receive buffer based on the current bandwidth and delay. And I use the bandwidth delay product calculation. So as a quick summary of network performance, there are two metrics, bandwidth and latency. And bandwidth is limited by the minimum bottleneck link. In the case of latency, that's affected by all the different links you have to cross.
Hiding latency is where you can get big wins in your application. If you want to take full advantage of all the throughput that's available, you need to be aware of the bandwidth delay product and make sure that you have as much data in flight as possible. Now we're going to jump into TCP.
TCP stands for transmission control protocol, and normally when you're working with Internet traffic you're dealing with packets. TCP provides a nice abstraction. You get this virtual circuit or a bidirectional serial byte stream. There's no such thing as a connection on the Internet in general. Everything is sent as packets.
With a TCP connection, you can shove a whole bunch of bytes into the connection and come out in the server on the other side in the same order. If they get lost somewhere, TCP will take care of retransmitting it. If they become corrupt, TCP will make sure that they get dropped so they'll get retransmitted.
They come in out of order, TCP will reorder all of it for you. It simplifies a lot of your life. It also provides flow control. The Internet doesn't have any sort of built-in flow control. If you were to write an application that spewed packets at the fastest rate the interface would support, all of those packets will go out onto the network and then they'll get dropped at the next hop. There aren't a lot of great ways to get feedback to know how many of those packets are getting dropped or if any of them are getting dropped at all.
There is no such thing as a connection on the Internet. All of the connection is stored in state that's on both the device that's establishing the connection and the device the connection was established to. There are some exceptions like NATs and firewalls, but for the most part, nothing on the Internet knows about your connection. This is really nice because if some device that you're communicating through happens to crash or reboot, the packets can be rerouted around that and your connection remains running.
It's not all magic. There's a lot of work that happens under the covers to make this happen, and there are some costs involved. The first thing to know is that we can't connect directly to a host name. Most of the time we get from users or from some other place a host name that we want to connect to. We first have to resolve that to a list of addresses before we can establish a connection.
So to do that, we issue a DNS query and the DNS server responds with a reply, and that'll give us a list of addresses. We'll pick one of those addresses, and we'll use the TCP 3-way handshake to establish the connection. First, we'll send a SYN packet to the server. The server will respond with a SYN ACK, and then we'll respond with an ACK.
The important thing to take away from all of this is that for each of these operations—the DNS request and the TCP 3-way handshake—this will take a minimum of one round-trip time. So on a high-latency network, this can take a lot of time. If there's packet loss and we have to count on a retransmit timer, that can also add even more time and more cost. So anytime you establish a new connection, you can send a SYN packet to the server. The server will respond with a SYN ACK, and then we'll respond with an ACK. If you don't establish a new connection, there's a time cost.
If you can reuse existing connections, you can sometimes avoid all of that and get some big performance gains. Once we have a connection established, we need some way to keep track of the data that we're sending. TCP uses something called sequence numbers for this. When TCP sends a packet with some payload, it indicates the first sequence number for the first byte of that payload, as well as the length. From that, we can calculate the sequence number of all the bytes inside the payload.
TCP uses this for the in-order delivery. When TCP sends out of order, it can use these sequence numbers to properly reorder things. It can also detect if there are gaps of missing data. It can use these sequence numbers to acknowledge to the remote side, "I've got everything up to this sequence number," so the remote side knows it no longer needs to hold onto that data to retransmit it.
One thing to be aware of with the sequence numbers is that they don't start off with zero. They actually use a pseudo-random initial value. If you're looking at these values in tcpdump, tcpdump fortunately normalizes these, so it's a lot easier. You can get a relative value. sequence number instead of an absolute number.
Once we have a connection established and we know how we're going to send and receive data, we still don't know how fast the network is. TCP uses something called a slow start to effectively probe the network and slowly speed up until it starts to get some packet loss. So when you establish a new connection and you start to send a lot of data, you have to go through the slow start.
If we issue an HTTP get request to a server and it's for a large resource, the server can't just dump all the packets on at once. It starts out by sending us a few packets. When we acknowledge that, we'll get a few more, and this keeps going until we get up to the full speed available on the network.
This isn't exactly accurate. It's a little bit more complex than that. But the important thing to take away is that any time you create a new TCP connection, you're going to have to go through the slow start again, and that can take some time to speed up. So again, it's really important to reuse an existing TCP connection as opposed to creating a new one.
Looking at packets in TCP dump can be pretty difficult, especially if you've got 10,000 packets to sort through. TCP dump just shows you the number of packets that you've got. It shows you text. It shows you the time stamps. It shows you the sequence numbers and a lot of other information.
Trying to find problems can be a real challenge. There's a wonderful open source tool called TCP trace that generates these great graphs that help you visualize what's going on with the TCP connection. This is a TCP trace time sequence graph. I'm going to go over some of the details because we're going to look at two more of these graphs. At the beginning, we have a sequence packet -- or a SYN packet. Shortly thereafter, we get our SYN ACK from the server, and we immediately respond with an ACK packet. On the second packet, we get a SYN packet. On the top, we have the window.
This is the remote side telling us don't send us anything beyond this sequence number because I don't have anywhere to store it. On the bottom, we have a green light that's tracking the acknowledged number. This is the remote side telling us I've received every sequence up to this point.
Most importantly, we have actual packets or payload we can see in here. We can see when the packets are sent, and we can see how much data is in that packet based on the height of the line. We can also see the difference between when the packet was sent and when it was acknowledged by the server. There's a lot of other great information you can see by looking at these graphs. You can detect problems with retransmits or out-of-order data. You can detect problems where the server actually stopped reading, so your acknowledged data actually runs into the window.
If we look at a slow start, this is an SCP connection. At the beginning, there's a bit of key exchange, and then about 500 milliseconds in, we start sending a lot of data. And we can see that TCP does a nice job of ramping up, but it does take a few hundred milliseconds. And this is on a really fast network with a low round-trip time.
If the round-trip time is higher, there's higher latency, this will take even longer. Once TCP goes through slow start, it switches over to the congestion avoidance algorithm that takes pretty good advantage of all the bandwidth that's available on a given network at any given time. As the network bandwidth fluctuates, it will fluctuate with it. In this particular case, we were able to take advantage of all 50 megabit to the remote machine.
When a packet goes missing, TCP needs some way to retransmit that data. If there's a lot of data flowing at any given time, we've got a whole bunch of payload packets that are going from the server to the client here, and there's one in red that went missing.
Every packet that comes in after the one that went missing will trigger an immediate ACK, and that ACK will say, "I got everything up to this sequence number." When the server gets four acknowledgements for the same sequence number, it triggers something called a fast retransmit. The server immediately sends the missing segment and continues on with what it was doing. In this particular case, you can see we didn't really interfere with the speed at which we were transmitting data. Everything just kept going without a hiccup.
If we only have a little bit of data flowing in a TCP connection at any given time, there's a good chance we may have to count on the retransmit timer. In this particular case, we have a few payload packets and the last one went missing. We acknowledge all the data that we have received so far, but the remote side doesn't know whether that packet is still in flight somewhere so it waits for the retransmit timer to fire. Sometime later, at least 200 milliseconds later, it retransmits the missing segment.
The important thing to keep in mind here is that if you're using a bunch of different TCP connections and putting a little bit of data over each connection, you're going to be running into the retransmit timer a lot and that costs you. If you can try and move all that traffic onto a single TCP connection, you can take advantage of the fast retransmit timer and get some big performance wins.
Another thing to be aware of when you're working with TCP is something called a SOX proxy. Most of you probably don't have SOX proxies you have to deal with on a regular basis. But some of your users may run into one of these networks. On these networks, there's a firewall and the firewall will block you from directly connecting to the server that you're interested in. Instead of connecting to the server, you have to connect to the SOX proxy. You then tell the SOX proxy you're interested in connecting to some server.
The SOX proxy establishes the connection to the server for you and then it starts relaying data back and forth. When you're using TCP, it's really important to remember to support SOX proxies. If you're using the right frameworks in the system, it will handle SOX proxies for you. But if you're rolling your own or working at the socket's layer directly, you need to make sure you have SOX proxy support in there.
So as a wrap-up of the best practices when using TCP, first of all, use TCP. It provides a bunch of great services and people have invested decades into improving TCP. It's really unlikely you're going to do a better job than TCP does on a given network. It's really important to reuse existing TCP connections. New connections cost a lot of time. Between the three-way handshake, the slow start, and if there's packet loss, if this is a high latency network, this can add up.
It's also really important to keep a lot of data in flight. The last four packets on the TCP connection are the ones that are going to be sensitive to loss. If you can keep more than four packets in flight at any given time, you're going to be able to take advantage of the fast retransmit and things will flow much better.
One technique you can use for this is to double buffer operations. Instead of serial assigning your requests, where you request something and when the response comes back you request the next thing, you can issue a request for multiple things so you always have a lot of packets coming back to you at any given time.
We're going to move into HTTP now. HTTP provides a very nice request response-based protocol. All of the headers are based on text. Parsing text can be a little bit tricky. We have some great frameworks that will handle this for you so you can avoid some of the pitfalls of parsing text.
The nice thing about all of these headers are that they provide some very rich metadata, so you can get some great information. HTTP has support for caching. There's also support for an HTTP-specific proxy, which we'll go into. HTTP has a few interesting things—persistent connections and pipeline requests, which we'll go into in more detail. Josh Graessley If you're dealing with HTTP, you need to support SOX proxies, and you also need to support proxies called HTTP proxies. HTTP proxies are slightly different. When you talk to an HTTP proxy, you don't tell it the server you're interested in connecting to.
You actually hand the whole HTTP request to the HTTP proxy. The HTTP proxy will forward this to the server, or if it already has the resource you're interested in cached, it'll give the response directly back to you. If it comes back from the server, it'll forward the response back to you from the server.
HTTP 1.0 did not have support for persistent connections. With HTTP 1.0, it was very simple. You opened a TCP connection. You issued your request. When you got a response back, both you and the server closed the connection. If you wanted to do another request, you had to open another TCP connection. As we alluded to earlier, establishing new TCP connections is a really expensive operation.
They fixed this in HTTP 1.1 with persistent connections. With a persistent connection, we perform our three-way handshake and establish a connection. And then we issue our request. When we get the response back, we can go ahead and issue another request. The only downside to this is that because we're issuing all these requests serially, we're having to pay at least one round trip time for every request response.
HTTP has another technique called pipelining. With pipelining, you establish a TCP connection, and then you issue all the requests at the same time, and then the responses come back to back. Instead of taking us four round-trip times, this has taken us two round-trip times. This is a huge performance win. In addition, because all the responses are coming back to back, we can take advantage of fast retransmits. Any time you can, take advantage of HTTP pipelining.
If we look at this another way, on the top, we have a serial persistent connection, and on the bottom, we're using pipelining to get the same resource. First, we get the root document, we parse it, we realize we need five resources. If we're using the persistent connection, we issue a request for the first resource.
When that comes back, we issue a request for the second resource and so on and so forth. If the round trip times are small on a low latency network, this isn't a really big deal. As the latencies get a lot bigger, you can get a huge win by switching to pipelining where you get your root document, you parse it, you realize you need five resources, you issue the request for all five resources at the same time, and they come back back to back.
A really nice analogy is if you were going to bake some cookies. You pull out the cookbook, you open it up, you see that you need eggs. So you go to the refrigerator, you don't have any eggs. You drive to the store, you pick up the eggs, you come back, and you look at the next ingredient and it's sugar.
Now you go back into the car, you drive to the store, you pick up the sugar. It's a really easy way to get the next ingredient. You go to the store, you pick up the eggs, you come back, and you look at the next ingredient, and it's sugar. It's a really inefficient way to bake. You'll end up baking all day long.
If you were to put together a list of everything you needed, pay the round trip time to the store once, you can get all of those things at once and save yourself a lot of time. The iTunes store uses HTTP on the back end, and they did some performance tests and realized that by switching to using pipelining, they were able to get almost a 3X performance improvement on some networks. So this isn't some small, trivial improvement. This is huge.
So wrapping up HTTP best practices, when you're supporting HTTP, make sure that you support HTTP and SOX proxies. You may never run into one of these networks, but there's a good chance some of your customers will, and they'll really appreciate that you did the work to make sure it'll work there.
Be sure that you support persistent connections at the very least. If you can, add support for pipelining. Pipelining does require server-side support. Most servers, I believe, will support it, but you'll want to check with your IT department or if you're running your own server, just make sure that that's going to work.
Now we're going to go into the APIs that you can use to take advantage of these protocols on the system. For TCP, the best API is CfSocketStream. CfSocketStream provides a really nice CF run loop integrated TCP connection. It also works well with the CF types. It provides a nice connect by host name API.
If you're dealing with a sockets layer, you're responsible for converting a host name to a list of addresses, and then telling the system to connect to one of those addresses. With CfSocketStream, you just pass the host name into us, and we'll take care of the rest for you. We do some really sophisticated things under the covers to try and establish a connection in a short period of time.
When we resolve the host name, we'll get back a list of addresses. And we'll sort it based on statistics and what we think is going to be the best address. We'll start a connection attempt to what we believe is going to be the best address. We'll also start a timer based on how long we expect that connection to take.
If that connection doesn't succeed within the time we expect for some reason, we'll actually start a second connection attempt to the next best address while we leave that first connection attempt going. We'll keep doing this until we've run out of addresses to try or until we've established a connection. There's a new variant of this in iOS 6 that we're going to talk about briefly soon called cellular fallback.
You'll get all of this for free if you're using CFSocketStream. If you try and roll your own, it will take a long time and it's really hard. Some other benefits you get from using CFSocketStream: we have cellular and VPN on-demand support built in. So if you're using sockets directly and you need to bring up the cellular interface in order to connect, there's nothing at the sockets layer that will trigger that. The same goes for VPNs. Nothing at the sockets layer will trigger the VPN to dial. If you're using CFSocketStream, all of that is handled automatically.
In addition, CFSocketStream has built-in support for TLS and SSL. It handles both server and client-side authentication, so you don't have to roll your own code. We also have support for SOCKS proxies. You won't have to write your own SOCKS proxy support. Unfortunately, it's not enabled by default. Your application is responsible for calling a function to fetch the proxy settings out. And then you need to set the CFStream property SOCKS proxy's setting on the CFSocketStream before you establish the connection.
A new feature in iOS 6 is something called cellular fallback. With cellular fallback, when we have the primary interface as Wi-Fi, we'll start a connection attempt over Wi-Fi, and we'll set up a timer. And if for some reason that connection attempt doesn't succeed within a short period of time, we'll actually start a parallel connection attempt over cellular.
It turns out there are a lot of Wi-Fi networks out there that work for most things, but they block access to certain things. When we're on a Wi-Fi network, we'd really like to use that Wi-Fi network for everything that'll work over Wi-Fi, but we don't want to lose access to all those things that will still work over cellular just because we happen to be associated with this Wi-Fi network.
Cellular fallback lets us do that, where we'll try and connect over Wi-Fi, but if for some reason we can't, we'll end up connecting over cellular. If your application is paying attention to the WAN flag that you get back from SC Network Reachability to indicate whether you're going to connect over cellular or not, it might get a little bit tripped up by this new feature. We've added some new APIs in iOS 6 to make this a lot simpler.
If your application absolutely doesn't want to connect over the cellular interface, you can set the CFStream property "no cellular" before you connect, and whatever we do, we will not connect you over the cellular interface. If, on the other hand, all you're interested in is knowing after the fact, did I end up connecting over cellular, you can use the CFStream property "connection is cellular" to determine whether the connection ended up being established over the cellular network.
A lot of us use Cocoa to program, and we'd really like to be able to use the NSInputStream and NSOutputStream classes. Unfortunately, there's not a great way to create these classes directly in Cocoa. You'll need to drop down to the CF APIs to create these objects. What we recommend, if you've got a host name you want to connect to, use CFStreamCreatePair with socket to host.
And if you have a Bonjour service, go ahead and use CFStreamCreatePair with socket to NetService. These will give you back a CFInputStream and a CFOutputStream, which you can then use as NSInputStreams and NSOutputStreams. If you're using Arc, it's really important to remember to use CFBridgingRelease. Another thing to be aware of is NSHost. It's only available on OS X and not on iOS. It does do asynchronous blocking resolve wherever it's instantiated, and for that reason, we recommend that you avoid it and do your name lookups in another way.
If you're trying to use HTTP or HTTPS, the absolute best API on both iOS and OS X is NSURLConnection. There is an older API called CFHTTPStream. Unfortunately, CFHTTPStream has some behaviors that we'd like to improve, but we can't because some clients may be depending on them. If you have existing code that's using CFHTTPStream, we strongly recommend you switch over to using NSURLConnection. If you're writing new code from scratch, please use NSURLConnection.
NSURLConnection provides a very nice asynchronous event-based API. It has a lot of fantastic features. It supports persistent connections. It'll handle pipelining for you. It handles HTTP as well as TLS and SSL authentication. It handles a lot of caching. It'll handle cookies. And it'll also take care of SOCKS and HTTP proxies for you automatically. You won't have to write any code to fetch settings or anything else. It just does what it's supposed to. It's great.
NSURLConnection goes through a fairly simple lifecycle. First, you create an NSURL request. The NSURL request says, here's what I'm interested in. And then you pass the NSURL request in to create the NSURL connection. You also specify a delegate. The delegate will get called as soon as the request has been sent. And it'll get notified when the response comes back. And it'll get notified as data comes in. And finally, it'll get notified when you've got the end of the resource or you've got an error.
When you're working with NSURLConnection, it's important to remember that an NSURLConnection does not necessarily correspond to a TCP connection. NSURLConnection supports persistent connections and pipelining. It means we're reusing a single TCP connection for multiple requests. NSURLConnection does this by maintaining a pool of connections. So when you put together a request, it will actually go and dynamically allocate one of the existing connections, if there is one, or create a new one from scratch.
In some cases, we may actually be giving you a response directly out of the cache, so there is no TCP connection involved in there whatsoever. NSURLConnection has support for HTTP authentication built in. It also handles SSL and TLS authentication. For HTTP, it supports the basic digest NTLS and OS X Kerberos methods. We also handle automatic proxy authentication. Your process doesn't necessarily get access to the credentials to authenticate to the proxy.
NSURLConnection will take care of all of it for you. It's really easy. If for some reason your application does want to get involved in authentication, in your delegate, you can specify a "will send request for authentication" challenge method, and that will give you a chance to intervene in the default behavior.
NSURLConnection does support pipelining, but pipelining is not enabled by default. It's your application's responsibility to enable pipelining. When you set up your NSURL request, you can indicate that you're interested in pipelining by calling "set HTTP should use pipelining." We strongly encourage you to do so. Any time you can take advantage of pipelining, you'll get some huge performance wins.
NSURLConnection has automatic cache built in. It's a single shared cache. You can get it using NSURLConnection shared URL cache. It's shared with everything in your process, so that includes other frameworks that you may call that use NSURLConnection under the covers. NSURLConnection doesn't really know about the workload of your particular application, so it uses some defaults that may not be tuned particularly well. But it works well for the default case.
There is a small in-memory cache that is about four megabytes and that overflows into about a 20 megabyte on disk cache. There is a limit that a single item in the cache can't take more than 5% of the cache. Your application has a chance to do some tuning. You can set the memory capacity as well as the disk capacity.
We recommend that you use TCP dump and watch the traffic. Go ahead and run your application through a typical workload and see if it's requesting the same resources over again. If it is, increase the cache until that no longer happens. We have some new functionality in iOS 6. We now support on disk cache for HTTPS resources.
In the past, HTTPS resources were limited only to the in-memory cache. If your application is interested in displaying web content, actually rendering it, the absolute best API is WebKit. It has support for everything we've talked about so far. It has caching support, it handles proxies, and on iOS, it actually does pipelining by default. It's a great API. If you're going to render web content, go ahead and use this. Before we move on, I kind of wanted to cover timeouts quickly.
There is no such thing as a good timeout. Round trip times on a network can be anywhere from less than a millisecond to well over 30 seconds. Giving up on some operation can be a huge disservice to the user. If you're trying to order WWDC tickets and the application had set a 10-second timeout, and because the servers tend to be a little bit slower to respond during that two hours where the WWDC tickets sell out, the server takes maybe 12 seconds to respond.
If your application timed out in 10 seconds, it's going to be a little bit slower to respond. If your application did not respond in 10 seconds but it would have succeeded if it held on for another two seconds, you're really doing your user a disservice. So it's important to remember not to use timeouts and cancel things. It may seem like a good idea to let the user know, ah, this probably isn't going to work. But if there's a chance it would have worked, you haven't really helped out the user.
The best technique is to allow the user to handle the timeout. Let the user indicate to you they're interested in something and then keep working on it until they indicate they're no longer interested. This doesn't mean hammer the network and try and connect every minute until you get a connection. You need to be smart about it.
There's a great API in the system called reachability that lets you register for notifications of when the network changes. Go ahead and make your attempt and if for some reason it doesn't succeed, watch for a reachability change notification. Don't force the user to hit refresh at some point. When you're on a new network, go ahead and make an attempt again. Which brings us to mobility. And we'll also cover a little bit of cost issues.
We have a number of challenges. The computers these days fit in pockets. And they have multiple interfaces. Some MacBooks still have two interfaces, Ethernet and Wi-Fi. A lot of iOS devices have both Wi-Fi and cellular. Unfortunately, the operating system can't just pick up connections from one interface and move them to another. We would love to be able to do that. But the technology just doesn't exist today.
It's really important for your application that you respond to network change notifications and do the right thing and migrate. There's a number of use cases where this is really important. If you have somebody on a train and they've got a cellular data connection, when they go into a tunnel, they're going to lose the Internet connectivity.
If your application throws up a dialogue and says, "Oh, my gosh, the Internet's gone," and they have to dismiss it, they get frustrated. They probably already know that the Internet's gone and they're not really happy about it and having a dialogue in their face isn't making them any happier. So if you can avoid dialogues, that's good.
The other thing that's really important is when they get back out of the tunnel, you don't want to have them have to babysit your application. As soon as you see that there's network connectivity again, you should go ahead and reissue the request. Don't make them wait for the connectivity to come back and then hit a refresh button. That's not a great experience.
Another place where it's really important to handle this correctly is when you're migrating from a cellular network to a Wi-Fi network. If your application is running on a device that has cellular connectivity, your connections are going over the cellular interface, when you come into a home environment, you're going to have a Wi-Fi interface that will come up, but your connection doesn't move over automatically. Your existing connection over cellular will continue to work just fine.
The downside is it's probably a little bit slower and it's probably a lot more expensive, both in a power perspective and cost. It's your application's responsibility to watch for change notifications and make the transition from the cellular interface to the Wi-Fi interface when you get notified that that new interface is available.
So we're going to go over how you handle that. When you're trying to establish a connection, you want to create a reachability object and then go ahead and make your attempt. Don't check the reachability object to determine whether something's available. It can be deceptive in some cases. You want to just go ahead and make the attempt anyhow. If you succeed, you're done. If you don't succeed, you want to wait for another reachability change notification before you attempt again. You keep doing this until you've finally established a connection or until the user has indicated they're no longer interested.
Once you have a connection established, you still have to stay on your toes. You want to keep using the reachability object that you created initially to establish the connection. You'll get reachability change notifications as new interfaces become available. This is the case where you might have an established connection over cellular, but you come home and Wi-Fi becomes available. When you get that reachability change notification, you want to try and establish a new connection. If you have an existing old connection, you want to leave that alone and continue doing work on that old connection.
If the new connection succeeds, you want to transition all of your work over to the new connection and tear down the old connection. If the new connection doesn't succeed, no big deal. You've still got your old connection working. You just go back to waiting for another reachability change and continue working on the old connection.
There are a lot of issues related to cost when working on these devices. They have a finite amount of battery power. And a lot of times the data can actually cost the user a lot of money. So we need to be very careful about whatever load we put on the network.
There are a number of techniques we can use to solve problems related to cost. The primary thing we do is avoid sending and receiving any data that we don't have to. The first technique is to cache data. If we can fetch something out of the cache, we don't have to pay the money or the power to get it off of the network.
Another thing that's really important is making sure that we're fetching appropriately sized resources. If we're on a device that doesn't have a retina display, there's no point in getting a higher resolution image that's going to take more bytes to get across the network. We can save the user some money and some power by fetching the lowest resolution image that's appropriate to their device.
Another thing that's really important is making sure that we only fetch what's necessary. There's no point in fetching data that the user is never going to use. They have to pay for it. They're going to lose a little bit of battery life for it. There is sort of a contradictory technique for saving power, which is to fetch data in bursts.
When it comes to power on the cellular interface, the cellular interface goes into a low power mode when it isn't sending data, and it does that based on an aisle timer. When it is sending data, it's in a very, very low power mode. When it's sending data, it's in a very, very high power mode that drains the battery fairly quickly. What you want to do is try and increase the amount of time you're spending in low power mode and avoid the amount of time you're spending in high power mode.
So you can do that by trying to fetch all of your data in a burst so you keep it in high power mode but you get as much done as possible during that time, and then you stop all network activity. Pandora takes advantage of this. When Pandora runs, it downloads the whole music file at the beginning. As it plays back, the interface falls back into low power mode, and it saves a lot of battery power that way.
Pandora could have been written differently so that it trickled the data at the bit rate that it played back the music, but that would have forced the cellular interface to stay in a high power mode for a very long time, and the battery life would have been abysmal. Debugging network applications can be a real challenge.
So we're going to talk about some techniques. One of the problems that you run into with debugging networking applications is that if you stop your application in the debugger and you're connected to a server, there's a good chance the server's going to timeout the connection to you. Timeouts are evil.
Some of the techniques that make debugging a lot easier for networking applications is to add a lot of logging, or basically resort to printf debugging. The system provides a whole lot of logging for you. It's turned off by default, but with some techniques, you can turn a lot of that on, and you may be able to diagnose problems without even adding extra logging to your own application. So we're going to go over the ways that you can turn a lot of the logging on.
Another thing that's really important when debugging networking problems is packet traces. It's really important to look at what your application is actually putting on the network. Looking at that in a TCP dump can be a real challenge. Fortunately there's a great tool out there called TCP trace which we'll cover as well. One technique that can be really useful for debugging is to have a TLS or SSL bypass. It's really important that if you put this in you don't ship it because it can become a security vulnerability.
But when you're looking at packet traces, if you're looking at ciphertext, it can be really hard. But if you're looking at the clear text, it can make things a lot more clear and easier to understand. Another issue you run into when debugging networking applications is that the network environment that you run on is probably a really nice, fast network with low latency. This is a pretty unrealistic experience.
Our users are out there running on more realistic networks that have very high latency, they have very little bandwidth. They're running into all kinds of problems that we don't run into on a daily basis. Fortunately we have some tools so you can simulate the less... The more realistic, less performant networks so you can make sure that your application will perform well in those environments.
For CF network, there's a lot of debugging that you can get... A lot of logging you can get. You can enable a CF network diagnostic environment variable on OS X and that will force CF network to log all of this information to the system log as well as a file.
This is a log information related to connecting and data flow if you're using CFSocketStream or NSURLConnection or basically anything above CFSocketStream. If you set this to 1, it includes a lot of internal CF network and event and state information. If you set it to 2, it adds a bunch of information about how it decides to reuse existing connections or establish new connections, if you're using the persistent connections or pipelining. If you set this to 3, it has a very powerful feature where it will actually log all the TLS and SSL decrypted content. This can be really useful for debugging problems, but it's also very dangerous.
Be very careful about how you use this. A lot of times, the things that are encrypted with TLS and SSL are done so for a reason, and with this logging turned on, it'll end up going to the system log, which anybody can get access to. So you need to be kind of careful with this. You can set another environment variable, CFNetworkIO_LOG_FILE, and that'll force all of the clear text to get output to that file instead.
If you're having trouble establishing a connection, a lot of that work happens at the lib system network layer. If you're using CF socket stream or any of those things, all of that goes through something in lib system network to establish connections. This is where the host name to address resolution occurs, this is where all of our logic for how we sort those addresses and when we decide to try and connect to different addresses happens.
You can enable the logging on OS X by using a default write, you can disable it by using the default delete, and then you can actually display the logs using syslog -w. Another really important technique is actually using TCP dump to look at your packet traces. We have a new feature in Mountain Lion, with TCP dump you can show the process ID of the process that generated the traffic. You can use the -k command to do that.
If you're using -k with the -w option to write all of the packets out to a file, you can run into some problems as -k does use a new -w. If you're using -k with the -w option to write all of the packets out to a file, you can run into some problems as -k does use a new lib pcap next generation format, which may not be compatible with other things.
So be careful if you're using -k with -w. On the iOS we don't have a command line, so there's no way to run tcpdump, but it's still really important to get packet traces. Fortunately, we have something called remote packet capture. If you have the developer tools installed, you can hook up an iOS device using USB to your Mac, and you can create a virtual interface for capturing packets.
You do this using rvicontrol -s, and you pass in the UDID of the iOS device. This will create a virtual interface like rvi0, which you can run tcpdump on. tcpdump will then show you all of the IPv4 and IPv6 packets going in and out of all of the interfaces on that iOS device. When you're all done, you can use rvicontrol -x and pass in the UDID, and it will tear down the virtual interface.
When you're using tcpdump, by default it will output a summary of each packet to the standard out. You can also have it write to a file instead of outputting the packets. You can use the -w option to do so. And then that packet file can be used for later analysis or to be passed into tcp trace. tcp trace is a fantastic open source tool. You can download a copy. It builds fine on OS X. It comes from tcptrace.org.
With TCP Trace, we're going to capture packets using tcpdump and we're going to use -w to save all those packets into a file. We're going to create xpl or xplot files using TCP Trace from that packet trace file that we created. If you have just TCP packets, you can run tcptrace -g and pass in the packet file. If you have HTTP traffic that isn't encrypted using TLS or SSL, there's a really nice module in TCP Trace called XHTTP.
This will actually do some analysis on the HTTP connections and it'll show you if you have a persistent connection or pipelining, it'll break things down by each connection and show you when requests go out and when responses come back on each of those connections. It's really useful. When you run this tool, it'll spit out the xpl files that you can then open in jplot or xplot. Finally, it's really important to test on realistic network environments.
The network link conditioner is available on Mac OS X. If you download the link conditioner, you can test it on any network environment. If you download the hardware I/O tools for Xcode from developer.apple.com, there will be a network system preference pane in the disk image. You double click on that, it installs itself. You can turn it on and you can simulate network environments that are worse than your current one. Unfortunately, you can't simulate better network environments. We're still working on that.
On iOS, we have a new feature in iOS 6 where you can actually enable this on the device. If you enable your device for development in Xcode, on the device, you can go into settings under developer, there should be a network link conditioner item. You can use it to run a network link conditioner. You can use that to turn the network link conditioner on and off. And you can set what kind of network you want to simulate.
[Transcript missing]
There are some related sessions. The next session in this room is simplifying networking with Bonjour. The best way to discover network services is using Bonjour, and they're going to go over the best practices for using Bonjour and talk about some cool technologies there. So as a quick summary, go ahead and use TCP. It provides a lot of great services. Be sure to reuse existing TCP connections.
Try and perform multiple concurrent requests on a single connection where you can. This lets you take advantage of fast retransmits and hide latency. Be sure to make sure that you support SOCKS proxies. If you're using HTTP, make sure that you're using pipelining everywhere you can. Make sure that you're also supporting SOCKS and HTTP proxies. Thank you very much.