【问题标题】:Why do you have to call URLConnection#getInputStream to be able to write out to URLConnection#getOutputStream?为什么必须调用 URLConnection#getInputStream 才能写入 URLConnection#getOutputStream?
【发布时间】:2011-06-18 04:32:16
【问题描述】:

我正在尝试写信给URLConnection#getOutputStream,但是,在我致电URLConnection#getInputStream 之前,实际上并没有发送任何数据。即使我将URLConnnection#doInput 设置为false,它仍然不会发送。有人知道为什么吗? API 文档中没有任何内容对此进行描述。

关于 URLConnection 的 Java API 文档:http://download.oracle.com/javase/6/docs/api/java/net/URLConnection.html

Java 的 URLConnection 读写教程:http://download.oracle.com/javase/tutorial/networking/urls/readingWriting.html

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;

public class UrlConnectionTest {

    private static final String TEST_URL = "http://localhost:3000/test/hitme";

    public static void main(String[] args) throws IOException  {

        URLConnection urlCon = null;
        URL url = null;
        OutputStreamWriter osw = null;

        try {
            url = new URL(TEST_URL);
            urlCon = url.openConnection();
            urlCon.setDoOutput(true);
            urlCon.setRequestProperty("Content-Type", "text/plain");            

            ////////////////////////////////////////
            // SETTING THIS TO FALSE DOES NOTHING //
            ////////////////////////////////////////
            // urlCon.setDoInput(false);

            osw = new OutputStreamWriter(urlCon.getOutputStream());
            osw.write("HELLO WORLD");
            osw.flush();

            /////////////////////////////////////////////////
            // MUST CALL THIS OTHERWISE WILL NOT WRITE OUT //
            /////////////////////////////////////////////////
            urlCon.getInputStream();

            /////////////////////////////////////////////////////////////////////////////////////////////////////////
            // If getInputStream is called while doInput=false, the following exception is thrown:                 //
            // java.net.ProtocolException: Cannot read from URLConnection if doInput=false (call setDoInput(true)) //
            /////////////////////////////////////////////////////////////////////////////////////////////////////////

        } catch (Exception e) {
            e.printStackTrace();                
        } finally {
            if (osw != null) {
                osw.close();
            }
        }

    }

}

