4 votes

Ruby, SSLSockets et le format de message APN amélioré d'Apple

J'essaie de mettre en œuvre la prise en charge du format de message Push Notification amélioré d'Apple dans mon application Rails, et je rencontre quelques problèmes frustrants. Il est clair que je ne comprends pas les sockets autant que je le pensais.

Mon principal problème est que si j'envoie tous les messages correctement, mon code se bloque, car socket.read se bloque jusqu'à ce que je reçoive un message. Apple ne renvoie pas tout ce qui est si vos messages semblaient corrects, alors mon programme se bloque.

Voici le pseudo-code de mon fonctionnement :

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

Évidemment, cela pose un certain nombre de problèmes :

  1. Si j'envoie des messages à un grand nombre de dispositifs et que le message d'échec est envoyé à mi-chemin du traitement, je ne verrai l'erreur que lorsque j'aurai essayé d'envoyer le message à tous les dispositifs.
  2. Comme mentionné précédemment, si tous les messages étaient acceptables pour Apple, mon application se bloquerait sur l'appel de lecture de la socket.

J'ai essayé de résoudre ce problème en lisant le contenu du socket dans un thread séparé :

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

Cela ne semble pas fonctionner. Les données ne semblent jamais être lues depuis le socket. Il s'agit probablement d'un malentendu sur la façon dont les sockets sont censés fonctionner.

L'autre solution à laquelle j'ai pensé est d'avoir un appel de lecture non bloquant... mais il ne semble pas que Ruby ait un appel de lecture non bloquant sur SSLSocket avant la version 1.9... que je ne peux malheureusement pas utiliser pour le moment.

Quelqu'un ayant une meilleure compréhension de la programmation des prises pourrait-il m'indiquer la bonne direction ?

3voto

Mando Escamilla Points 948

Cam a raison : la manière traditionnelle de gérer cette situation est de IO.select

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

Cela permettra de vérifier ssl pour "lisibilité" pendant 5 secondes et retourner ssl si c'est lisible ou nil autrement.

1voto

cam Points 8882

Pouvez-vous utiliser IO.select ? Il vous permet de spécifier un délai d'attente, ce qui vous permet de limiter la durée du blocage. Voir la spécification pour plus de détails : http://github.com/rubyspec/rubyspec/blob/master/core/io/select_spec.rb

0voto

Martin Alléus Points 3939

Cela m'intéresse aussi, c'est une autre approche, malheureusement avec ses propres défauts.

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('CCN');
    puts "Command: #{command} Code: #{errorCode} Identifier: #{identifier}"
    // Before reconnecting, the problem (assuming incorrect token) must be solved
    break
  end
end

Cela semble fonctionner, et puisque je maintiens une connexion persistante, je peux sans problème me reconnecter dans l'interface de l'entreprise. rescue et recommencer à nouveau.

Il y a cependant quelques problèmes. Le principal problème que je cherche à résoudre est celui des déconnexions causées par l'envoi de jetons de périphérique incorrects (par exemple à partir de constructions de développement). Si j'ai 100 jetons de périphérique auxquels j'envoie un message, et que quelque part au milieu il y a un jeton incorrect, mon code me permet de savoir lequel c'était (en supposant que j'ai fourni de bons identifiants). Je peux alors supprimer le jeton défectueux et envoyer le message à tous les dispositifs qui sont apparus après le jeton défectueux (puisque le message ne leur a pas été envoyé). Mais si le jeton erroné se trouve quelque part à la fin de la centaine, le message rescue ne se produit pas avant la prochaine fois que j'envoie des messages.

Le problème semble être que le code n'est pas vraiment en temps réel. Si je devais envoyer, disons, 10 messages à 10 jetons incorrects avec ce code, tout irait bien, la boucle se déroulerait et aucun problème ne serait signalé. Il semble que write() n'attend pas que tout s'éclaircisse, et les boucles s'exécutent avant que la connexion ne soit interrompue. La prochaine fois que la boucle sera exécutée, la fonction write() échoue (puisque nous avons été déconnectés depuis la dernière fois) et nous obtenons l'erreur suivante.

S'il existe un autre moyen de répondre à l'échec de la connexion, cela pourrait résoudre le problème.

0voto

sergeych Points 326

Il existe un moyen simple. Après avoir écrit vos messages, essayez de les lire en mode non bloquant :

ssl.connect
ssl.sync = true # then ssl.write() flushes immediately
ssl.write(your_packed_frame)
sleep(0.5)      # so APN have time to answer
begin
  error_packet = ssl.read_nonblock(6) # Read one packet: 6 bytes
  # If we are here, there IS an error_packet which we need to process
rescue  IO::WaitReadable
  # There is no (yet) 6 bytes from APN, probably everything is fine
end

Je l'utilise avec MRI 2.1 mais il devrait aussi fonctionner avec les versions antérieures.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X