HttpURLConnection : почему для каждого фрагмента возникает 5-секундная задержка, когда указан заголовок Expect: 100-Continue?

Я экспериментирую с заголовком RFC Ожидать в java HttpURLConnection, который отлично работает, за исключением одной детали.

Существует 5-секундный период ожидания между отправкой тела Режим фиксированной длины или между каждым фрагментом в Режим потоковой передачи фрагментов.

Вот клиентский класс

public static void main(String[] args)throws Exception
 {
   HttpURLConnection con=(HttpURLConnection)new URL("http://192.168.1.2:2000/ActionG").openConnection();
   
   //for 100-Continue logic
   con.setRequestMethod("POST");
   con.setRequestProperty("Expect", "100-Continue");
  
   //responds to 100-continue logic
   con.setDoOutput(true);
   con.setChunkedStreamingMode(5);
   con.getOutputStream().write("Hello".getBytes());
   con.getOutputStream().flush();
   con.getOutputStream().write("World".getBytes());
   con.getOutputStream().flush();
   con.getOutputStream().write("123".getBytes());
   con.getOutputStream().flush();

   //decode response and response body/error if any
   System.out.println(con.getResponseCode()+"/"+con.getResponseMessage());      
   con.getHeaderFields().forEach((key,values)->
   {
    System.out.println(key+" = "+values);
    System.out.println("=================== = ");
   });      
   try(InputStream is=con.getInputStream()){System.out.println(new String(is.readAllBytes()));}
   catch(Exception ex)
   {
    ex.printStackTrace(System.err);
   
    InputStream err=con.getErrorStream();
    if (err!=null)
    {
     try(err){System.err.println(new String(is.readAllBytes()));}
     catch(Exception ex2){throw ex2;}
    }  
   } 
   con.disconnect();
  }

Я загружаю 3 чанка. На стороне сервера принимается 5 пакетов данных

  1. Все заголовки. ответьте 100 Продолжить

  2. 3 куска. Для каждого фрагмента ответьте 100 Продолжить

  3. Последний фрагмент [длина 0]. ответить 200 ОК

Вот тестовый сервер

final class TestServer
{
 public static void main(String[] args)throws Exception
 {
  try(ServerSocket socket=new ServerSocket(2000,0,InetAddress.getLocalHost()))
  {
   int count=0;
   try(Socket client=socket.accept())
   {
    int length;
    byte[] buffer=new byte[5000];
    InputStream is=client.getInputStream();
    
    OutputStream os=client.getOutputStream();
    while((length=is.read(buffer))!=-1)
    {
     System.out.println(++count);
     System.out.println(new String(buffer,0,length));
     System.out.println("========= = ");
  
     if (count<5)
     {
      os.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes());
      os.flush();
     }
     else
     {
      os.write("HTTP/1.1 200 Done\r\nContent-Length:0\r\n\r\n".getBytes());
      os.flush();
      break;
     }   
    }
   }
  }
 }
}

Выход:

1
POST /ActionG HTTP/1.1
Expect: 100-Continue
User-Agent: Java/17.0.2
Host: 192.168.1.2:2000
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-type: application/x-www-form-urlencoded
Transfer-Encoding: chunked


==========   //5 seconds later
2
5
Hello

==========   //5 seconds later
3
5
World

==========  //5 seconds later
//this and the last chunk come seperatly but with no delay
4
3
123

==========
5
0


==========

Я проверил каждый метод тайм-аута в моем объекте подключения.

System.out.println(con.getConnectTimeout());
System.out.println(con.getReadTimeout());

Оба возвращают 0

Так откуда же взялась эта 5-секундная задержка?

Я использую JDK 17.0.2 с Windows 10

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
36
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

3 Chunks. For each chunk respond with 100 Continue

Expect: 100-Continue работает не так. Код вашего сервера совершенно не соответствует тому, что вы пытаетесь сделать. На самом деле ваш серверный код совершенно не подходит для HTTP-сервера в целом. Он даже не пытается анализировать HTTP-протокол. Ни заголовки HTTP, ни фрагменты HTTP, ничего. Есть ли причина, по которой вы не используете реальную реализацию HTTP-сервера, такую ​​как собственный HttpServer Java?