【问题讨论】:

    标签: java httpurlconnection


    【解决方案1】:

    调用 getInputStream() 表示客户端已完成发送请求,并准备好接收响应(根据 HTTP 规范)。似乎 URLConnection 类内置了这个概念,并且在请求输入流时必须刷新()输出流。

    正如其他响应者所说,您应该能够自己调用 flush() 来触发写入。

    【讨论】:

    • 嗨,James,感谢您提供有关 HTTP 规范的详细信息。这个答案与我正在寻找的内容一致。那么是不是因为 HTTP 规范要求必须有响应才能成为请求?关于使用flush(),你会看到我试过了,但我没有看到另一端发出请求。
    • 还要确保以正确的顺序获取流,在此过程中调用flush。在初始获取后未能刷新将导致通信标头无法在各方之间传输,并最终导致死锁。
    • 所以getOuputStream()flush()write(),最后又是flush()
    【解决方案2】:

    (从你的第一个问题转贴。无耻的自插) 不要自己摆弄 URLConnection,让Resty 处理吧。

    这是您需要编写的代码(我假设您正在获取文本):

    import static us.monoid.web.Resty.*;
    import us.monoid.web.Resty;  
    ...    
    new Resty().text(TEST_URL, content("HELLO WORLD")).toString();
    

    【讨论】:

      【解决方案3】:

      用于 URLConnection 和 HttpURLConnection 的 API(无论好坏)是为用户设计的,以遵循非常特定的事件序列:

      1. 设置请求属性
      2. (可选)getOutputStream(),写入流,关闭流
      3. getInputStream(),从流中读取,关闭流

      如果您的请求是 POST 或 PUT,则需要可选步骤 #2。

      据我所知,OutputStream 不像套接字,它不直接连接到服务器上的 InputStream。相反,在您关闭或刷新流并调用 getInputStream() 后,您的输出将内置到请求中并发送。语义基于您将要阅读响应的假设。我看到的每个示例都显示了这种事件顺序。我当然同意你和其他人的观点,即这个 API 与普通的流 I/O API 相比是违反直觉的。

      您链接到的tutorial 声明“URLConnection 是一个以HTTP 为中心的类”。我将其解释为这些方法是围绕请求-响应模型设计的,并假设它们将如何使用。

      对于它的价值,我发现这个bug report 比 javadoc 文档更好地解释了该类的预期操作。报告的评估表明“发出请求的唯一方法是调用 getInputStream。”

      【讨论】:

      • 很好地发现了这个错误!这真的把事情弄清楚了,我很高兴知道它已经被记录在某个地方。谢谢!
      • Java 8 仍然有这个错误/未记录的功能,正如我刚刚发现的那样。 (我在寻找此问题的解决方案或解决方法时发现了此页面。)在提出原始问题六年后,现在有更好的选择吗?
      【解决方案4】:

      根本原因是它必须自动计算 Content-length 标头(除非您使用分块或流模式)。在看到所有输出之前它不能这样做,并且它必须在输出之前发送它,所以它必须缓冲输出。并且它需要一个决定性的事件来知道最后一个输出何时实际被写入。所以它使用 getInputStream() 。那时它会写入包含内容长度的标头,然后是输出,然后它开始读取输入。

      【讨论】:

      • 它不必自动计算 Content-Length。您可以调用 setFixedLengthStreamingMode 给它长度。然后将禁用内部缓冲。
      • @vocaro 同意,但 OP 并没有这样做,这就是我提到的情况,这解释了他所看到的行为。
      • 不会关闭输出流也这样做,这样就不需要调用 getInputStream 了吗?
      【解决方案5】:

      正如我的实验所显示的(java 1.7.0_01)代码:

      osw = new OutputStreamWriter(urlCon.getOutputStream());
      osw.write("HELLO WORLD");
      osw.flush();
      

      不向服务器发送任何内容。它只是将那里写入的内容保存到内存缓冲区。因此,如果您要通过 POST 上传大文件 - 您需要确保有足够的内存。在桌面/服务器上可能不是什么大问题,但在 android 上可能会导致内存不足错误。这是尝试写入输出流时堆栈跟踪的示例,并且内存耗尽。

      Exception in thread "Thread-488" java.lang.OutOfMemoryError: GC overhead limit exceeded
          at java.util.Arrays.copyOf(Arrays.java:2271)
          at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:113)
          at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93)
          at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:140)
          at sun.net.www.http.PosterOutputStream.write(PosterOutputStream.java:78)
          at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
          at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:282)
          at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
          at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:135)
          at java.io.OutputStreamWriter.write(OutputStreamWriter.java:220)
          at java.io.Writer.write(Writer.java:157)
          at maxela.tables.weboperations.POSTRequest.makePOST(POSTRequest.java:138)
      

      在跟踪的底部,您可以看到 makePOST() 方法,它执行以下操作:

           writer = new OutputStreamWriter(conn.getOutputStream());                      
          for (int j = 0 ; j < 3000 * 100 ; j++)
          {
            writer.write("&var" + j + "=garbagegarbagegarbage_"+ j);
          }
         writer.flush();
      

      writer.write() 抛出异常。 此外,我的实验表明,只有在调用 urlCon.getOutputStream() 之后才会引发与服务器的实际连接/IO 相关的任何异常。即使urlCon.connect() 似乎是“虚拟”方法,它不做任何物理连接。 但是,如果您调用 urlCon.getContentLengthLong() 从服务器响应标头返回 Content-Length: 标头字段 - 那么 URLConnection.getOutputStream() 将被自动调用,以防出现异常 - 它将被抛出。

      urlCon.getOutputStream()抛出的异常都是IOException,我遇到过以下几种:

                      try
                      {
                          urlCon.getOutputStream();
                      }
                      catch (UnknownServiceException ex)
                      {
                          System.out.println("UnkownServiceException():" + ex.getMessage());
                      }
      
                      catch (ConnectException ex)
                      {
                          System.out.println("ConnectException()");
                          Logger.getLogger(POSTRequest.class.getName()).log(Level.SEVERE, null, ex);
                      }
      
                      catch (IOException ex) {
                          System.out.println("IOException():" + ex.getMessage());
                          Logger.getLogger(POSTRequest.class.getName()).log(Level.SEVERE, null, ex);
                      }
      

      希望我的小研究对人们有所帮助,因为 URLConnection 类在某些情况下有点违反直觉,因此,在实现它时 - 人们需要知道它处理的是什么。

      第二个原因是:使用服务器时 - 由于多种原因(连接、dns、防火墙、httpresponses、服务器无法接受连接、服务器无法及时处理请求),使用服务器可能会失败。因此,了解引发的异常如何解释连接实际发生的情况很重要

      【讨论】:

        【解决方案6】:

        虽然 getInputStream() 方法确实可以使 URLConnection 对象发起 HTTP 请求,但这不是必须的。

        考虑实际的工作流程:

        1. 构建请求
        2. 提交
        3. 处理响应

        步骤 1 包括通过 HTTP 实体在请求中包含数据的可能性。碰巧的是,URLConnection 类提供了一个 OutputStream 对象作为提供此数据的机制(出于许多与此处无关的原因,这是理所当然的)。可以说,这种机制的流式特性为程序员在提供数据时提供了一定程度的灵活性,包括在完成请求之前关闭输出流(以及任何提供它的输入流)的能力。

        换句话说,第 1 步允许为请求提供数据实体,然后继续构建它(例如通过添加标头)。

        第 2 步实际上是一个虚拟步骤,并且可以自动化(就像在 URLConnection 类中一样),因为如果没有响应,提交请求是没有意义的(至少在 HTTP 协议的范围内)。

        这将我们带到第 3 步。在处理 HTTP 响应时,通过调用 getInputSteam() 检索到的响应实体只是我们可能感兴趣的事情之一。响应由状态、标头、和可选的实体。第一次请求其中任何一个时,URLConnection 将执行虚拟步骤 2 并提交请求。

        无论实体是否通过连接的输出流发送,也无论是否期望返回响应实体,程序总是想知道结果(由 HTTP 状态代码提供)。在 URLConnection 上调用 getResponseCode() 会提供此状态,并且打开结果可能会结束 HTTP 对话而无需调用 getInputStream()。

        因此,如果正在提交数据,并且不需要响应实体,请不要这样做:

        // request is now built, so...
        InputStream ignored = urlConnection.getInputStream();
        

        ... 这样做:

        // request is now built, so...
        int result = urlConnection.getResponseCode();
        // act based on this result
        

        【讨论】:

          猜你喜欢
          • 2012-04-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多