2016-07-27 16 views
0

我正在使用Jetty 9,並試圖在所有正文到達服務器之前處理PUT請求的標頭。下面是我做了什麼:Jetty嵌入式 - PUT動詞 - 正文到達之前的處理標頭

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 

看着我只能讓我的過濾代碼執行後,所有的時間戳身體已經抵達。任何人都可以解釋我該怎麼做?一個典型的用例是:客戶端嘗試上傳5GB文件。只要標題到達,我想檢查它們是否正常(例如,通過檢查是否存在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」標頭是完全有效/合法的,也就是所謂的[分塊傳輸編碼](https://en.wikipedia.org/wiki/Chunked_transfer_encoding),它Jetty作爲客戶端和/或服務器支持開箱即用。 –

0

使用多個請求。例如第一個請求包含自定義標頭,後續請求用於上傳5GB文件。

+0

對不起,這是不是一種選擇。我需要爲服務器端的每個請求執行此操作,因爲我對「其他」客戶端實現沒有任何控制。 – xmas79

+0

Http基於TCP。如果客戶端不關閉http連接,則不用擔心服務器,超時將被觸發,並且如果請求的數據大小足夠,服務器將接收部分數據,例如http頭...解析HTTP請求沒問題。現在檢查標題,如果拒絕,可以關閉它,不管客戶端。 – samm

+0

這是錯誤的。服務器(Jetty)總是獲取部分數據。問題是Jetty似乎沒有給我選項,以便在頭部到達時或者主體仍然到達時檢查請求的頭部,不管它是5k還是5MB或5GB。它將在觸發我的過濾器代碼之前攝取所有數據。但現實情況是,如果服務器在大約1k(標題)之後已經知道它可以斷開連接,那麼服務器不應該攝入5GB的數據。 – xmas79

0

你沒有在你的HelloServlet.doPut()中做任何事情,所以你基本上告訴Servlet容器(又名Jetty)你完成了處理該請求。

Jetty中的請求處理由網絡中的一系列緩衝區處理。

您的PUT頭文件和您的正文內容的開頭可能適合單個緩衝區。

碼頭將解析頭出來,然後開始請求的Servlet鏈的調度,打你HelloFilter,然後將過濾器與chain.doFilter(request, response);

的時間點時HelloServlet.doPut()移動它沿鏈已經處理了標題,並且主體內容的開始尚未處理,正在等待doPut()中的實現調用HttpServletRequest.getInputStream()並開始處理它,此時Jetty可以自由地從網絡讀取更多緩衝區。

注意:如果你的servlet退出而不讀取請求輸入流,反應未指明Connection: close,然後碼頭將被迫讀完成整個請求尋找下一個請求後,它(被作爲HTTP/1中的persistent connection。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