Ruby, SSLSockets и расширенный формат сообщений Apple APN

Я пытаюсь реализовать поддержку расширенного формата сообщений push-уведомлений Apple в своем приложении Rails, и у меня возникают некоторые неприятные проблемы. Я явно не понимаю сокеты так, как я думал.

Моя основная проблема заключается в том, что если я отправляю все сообщения правильно, мой код зависает, потому что socket.read будет блокироваться, пока я не получу сообщение. Apple не возвращает ничего, если ваши сообщения выглядели нормально, поэтому моя программа блокируется.

Вот некоторый псевдокод того, как у меня это работает:

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

Очевидно, что здесь есть ряд проблем:

  1. Если я отправляю сообщения на большое количество устройств, а сообщение об ошибке отправляется на полпути обработки, то я не увижу ошибку до тех пор, пока не попытаюсь отправить сообщение на все устройства.
  2. Как упоминалось ранее, если все сообщения были приемлемы для Apple, мое приложение зависало на вызове чтения сокета.

Один из способов, которым я пытался решить эту проблему, - чтение из сокета в отдельном потоке:

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

Это не работает. Кажется, что данные никогда не читаются из сокета. Вероятно, это неправильное понимание того, как должны работать сокеты.

Другое решение, о котором я подумал, - это неблокирующий вызов чтения... но не похоже, что Ruby имеет неблокирующий вызов чтения в SSLSocket до версии 1.9... который я, к сожалению, не могу использовать прямо сейчас.

Может ли кто-нибудь, кто лучше разбирается в программировании сокетов, указать мне правильное направление?


person MikeQ    schedule 19.07.2010    source источник


Ответы (4)


cam верен: традиционный способ справиться с этой ситуацией — использовать IO.select

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

Это проверит ssl на «читабельность» в течение 5 секунд и вернет ssl, если он доступен для чтения, или nil в противном случае.

person Mando Escamilla    schedule 18.08.2010

Вы можете использовать IO.select? Он позволяет вам указать тайм-аут, чтобы вы могли ограничить количество времени, которое вы блокируете. Подробнее см. в спецификации: http://github.com/rubyspec/rubyspec/blob/master/core/io/select_spec.rb

person cam    schedule 17.08.2010

Меня это тоже интересует, это другой подход, к сожалению, со своими недостатками.

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

Кажется, это работает, и, поскольку я сохраняю постоянное соединение, я могу без проблем переподключиться в коде rescue и начать заново.

Однако есть некоторые проблемы. Основная проблема, которую я пытаюсь решить, — это отключения, вызванные отправкой неправильных токенов устройства (например, из сборок разработки). Если у меня есть 100 токенов устройств, которым я отправляю сообщение, и где-то посередине находится неправильный токен, мой код позволяет мне узнать, какой это был (при условии, что я предоставил хорошие идентификаторы). Затем я могу удалить неисправный токен и отправить сообщение на все устройства, появившиеся после неисправного (поскольку сообщение им не было отправлено). Но если неправильный токен находится где-то в конце 100, rescue не произойдет до следующего раза, когда я отправлю сообщения.

Проблема заключается в том, что код на самом деле не в реальном времени. Если бы я отправил, скажем, 10 сообщений на 10 неправильных токенов с этим кодом, все было бы в порядке, цикл прошел и никаких проблем не было бы сообщено. Кажется, что write() не ждет, пока все прояснится, и циклы пройдут до того, как соединение будет разорвано. В следующий раз, когда цикл будет запущен, команда write() завершится ошибкой (поскольку мы фактически были отключены с момента последнего раза), и мы получим ошибку.

Если есть альтернативный способ отреагировать на неудачное соединение, это может решить проблему.

person alleus    schedule 13.08.2010

Есть простой способ. После того, как вы напишете свои сообщения, попробуйте прочитать их в неблокирующем режиме:

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

Я использую его с MRI 2.1, но он должен работать и с более ранними версиями.

person sergeych    schedule 14.04.2014