12

I was testing a HTTP servlet implementation (kindly shared by BalusC) which supports HTTP byte range requests.

I have found some peculiar differences between different HTTP clients, and was wondering if I am not missing anything. I have used a >2G mp4 video file for my tests and was capturing packets with Wireshark. This is roughly what happens:

  • Samsung Galaxy SII:

    • HTTP GET request for file comes, asking for byte range [0; <almost the end of the file>]
    • server responds, starts to stream the file
    • each subsequent chunk is served within the bounds of the same HTTP response. No new HTTP request is sent (unless the video is fast forwarded to a certain position). Streaming code chunk for this is quite simple, it reads RandomAccessFile input and writes to OutputStream output via byte[] buffer:

      while ((read = input.read(buffer)) > 0) {
          output.write(buffer, 0, read);
      }
      
  • iPad 1
    • HTTP GET request for file comes, asking for byte range [0; <almost the end of the file>]
    • server responds, starts to stream the file
    • iPad gets a chunk or two and then unilaterally decides to stop accepting bytes from the server and issues a separate GET request for the next chunk of the file. New range boundaries are e.g. [100, almost the end of the file]. The video is shown OK.
    • cycle repeats again from step 2. Left boundary always moves towards the end of the file.

I didn't investigate how exactly the connection is terminated. It could be that iPad stops sending TCP ACK packets, I suppose this doesn't really matter that much.

My problem is that for every terminated connection I get java.net.SocketException: Broken pipe exception. This not only pollutes the logs (which is a minor/solvable problem) but I believe this can hurt performance as raising exceptions is quite expensive. When watching simple video, the exception rate was around 1 exception/sec, but if the server had 100 concurrent users, then the JVM would probably be spending loads of time just calculating stack traces instead of doing real work.

I have also tested this on iPhone using iOS 6 and was able to observe the same behaviour as iPad 1. Just to reiterate, this does not happen on Samsung Android nor any desktop browsers I have tried, including Safari on a desktop Mac.

Questions:

  • is this a know bug/feature of iPad/iPhone?
  • is there a workaround for this?
mindas
  • 26,463
  • 15
  • 97
  • 154
  • Found [the same question asked by someone else](https://groups.google.com/forum/#!msg/youtube-api-gdata/V6RU2h9afBg/ibaJ7yOHjBAJ), as well as [another one](http://stackoverflow.com/questions/6094556/mobile-safari-makes-multiple-video-requests), unfortunately no useful answers so far :( – mindas Oct 03 '12 at 09:15

4 Answers4

3

IIRC, "broken pipe" simply means the other side received data after it closed its read end.

The most reasonable thing I can think of is that it's an attempt to not waste large amounts of bandwidth downloading video that never gets watched (perhaps something they've agreed with carriers, which I suspect is the reasoning behind the "live streaming" restriction:

"Video streaming content over a cellular network longer than 10 minutes must use HTTP Live Streaming and include a baseline 64 kbps audio-only HTTP Live stream."

The only other easy way to throttle the download is to stop read()ing and wait for the receive window to fill up, but that's not always easy to do (NSURLConnection really doesn't make this easy, for example).

If you're exceptionally lucky, the client will close its write end (such that the server will read() EOF) and wait for a little while before closing its read end. In this case, it might be safe to assume that the client no longer wants the rest of the download. RFC 2616 is a little hazy (it seems to forget that sockets can be closed in only one direction) but mentions "graceful close" (which according to Microsoft involves closing the write side and finishing reading from the read side until a timeout passes), but also says

Servers SHOULD NOT close a connection in the middle of transmitting a response, unless a network or client failure is suspected.

So if you know it's an iDevice and you read EOF, then it might be safe for the server to close the socket, provided you've thoroughly tested that it doesn't break anything — changing HTTP behaviour depending on User-Agent just seems like a terrible idea.

Alternatively, don't care. You can do U-A sniffing and ignore the exception if it's an iDevice (which seems less terrible than changing HTTP behaviour). The exception overhead is almost certainly negligible, and probably far lower than the overhead of printing it to a log. 100 exceptions a second is nothing. Profile it if you're unsure.

