2016-07-27 5 views
0

Я использую Jetty 9, и я пытаюсь обработать заголовки запроса PUT до того, как все тело прибыло на сервер. Вот что я сделал:Jetty Embedded - PUT Verb - заголовки процессов до того, как тело поступило

Server.java:

public class SimplestServer 
{ 
    public static void main(String[] args) throws Exception 
    { 
     Server server = new Server(9080); 

     ServletHandler handler = new ServletHandler(); 
     server.setHandler(handler); 

     handler.addServletWithMapping(HelloServlet.class, "/*"); 
     handler.addFilterWithMapping(HelloPrintingFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); 

     server.start(); 
     server.dumpStdErr(); 
     server.join(); 
    } 

    public static class HelloServlet extends HttpServlet { 
     private static final long serialVersionUID = 1L; 

     @Override 
     protected void doGet(HttpServletRequest request, HttpServletResponse response) 
     throws ServletException, IOException { 
      System.out.println(System.currentTimeMillis() + ": Hello from HelloServlet GET"); 
     } 

     @Override 
     protected void doPut(HttpServletRequest request, HttpServletResponse response) 
     throws ServletException, IOException { 
      System.out.println(System.currentTimeMillis() + ": Hello from HelloServlet PUT"); 
     } 
    } 

    public static class HelloPrintingFilter implements Filter { 
     @Override 
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
     throws IOException, ServletException { 
      System.out.println(System.currentTimeMillis() + ": Hello from filter"); 
      chain.doFilter(request, response); 
     } 

     @Override 
     public void init(FilterConfig arg0) throws ServletException { 
      System.out.println(System.currentTimeMillis() + ": Init from filter"); 
     } 

     @Override 
     public void destroy() { 
      System.out.println(System.currentTimeMillis() + ": Destroy from filter"); 
     } 
    } 
} 

Client.java

public class SimplestClient 
{ 
    public static void main(String[] args) throws Exception 
    { 
     URL url = new URL("http://localhost:9080/resource"); 
     HttpURLConnection httpCon = (HttpURLConnection) url.openConnection(); 
     httpCon.setDoOutput(true); 
     httpCon.setRequestMethod("PUT"); 
     OutputStream out = httpCon.getOutputStream(); 
     byte[] b = new byte[65536]; 
     Random r = new Random(); 
     r.nextBytes(b); 
     for (int i = 0; i < 1024; i++) { 
      out.write(b); 
     } 
     System.out.println(System.currentTimeMillis() + ": Data sent. Waiting 5 seconds..."); 

     try { 
      Thread.sleep(5000); 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 
     out.close(); 
     System.out.println(System.currentTimeMillis() + ": Done!"); 
     httpCon.getInputStream(); 
    } 
} 

В двух словах, программа-сервер прослушивает соединения на порт 9080, когда запрос прибывает фильтр HelloPrintingFilter выполняется, тогда запрос обрабатывается HelloServlet. Вместо этого клиент подключается к серверу, отправляет кучу данных, затем спит в течение 5 секунд и, наконец, закрывает соединение с сервером.

Перспективе обеих программ дает следующий результат:

Клиент:

1469613522350: Data sent. Waiting 5 seconds... 
1469613527351: Done! 

Сервер:

1469613527373: Hello from filter 
1469613527373: Hello from HelloServlet PUT 

Глядя на временные метки я только могу получить мой код фильтра выполняется после тело пришло. Может ли кто-нибудь объяснить мне, как это сделать? Типичный пример использования: клиент пытается загрузить файл размером 5 ГБ. Как только загонятся заголовки, я хочу проверить, все ли в порядке (например, проверка наличия заголовка Content-MD5 или любого другого настраиваемого заголовка, который я должен проверить). Если запрос в порядке, начните обработку тела. Если запрос не подходит, закройте соединение.

Спасибо.

ответ

0

Попался ! Проблема была не на стороне сервера, а на стороне клиента, которая была предназначена только как заглушка. В частности, проблема заключалась в буферизации в HttpUrlConnection.

Напоминая Мои client.java, у меня есть:

for (int i = 0; i < 1024; i++) { 
     out.write(b); 
    } 

и если изменить цикл, чтобы что-то вроде

for (int i = 0; i < 1024*1024; i++) { 
     out.write(b); 
    } 

я сразу же получить OutOfMemoryError исключение, не получив ничего на стороне сервера, что указывает на ни один байт не был передан. И, конечно, это правильно, потому что, прежде чем положить заголовки на провод, HttpUrlConnection должен знать длину тела, так как он должен излучать заголовок Content-Length. Изменение реализации клиента на сырые сокеты, эффективно контролирующее, когда байты идут на провод, разрешило проблему.

В качестве дополнительной заметки код сервера можно дополнительно упростить, удалив класс фильтра. Полный серверный код:

server.java:

public class SimplestServer 
{ 
    public static void main(String[] args) throws Exception 
    { 
     Server server = new Server(9080); 

     ServletHandler handler = new ServletHandler(); 
     server.setHandler(handler); 

     handler.addServletWithMapping(HelloServlet.class, "/*"); 

     server.start(); 
     server.dumpStdErr(); 
     server.join(); 
    } 

