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

31
我正试图向 URLConnection#getOutputStream 写入数据,但是在调用 URLConnection#getInputStream 之前并没有实际发送任何数据。即使我将 URLConnnection#doInput 设置为 false,它仍然不会发送数据。有人知道这是为什么吗?API文档中没有描述这一点。
Java API文档关于URLConnection的说明: 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个回答

41
URLConnection和HttpURLConnection的API(不管好坏)都是设计成用户按照非常特定的事件顺序进行操作的:
1. 设置请求属性 2. (可选)getOutputStream(),向流中写入数据,关闭流 3. getInputStream(),从流中读取数据,关闭流
如果你的请求是POST或PUT,你需要进行可选的第2步。
据我所知,OutputStream不像一个套接字,它并不直接连接到服务器上的InputStream。相反,在你关闭或刷新流之后,并调用getInputStream()方法,你的输出会被构建成一个请求并发送出去。这种语义是基于你可能希望读取响应的假设。我看到的每个示例都展示了这种事件顺序。当与正常的流I/O API进行比较时,我完全同意你和其他人对这个API的反直觉性的看法。
你提供的链接tutorial中指出"URLConnection是一个以HTTP为中心的类"。我理解这句话的意思是这些方法是围绕着请求-响应模型设计的,并且假设它们将被用于这种方式。
就我所知,我找到了这个错误报告,它比javadoc文档更好地解释了该类的预期操作。报告的评估指出:“唯一发送请求的方法是调用getInputStream。”

漏洞找得不错!这真的澄清了事情,我很高兴知道它已经被记录在某个地方。谢谢! - John
1
Java 8仍然存在这个错误/未记录的功能,我刚刚发现了。 (在寻找解决此问题的解决方案或解决方法时,我找到了此页面。)现在,在原始问题提出六年后,是否有更好的替代方案? - Dynotherm Connector

4
尽管getInputStream()方法可以使URLConnection对象发起HTTP请求,但这并不是必须的。请考虑实际工作流程:
1.构建一个请求 2.提交 3.处理响应
第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

根据我的实验结果(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() 抛出异常。我的实验表明,与服务器的实际连接/IO相关的任何异常仅在调用 urlCon.getOutputStream() 后抛出。即使 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、防火墙、HTTP响应、服务器无法接受连接、服务器无法及时处理请求),与服务器的工作可能会失败。因此,重要的是要了解引发异常如何说明连接的实际情况。

1

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

正如其他回答者所指出的,您应该能够自己调用flush()来触发写入。


嗨詹姆斯,感谢您提供有关HTTP规范的详细信息。那个答案符合我正在寻找的方向。所以是因为HTTP规范要求请求必须有响应吗?关于使用 flush(),您会发现我已经尝试过,但在另一端并没有看到请求。 - John
还要确保以正确的顺序获取流,同时在此过程中调用flush。如果在初始获取后未刷新,则会导致通信标头未在各方之间传输,最终导致死锁。 - pnt
所以是 getOuputStream()flush()write(),最后再次 flush() - John

1
根本原因是它必须自动计算Content-length头(除非您使用分块或流模式)。在看到所有输出之前,它无法执行此操作,而且必须在输出之前发送它,因此必须缓冲输出。它需要一个决定性事件来知道最后一个输出实际上已经被写入。因此,它使用getInputStream()进行此操作。在那时,它会写入包括内容长度在内的标头,然后是输出,然后开始读取输入。

它不必自动计算Content-Length。您可以调用setFixedLengthStreamingMode来指定长度。然后会禁用内部缓冲。 - vocaro
@vocaro同意,但OP并没有这样做,这就是我所解决的问题,并解释了他所看到的行为。 - user207421
1
关闭输出流也能实现这个功能,这样调用getInputStream就不再必要了吗? - Thomas Andrews

-3

(从您的第一个问题重新发布。无耻的自我宣传) 不要自己摆弄URLConnection,让Resty来处理。

这是您需要编写的代码(我假设您会收到文本返回):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接