You could also file a bug with Apple, but as these things go, it's not particularly dubious network behaviour.

Community
  • 1
  • 1
tc.
  • 33,468
  • 5
  • 78
  • 96
  • I don't think I agree with your point about "wasting large amounts of bandwidth"; the client can simply delay TCP ACKs in order to get the next chunk later. And least it can do is to gracefully close the connection. Anyway, thanks for your answer - it is the best I've got. I awarded you the points but will keep the question open in case if someone provides a better explanation on why iPad/iPhone behaviour is broken (if it is). – mindas Oct 09 '12 at 09:02
  • @mindas The client *could* delay ACKs, but the server will still continue sending packets until the send window fills up, and again, this is not easy to accomplish with `NSURLConnection`. And again, the client *might* attempt a "graceful close" but timeout; without more information it's hard to tell. – tc. Oct 12 '12 at 22:18
1

You need to send a Accept Ranges header when streaming on iPhone. This may be causing your problem. Look at this post

MP4 plays when accessed directly, but not when read through PHP, on iOS

Community
  • 1
  • 1
Ascii Dude
  • 392
  • 2
  • 12
  • Thanks for the answer. In fact, I am sending `Accept-Ranges: bytes` header, otherwise the video wouldn't be playable at all. Arbitrary stream positioning also works, the only problem I have are terminated connections. – mindas Oct 02 '12 at 09:02
  • Don't know, I used the solution in that link and it works. What about Connection Keep-Alive header? Just googling, it looks like a common problem that may be unrelated to iOS: http://mikeschubert.com/2006/08/03/javanetsocketex/ – Ascii Dude Oct 02 '12 at 14:01
  • Solution works, but I want to understand why does iPad/iPhone behave differently than Android and desktop? If this was a database problem, it would also fail elsewhere, but now it only fails for Apple mobile devices. – mindas Oct 02 '12 at 16:23
  • Is it possible that iOS doesn't allow connections to stay alive as long as most browsers do? I would follow that line of inquiry. Or add some code to force the connection to stay alive, or check that it is still open before requesting the next chunk. – Ascii Dude Oct 02 '12 at 21:23
1

First of all,

spending loads of time just calculating stack traces

is not going to calculate if you are not gonna call get/printStackTrace(). turn it off the log or catch it and avoid it somewhere.

I had the same problems, it didn't go away for me either. Well, those are really stupid choices to make but you can use a load balancer which is accepting connections and redirecting it your server tomcat or glassfish whatever you are using. i observed this lack of broken-pipe behavior when i started to use ELB on AWS. NGINX or Apache could do some frontier communication for you.

I am saying this because even the operating system could be the reason why JVM doesn't receive proper TCP communication shutdown because of the JVM implementation on the OS.

Hayati Guvence
  • 718
  • 2
  • 6
  • 22
  • 2
    While it might not calculate the symbols, information needed to calculate the stack trace needs to be saved before the stack is overwritten; I've seen code override `Throwable.fillInStackTrace()` to avoid this overhead (the code was eventually rewritten to not throw an exception on every line of the image). – tc. Oct 08 '12 at 23:33
1

Coming back on our shared finding. Take a look at this discussion on the apple website. It seems that now this issue has been causing problems with iOS6 consuming too much data while streaming.

(In Dutch language but exactly the same issue reported here. Android doing 1 request, iOS doing multiple requests)

Time to re-test this stuff with iOS6.0.1 to see if they indeed FIXED the range request problems.

Just tested on my iPod Touch 5th Gen - iOS6.0.1: The range request is still requesting 0-1, then a few times 0-full file followed by smaller ranges. However it still looks messy

bart s
  • 5,068
  • 1
  • 34
  • 55
  • Thanks for sharing this. From what I understood, it looks like they have acknowledged this as a problem on one of the apps (podcast?) but not the browser. Anyway, please keep us posted if you find anything. I already gave up on this issue ;-( – mindas Nov 16 '12 at 14:26