    public static class HelloServlet extends HttpServlet { 
     private static final long serialVersionUID = 1L; 

     @Override 
     protected void doGet(HttpServletRequest request, HttpServletResponse response) 
     throws ServletException, IOException { 
      System.out.println(System.currentTimeMillis() + ": Hello from HelloServlet GET"); 
     } 

     @Override 
     protected void doPut(HttpServletRequest request, HttpServletResponse response) 
      throws ServletException, IOException { 
      System.out.println(System.currentTimeMillis() + ": Hello from HelloServlet PUT"); 

      // Perform some checks here 
      if (request.getHeader("X-Key") == null) 
      { 
       response.setHeader("Connection", "close"); 
       response.sendError(HttpServletResponse.SC_FORBIDDEN); 
       System.out.println(System.currentTimeMillis() + ": Filter --> X-Key failed!"); 
       return; 
      } 

      // Everything OK! Read the stream. 
      System.out.println(System.currentTimeMillis() + ": Proceded!!"); 
      InputStream body = request.getInputStream(); 
      long bytesReadSoFar = 0; 
      byte[] data = new byte[65536]; 
      while (true) { 
       int bytesRead = body.read(data); 
       if (bytesRead < 0) 
        break; 
       bytesReadSoFar += bytesRead; 
      } 
      System.out.println(System.currentTimeMillis() + ": Finished! Read " + bytesReadSoFar + " bytes."); 
      response.setHeader("Connection", "close"); 
      response.setStatus(HttpServletResponse.SC_OK); 
     } 
    } 
} 
+0

Неправильные предположения. Совершенно корректно/законно отправлять содержимое запроса и/или ответа без заголовка Content-Length, который известен как [chunked transfer encoding] (https://en.wikipedia.org/wiki/Chunked_transfer_encoding), который Jetty поддерживает как клиент, так и сервер. –

0

Используйте несколько запросов. например первый запрос включает пользовательский заголовок, а последующие запросы используются для загрузки файла размером 5 ГБ.

+0

К сожалению, это не вариант. Мне нужно сделать это для каждого запроса на стороне сервера, потому что у меня нет никакого контроля над реализацией клиентов «других». – xmas79

+0

Http основан на TCP. Не беспокойтесь о сервере, если клиент не закрывает http-соединение, тайм-аут будет запущен, и если размер данных запроса будет достаточным, сервер получит частичные данные, такие как заголовки http ... разбор HTTP-запроса это нормально. Теперь проверьте заголовки, если их отклонить, вы можете закрыть его независимо от клиента. – samm

+0

Это неправильно. Сервер (Jetty) всегда получает частичные данные. Проблема в том, что Jetty, похоже, не дает мне возможность проверять заголовки запроса, когда появились заголовки, или пока тело все еще прибывает, будь то 5k или 5MB или 5GB. Он будет глотать ВСЕ данные перед запуском моего кода фильтра. Но реальность заключается в том, что серверу никогда не следует глотать 5 ГБ данных, если после примерно 1 тыс. (Заголовок) он уже знает, что может отказаться от соединения. – xmas79

0

Вы ничего не делаете в своем HelloServlet.doPut(), так что в основном вы сообщаете контейнеру сервлетов (aka Jetty), что вы закончили обработку этого запроса.

Обработка запросов в Jetty обрабатывается рядом буферов из сети.

Ваши заголовки PUT и начало содержимого вашего тела, вероятно, подходят в одном буфере.

Причал будет анализировать заголовки, и затем начать отправку запроса в Servlet цепи, попав ваш HelloFilter, а затем ваш фильтр перемещает его вдоль цепи с chain.doFilter(request, response);

Момент времени, когда HelloServlet.doPut() , заголовки обработаны, а начало содержимого тела не дожидалось вашей реализации в doPut(), чтобы позвонить HttpServletRequest.getInputStream() и начать ее обработку, после чего Jetty может начать читать больше буферов с сети.

Примечание: если ваш сервлет выходы без чтения входного потока запроса и ответ не указали Connection: close, то Причал будет вынужден читать весь запрос завершение ищет следующий запрос после того, как (известно, как persistent connection в HTTP/1.1 спецификация)

Ближе всего вы достигнете своей заявленной цели, чтобы отклонить содержание тела запроса - использовать то, что у вас есть в спецификации HTTP/1.1 (при условии, что это запрос HTTP/1.1). А именно, правильный код статуса ответа и инициированный сервером Connection: close ответный заголовок.

Вот полный пример:

package jetty; 

import java.io.File; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.InputStreamReader; 
import java.io.OutputStream; 
import java.io.StringWriter; 
import java.net.InetSocketAddress; 
import java.net.Socket; 
import java.nio.charset.StandardCharsets; 
import java.util.concurrent.ThreadLocalRandom; 
import java.util.concurrent.TimeUnit; 

import javax.servlet.ServletException; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 

import org.eclipse.jetty.server.Server; 
import org.eclipse.jetty.server.ServerConnector; 
import org.eclipse.jetty.server.handler.DefaultHandler; 
import org.eclipse.jetty.server.handler.HandlerCollection; 
import org.eclipse.jetty.servlet.ServletContextHandler; 
import org.eclipse.jetty.util.IO; 
import org.eclipse.jetty.util.Uptime; 
import org.junit.AfterClass; 
import org.junit.BeforeClass; 
import org.junit.Test; 

public class PutRejectExample 
{ 
    public static class RejectServlet extends HttpServlet 
    { 
     @Override 
     protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
     { 
      timedLog("doPut() - enter"); 
      if (req.getHeader("X-Key") == null) 
      { 
       resp.setHeader("Connection", "close"); 
       resp.sendError(HttpServletResponse.SC_FORBIDDEN); 
       timedLog("doPut() - rejected"); 
       return; 
      } 

      File output = File.createTempFile("reject-", ".dat"); 
      try (FileOutputStream out = new FileOutputStream(output)) 
      { 
       IO.copy(req.getInputStream(), out); 
      } 
      resp.setStatus(HttpServletResponse.SC_OK); 
      resp.setHeader("Connection", "close"); // be a good HTTP/1.1 citizen 
      timedLog("doPut() - exit"); 
     } 
    } 

    private static Server server; 
    private static int port; 

    private static void timedLog(String format, Object... args) 
    { 
     System.out.printf(Uptime.getUptime() + "ms " + format + "%n", args); 
    } 

    @BeforeClass 
    public static void startServer() throws Exception 
    { 
     server = new Server(); 
     ServerConnector connector = new ServerConnector(server); 
     connector.setPort(0); 
     server.addConnector(connector); 

     // collection for handlers 
     HandlerCollection handlers = new HandlerCollection(); 
     server.setHandler(handlers); 

     // servlet context 
     ServletContextHandler context = new ServletContextHandler(); 
     context.addServlet(RejectServlet.class, "/reject"); 
     handlers.addHandler(context); 

     // default handler 
     handlers.addHandler(new DefaultHandler()); 

     // start server 
     server.start(); 

     // grab port 
     port = connector.getLocalPort(); 
    } 

    @AfterClass 
    public static void stopServer() throws Exception 
    { 
     server.stop(); 
    } 

    private void performPUT(int requestSize, String... extraRequestHeaders) throws IOException 
    { 
     StringBuilder req = new StringBuilder(); 
     req.append("PUT /reject HTTP/1.1\r\n"); 
     req.append("Host: localhost:").append(port).append("\r\n"); 
     req.append("Content-Length: ").append(requestSize).append("\r\n"); 
     for (String extraHeader : extraRequestHeaders) 
     { 
      req.append(extraHeader); 
     } 
     req.append("\r\n"); 

     timedLog("client open connection"); 
     try (Socket socket = new Socket()) 
     { 
      socket.connect(new InetSocketAddress("localhost", port)); 

      try (OutputStream out = socket.getOutputStream(); 
       InputStream in = socket.getInputStream(); 
       InputStreamReader reader = new InputStreamReader(in)) 
      { 
       timedLog("client send request (headers + body)"); 
       try 
       { 
        // write request line + headers 
        byte headerBytes[] = req.toString().getBytes(StandardCharsets.UTF_8); 
        out.write(headerBytes); 
        out.flush(); 

        // write put body content 
        int bufSize = 65535; 
        byte[] buf = new byte[bufSize]; 
        int sizeLeft = requestSize; 
        while (sizeLeft > 0) 
        { 
         int writeSize = Math.min(sizeLeft, bufSize); 
         ThreadLocalRandom.current().nextBytes(buf); 
         out.write(buf, 0, writeSize); 
         out.flush(); 
         sizeLeft -= writeSize; 
         try 
         { 
          // simulate a slower connection 
          TimeUnit.MILLISECONDS.sleep(10); 
         } 
         catch (InterruptedException ignore) 
         { 
          // ignore 
         } 
        } 
       } 
       catch (IOException e) 
       { 
        timedLog("client request send exception"); 
        e.printStackTrace(System.out); 
       } 
       timedLog("client send request complete"); 

       timedLog("client read response"); 
       try 
       { 
        StringWriter respStream = new StringWriter(); 
        IO.copy(reader, respStream); 

        timedLog("client response: %s", respStream.toString()); 
       } 
       catch (IOException e) 
       { 
        timedLog("client read response exception"); 
        e.printStackTrace(System.out); 
       } 
      } 
     } 
     timedLog("client connection complete"); 
    } 

    @Test 
    public void testBadPost() throws IOException 
    { 
     timedLog("---- testBadPost()"); 
     performPUT(1024 * 1024 * 10); 
    } 

    @Test 
    public void testGoodPost() throws IOException 
    { 
     timedLog("---- testGoodPost()"); 
     performPUT(1024 * 1024 * 10, "X-Key: foo\r\n"); 
    } 
} 

Это использует сырые Socket и сырые потоки, чтобы избежать путаясь всеми буферным присутствующего в HttpUrlConnection.

Выход вы увидите для нормальной/счастливый случай, как это ...

416ms ---- testGoodPost() 
416ms client open connection 
2016-07-27 06:40:22.180:INFO:oejs.AbstractConnector:main: Started [email protected]{HTTP/1.1,[http/1.1]}{0.0.0.0:46748} 
2016-07-27 06:40:22.181:INFO:oejs.Server:main: Started @414ms 
421ms client send request (headers + body) 
494ms doPut() - enter 
2084ms doPut() - exit 
2093ms client send request complete 
2093ms client read response 
2094ms client response: HTTP/1.1 200 OK 
Date: Wed, 27 Jul 2016 13:40:22 GMT 
Connection: close 
Server: Jetty(9.3.11.v20160721) 
2094ms client connection complete 

Выход для отвергнутого случае будет выглядеть следующим образом ...

2095ms ---- testBadPost() 
2095ms client open connection 
2096ms client send request (headers + body) 
2096ms doPut() - enter 
2101ms doPut() - rejected 
2107ms client request send exception 
java.net.SocketException: Broken pipe 
    at java.net.SocketOutputStream.socketWrite0(Native Method) 
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109) 
    at java.net.SocketOutputStream.write(SocketOutputStream.java:153) 
    at jetty.PutRejectExample.performPUT(PutRejectExample.java:137) 
    at jetty.PutRejectExample.testBadPost(PutRejectExample.java:180) 
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
    at java.lang.reflect.Method.invoke(Method.java:498) 
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) 
2109ms client send request complete 
2109ms client read response 
2109ms client response: HTTP/1.1 403 Forbidden 
Date: Wed, 27 Jul 2016 13:40:23 GMT 
Cache-Control: must-revalidate,no-cache,no-store 
Content-Type: text/html;charset=iso-8859-1 
Content-Length: 322 
Connection: close 
Server: Jetty(9.3.11.v20160721) 

<html> 
<head> 
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> 
<title>Error 403 </title> 
</head> 
<body> 
<h2>HTTP ERROR: 403</h2> 
<p>Problem accessing /reject. Reason: 
<pre> Forbidden</pre></p> 
<hr /><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.11-SNAPSHOT</a><hr/> 
</body> 
</html> 

2109ms client connection complete