При использовании Expect: 100-Continue клиент должен отправить ТОЛЬКО заголовки запроса, а затем ОСТАНОВИТЬСЯ И ЖДАТЬ несколько секунд, чтобы увидеть, отправляет ли сервер ответ 100 или нет:

  • Если сервер отвечает 100, клиент может завершить запрос, отправив тело запроса, а затем получить окончательный ответ.

  • Если сервер отвечает чем-либо, кроме 100, клиент может немедленно завершить свою работу, не отправляя тело запроса вообще.

  • Если ответ не получен, клиент может завершить запрос, отправив тело запроса и получив окончательный ответ.

Весь смысл Expect: 100-Continue заключается в том, чтобы клиент запрашивал разрешение перед отправкой большого тела запроса. Если серверу не нужно тело (например, заголовки описывают неудовлетворительные условия и т. д.), клиенту не нужно тратить усилия и пропускную способность на отправку тела запроса, который будет просто отклонен.

HttpURLConnection имеет встроенную поддержку для обработки ответов 100, но оговорки см. в Как дождаться ответа Expect 100-continue в Java с помощью HttpURLConnection. Также см. JDK-8012625: Неправильная обработка HTTP/1.1 "Ожидание: 100-продолжить" в HttpURLConnection.

Но ваш серверный код, как показано, нуждается в серьезной переработке для обработки HTTP должным образом, не говоря уже об обработке запросов должным образом, разбитых на фрагменты.

Как Я сказал в Моем вопросе. Я экспериментирую с этим заголовком. Я никогда не собирался ничего анализировать или использовать какой-либо существующий сервер, поскольку я хотел проанализировать, как выглядят пакеты запроса и ответа, и поэтому я создал MRE, чтобы продемонстрировать проблему. В отчете об ошибке было сказано, что проблема решена, но я не смог найти код, как ее решить. Если бы вы могли опубликовать этот код здесь для отладки, я отмечу это как принятый ответ.

Sync it 16.03.2022 20:15

Код клиента выглядит нормально, по большей части. Проблема на стороне сервера. Как я уже сказал, ваш серверный код совершенно не подходит для HTTP и должен быть переписан, чтобы фактически анализировать протокол HTTP, вы не можете избежать этого. Пока вы не исправите это, ваша проблема с 5-секундной задержкой неактуальна, поскольку у вас неисправная тестовая среда.

Remy Lebeau 16.03.2022 20:28

Так что спасибо @Remy Lebeau за понимание того, как правильно анализировать этот специальный заголовок. Я заметил, что после создания базового синтаксического анализатора и после правильного ответа на заголовки chunked[Transfer-Encoding:chunked header] и потоковой передачи фиксированной длины[Content-Length header], что иногда мой клиент все еще зависал и время от времени ждал 5 секунд, а иногда и работать без проблем.

После нескольких часов отладки класса com.sun.www.http.HttpURLСоединение я понял, что еще один недостаток был больше не на стороне сервера, а на стороне клиента кода. Особенно этот бит

con.getOutputStream().write("Hello".getBytes());
con.getOutputStream().flush();
con.getOutputStream().write("World".getBytes());
con.getOutputStream().flush();
con.getOutputStream().write("123".getBytes());
con.getOutputStream().flush();

Я ошибочно предположил, что получитьвыходнойпоток() в этом классе будет кэшировать возвращаемый поток вывода, но на самом деле он каждый раз возвращает новый поток вывода.

Поэтому мне пришлось изменить код на этот

OutputStream os=client.getOutputStream();
os.write("Hello".getBytes());
os.flush();
os.write("World".getBytes());
os.flush();
os.write("123".getBytes());
os.flush();

Это, наконец, решило все мои проблемы. Работает для потоковой передачи фрагментами и фиксированной длины

Другие вопросы по теме