为什么必须调用URLConnection#getInputStream才能写出URLConnection#getOutputStream?

时间:2011-01-30 18:05:44

标签: java httpurlconnection

我正在尝试写出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();
            }
        }

    }

}

6 个答案:

答案 0 :(得分:37)

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

  1. 设置请求属性
  2. (可选)getOutputStream(),写入流,关闭流
  3. getInputStream(),从流中读取,关闭流
  4. 如果您的请求是POST或PUT,则需要选择步骤#2。

    据我所知,OutputStream不像套接字,它不直接连接到服务器上的InputStream。相反,在关闭或刷新流之后,AND调用getInputStream(),您的输出将内置到Request中并发送。语义基于您希望读取响应的假设。我见过的每个例子都显示了这种事件的顺序。我肯定会同意你和其他人的说法,与普通的流I / O API相比,这个API是违反直觉的。

    您链接到的tutorial表示“URLConnection是一个以HTTP为中心的类”。我认为这意味着这些方法是围绕Request-Response模型设计的,并假设它们将如何使用。

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

答案 1 :(得分:4)

尽管getInputStream()方法肯定会导致URLConnection对象发起HTTP请求,但并不要求这样做。

考虑实际的工作流程:

  1. 构建请求
  2. 提交
  3. 处理回复
  4. 步骤1包括通过HTTP实体在请求中包括数据的可能性。碰巧的是,URLConnection类提供了一个OutputStream对象作为提供这些数据的机制(并且由于许多原因在这里并不特别相关)。可以说,在提供数据之前,这种机制的流媒体特性为程序员提供了一定的灵活性,包括在完成请求之前关闭输出流(以及任何输入流的输入流)的能力。

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

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

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

    无论是否通过连接的输出流发送实体,无论是否预期返回响应实体,程序始终都想知道结果(由HTTP状态代码提供)。在URLConnection上调用getResponseCode()会提供此状态,并且在不调用getInputStream()的情况下切换结果可能会结束HTTP会话。

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

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

    ......这样做:

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

答案 2 :(得分:2)

正如我的实验所示(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()似乎是“虚拟”方法,它不进行任何物理连接。 但是,如果从服务器response-headers中调用返回Content-Length:header字段的urlCon.getContentLengthLong(),则会自动调用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,防火墙,http响应,服务器无法接受连接,服务器无法及时处理请求),服务器的工作可能会失败。因此,了解如何引发的异常可以解释连接实际发生的情况非常重要。

答案 3 :(得分:1)

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

正如另一个响应者所说,你应该能够自己调用flush()来触发写入。

答案 4 :(得分:1)

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

答案 5 :(得分:-3)

(从你的第一个问题转发。无耻的自我插件) 不要自己动摇URLConnection,让Resty处理它。

以下是您需要编写的代码(我假设您正在收回文本):

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