views:

178

answers:

3

I'm trying to implement support for Apple's enhanced Push Notification message format in my Rails app, and am having some frustrating problems. I clearly don't understand sockets as much as I thought I did.

My main problem is that if I send all messages correctly, my code hangs, because socket.read will block until I receive a message. Apple doesn't return anything if your messages looked OK, so my program locks up.

Here is some pseudocode for how I have this working:

cert = File.read(options[:cert])
ctx = OpenSSL::SSL::SSLContext.new
ctx.key = OpenSSL::PKey::RSA.new(cert, options[:passphrase])
ctx.cert = OpenSSL::X509::Certificate.new(cert)

sock = TCPSocket.new(options[:host], options[:port])
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
ssl.sync = true
ssl.connect

messages.each do |message|
  ssl.write(message.to_apn)
end

if read_buffer = ssl.read(6)
  process_error_response(read_buffer)
end

Obviously, there are a number of problems with this:

  1. If I'm sending messages to a large number of devices, and the failure message is sent half way through processing, then I'm not going to actually see the error until I've already tried to send to all devices.
  2. As mentioned earlier, if all messages were acceptable to Apple, my app will hang on the socket read call.

One way I've tried to solve this is by to reading from the socket in a separate thread:

Thread.new() {
  while data = ssl.read(6)
    process_error_response(data)
  end
}

messages.each do |message|
  ssl.write(message.to_apn)
end

ssl.close
sock.close

This doesn't seem to work. Data never seems to be read from the socket. This is probably a misunderstanding I have about how sockets are supposed to work.

The other solution I have thought of is having a non-blocking read call... but it doesn't seem like Ruby has a non blocking read call on SSLSocket until 1.9... which I unfortunately cannot use right now.

Could someone with a better understanding of socket programming please point me in the right direction?

A: 

I'm interested in this too, this is another approach, unfortunately with it's own flaws.

messages.each do |message|
  begin
    // Write message to APNS
    ssl.write(message.to_apn)
  rescue
    // Write failed (disconnected), read response
    response = ssl.read(6)
    // Unpack the binary response and print it out
    command, errorCode, identifier = response.unpack('CCI');
    puts "Command: #{command} Code: #{errorCode} Identifier: #{identifier}"
    // Before reconnecting, the problem (assuming incorrect token) must be solved
    break
  end
end

This seems to work, and since I'm keeping a persistent connection, I can without problems reconnect in the rescue code and start over again.

There are some issues though. The main problem I'm looking to solve is disconnects caused by sending in incorrect device tokens (for example from development builds). If I have 100 device tokens that I send a message to, and somewhere in the middle there is an incorrect token, my code lets me know which one it was (assuming I supplied good identifiers). I can then remove the faulty token, and send the message to all devices that appeared after the faulty one (since the message didn't get sent to them). But if the incorrect token is somewhere in the end of the 100, the rescue doesn't happen until the next time I send messages.

The problem seams to be that the code isn't really in real time. If I were to send in, say, 10 messages to 10 incorrect tokens with this code, everything would be just fine, the loop will go through and no problems will be reported. It seems that write() doesn't wait for everything to clear up, and the loops runs through before the connection is terminated. The next time the loop will be run, the write() command fails (since we've actually been disconnected since the last time) and we would get the error.

If there is an alternative way to respond to the failed connection, this could solve the problem.

alleus
+1  A: 

Can you use IO.select? It lets you specify a timeout, so you could at limit the amount of time you block. See the spec for details: http://github.com/rubyspec/rubyspec/blob/master/core/io/select_spec.rb

cam
+2  A: 

cam is correct: the traditional way to handle this situation is with IO.select

if IO.select([ssl], nil, nil, 5)
  read_buffer = ssl.read(6)
  process_error_response(read_buffer)
end

This will check ssl for "readability" for 5 seconds and return ssl if it's readable or nil otherwise.

mando