Я взаимодействую с API, который принимает запросы POST через HTTPS и отвечает XML. Генерация данных удаленным сервером занимает много времени, а это означает, что мой локальный клиент ждет несколько минут между отправкой запроса POST и получением ответа. Иногда ответ возвращается, как и ожидалось, но в других случаях Ruby (2.3.1p112) после долгой паузы вызывает следующее исключение:
Exception occurred: Errno::ECONNRESET - Connection reset by peer
/usr/share/ruby/openssl/buffering.rb:178:in `sysread_nonblock'
/usr/share/ruby/openssl/buffering.rb:178:in `read_nonblock'
/usr/share/ruby/net/protocol.rb:154:in `rbuf_fill'
/usr/share/ruby/net/protocol.rb:136:in `readuntil'
/usr/share/ruby/net/protocol.rb:146:in `readline'
/usr/share/ruby/net/http/response.rb:40:in `read_status_line'
/usr/share/ruby/net/http/response.rb:29:in `read_new'
/usr/share/ruby/net/http.rb:1437:in `block in transport_request'
/usr/share/ruby/net/http.rb:1434:in `catch'
/usr/share/ruby/net/http.rb:1434:in `transport_request'
/usr/share/ruby/net/http.rb:1407:in `request'
Из любопытства я попытался сделать те же запросы с помощью cURL и обнаружил, что каждый раз получаю ответ, никогда не сталкиваясь со сбросом соединения, инициированным удаленным хостом. Запросы cURL и Ruby выполнялись на одном компьютере, поэтому я решил, что это не низкоуровневая система или проблема с сетью. Пытаясь найти какую-то разницу, я запустил Wireshark и посмотрел на пакеты, отправляемые туда и обратно во время запросов.
Сначала с Руби:
1302978 3988.123708 [local-ip] → [remote-ip] SSL 286 Client Hello
1302981 3988.189299 [remote-ip] → [local-ip] TLSv1.2 463 Server Hello, Certificate
1302982 3988.189388 [local-ip] → [remote-ip] TCP 54 55265 → 443 [ACK] Seq=233 Ack=1361 Win=65535 Len=0
1302983 3988.189389 [local-ip] → [remote-ip] TCP 54 55265 → 443 [ACK] Seq=233 Ack=2721 Win=65535 Len=0
1302984 3988.189389 [local-ip] → [remote-ip] TCP 54 55265 → 443 [ACK] Seq=233 Ack=3130 Win=65535 Len=0
1302985 3988.194978 [remote-ip] → [local-ip] TLSv1.2 396 Server Key Exchange, Server Hello Done
1302986 3988.195047 [local-ip] → [remote-ip] TCP 54 55265 → 443 [ACK] Seq=233 Ack=3472 Win=65535 Len=0
1302987 3988.195812 [local-ip] → [remote-ip] TLSv1.2 180 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
1302988 3988.257643 [remote-ip] → [local-ip] TCP 60 443 → 55265 [ACK] Seq=3472 Ack=359 Win=35098 Len=0
1302989 3988.272131 [remote-ip] → [local-ip] TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
1302990 3988.272200 [local-ip] → [remote-ip] TCP 54 55265 → 443 [ACK] Seq=359 Ack=3523 Win=65535 Len=0
1302994 3988.339450 [remote-ip] → [local-ip] TCP 60 443 → 55265 [ACK] Seq=3523 Ack=945 Win=34512 Len=0
1302995 3988.339455 [remote-ip] → [local-ip] TCP 60 443 → 55265 [ACK] Seq=3523 Ack=1206 Win=34251 Len=0
1406186 4376.828078 [remote-ip] → [local-ip] TCP 54 443 → 55265 [RST, ACK] Seq=3523 Ack=1206 Win=9300 Len=0
Затем с помощью cURL:
51468 268.062527 [local-ip] → [remote-ip] SSL 292 Client Hello
51472 268.125416 [remote-ip] → [local-ip] TLSv1.2 463 Server Hello, Certificate
51473 268.125530 [local-ip] → [remote-ip] TCP 54 53819 → 443 [ACK] Seq=239 Ack=1361 Win=65535 Len=0
51474 268.125531 [local-ip] → [remote-ip] TCP 54 53819 → 443 [ACK] Seq=239 Ack=2721 Win=65535 Len=0
51475 268.125531 [local-ip] → [remote-ip] TCP 54 53819 → 443 [ACK] Seq=239 Ack=3130 Win=65535 Len=0
51476 268.132509 [remote-ip] → [local-ip] TLSv1.2 396 Server Key Exchange, Server Hello Done
51477 268.132604 [local-ip] → [remote-ip] TCP 54 53819 → 443 [ACK] Seq=239 Ack=3472 Win=65535 Len=0
51479 268.158620 [local-ip] → [remote-ip] TLSv1.2 129 Client Key Exchange
51481 268.220146 [remote-ip] → [local-ip] TCP 60 443 → 53819 [ACK] Seq=3472 Ack=314 Win=35137 Len=0
51482 268.220216 [local-ip] → [remote-ip] TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
51483 268.281636 [remote-ip] → [local-ip] TCP 60 443 → 53819 [ACK] Seq=3472 Ack=365 Win=35086 Len=0
51484 268.281642 [remote-ip] → [local-ip] TLSv1.2 105 Change Cipher Spec, Encrypted Handshake Message
51485 268.281718 [local-ip] → [remote-ip] TCP 54 53819 → 443 [ACK] Seq=365 Ack=3523 Win=65535 Len=0
51487 268.344020 [remote-ip] → [local-ip] TCP 60 443 → 53819 [ACK] Seq=3523 Ack=770 Win=34681 Len=0
62427 328.950531 [local-ip] → [remote-ip] TCP 54 [TCP Keep-Alive] 53819 → 443 [ACK] Seq=769 Ack=3523 Win=65535 Len=0
62435 329.012004 [remote-ip] → [local-ip] TCP 60 [TCP Window Update] 443 → 53819 [ACK] Seq=3523 Ack=770 Win=65535 Len=0
72644 389.584563 [local-ip] → [remote-ip] TCP 54 [TCP Keep-Alive] 53819 → 443 [ACK] Seq=769 Ack=3523 Win=65535 Len=0
72647 389.647037 [remote-ip] → [local-ip] TCP 60 [TCP Keep-Alive ACK] 443 → 53819 [ACK] Seq=3523 Ack=770 Win=65535 Len=0
Соответствующее отличие, которое я обнаружил, заключается в том, что cURL отправляет пакет TCP Keep-Alive каждую минуту, ожидая ответа, сообщая серверу, что он все еще активен и хочет сохранить свое соединение открытым. Ruby Net::HTTP не отправляет эти пакеты и в конечном итоге получает TCP RST (сброс) от удаленного хоста, что вызывает исключение Errno::ECONNRESET.
Итак, мой вопрос: есть ли способ настроить Net::HTTP для отправки этих пакетов TCP Keep-Alive и оставить мое соединение открытым? Спасибо!
Изменить: мне удалось обойти это, переключившись на Typhoeus (который обертывает cURL) и передав параметр tcp_keepalive: true
в мои запросы, который является флагом, который указывает cURL отправлять проверки активности.
Я оставлю этот вопрос открытым на тот случай, если кто-то поймет, можно ли получить такое же поведение от Net::HTTP.