【问题标题】:HttpURLConnection executing two timesHttpURLConnection 执行两次
【发布时间】:2022-02-02 10:32:03
【问题描述】:

我正在使用HttpURLConnection 向服务器发出请求并将一些数据保存在Database 中,但是当我发出请求时,它会执行两次,这会在数据库中添加两个相同的行。

注意:我也从 iOS 向服务器发出相同的请求,并且它运行良好,这只发生在 Android

这是我提出请求的代码:

URL url = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();

httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);

OutputStream outputStream = (OutputStream)httpURLConnection.getOutputStream();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));

String post_data = URLEncoder.encode("user_name" ,"UTF-8") + "=" + URLEncoder.encode(user,"UTF-8")+"&"
        +URLEncoder.encode("user_id" ,"UTF-8") + "=" + user_id+"&"
        +URLEncoder.encode("manager_id" ,"UTF-8") + "=" + manager+"&"
        +URLEncoder.encode("company_id" ,"UTF-8") + "=" + company+"&"
        +URLEncoder.encode("user_role" ,"UTF-8") + "=" + URLEncoder.encode(user_role ,"UTF-8");

bufferedWriter.write(post_data);
bufferedWriter.close();
outputStream.close();

InputStream inputStream = httpURLConnection.getInputStream();
Reader reader = new InputStreamReader(inputStream);

final char[] buf = new char[256];

final StringBuffer sb = new StringBuffer();

while (true) {
    int length = reader.read(buf);
    if (length == -1) break;
    sb.append(buf, 0, length);
}

reader.close();
inputStream.close();

【问题讨论】:

  • 您正在关闭中间的输出流。你能测试同样的东西,但最后关闭它们(在你关闭输入流的同时)。最后还要调用 httpURLConnection.disconnect() 。 iOS 和 Android 使用不同的套接字机制。大概与这个事实有关。同样在调用阅读器之前,请对输出流进行刷新。另一点要检查的是服务器的响应速度有多快?不确定Android是否会在一段时间后发送自动重试,以防服务器没有响应。
  • @Besart 你用的是哪个版本的安卓?
  • 确保代码不在 Android Lifecyle Hook 中。
  • 我会放一些调试日志或附加调试器,看看这段代码是否从某个地方运行了两次,比如生命周期钩子。
  • @pringi 在这种情况下,关闭输出流是良性的。调用disconnect() 会禁用连接池。它可能没有任何帮助。

标签: java android xml httpconnection


【解决方案1】:

HttpURLConnection 自动重试机制导致请求被重复两次。

问题分析与定位

HttpURLConnection 使用 Sun 私有 HTTP 协议实现类:HttpClient.java 关键是以下发送请求和解析响应头的方法:

569       /** Parse the first line of the HTTP request.  It usually looks
570           something like: "HTTP/1.0 <number> comment\r\n". */
571   
572       public boolean parseHTTP(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
573       throws IOException {
574           /* If "HTTP/*" is found in the beginning, return true.  Let
575            * HttpURLConnection parse the mime header itself.
576            *
577            * If this isn't valid HTTP, then we don't try to parse a header
578            * out of the beginning of the response into the responses,
579            * and instead just queue up the output stream to it's very beginning.
580            * This seems most reasonable, and is what the NN browser does.
581            */
582   
583           try {
584               serverInput = serverSocket.getInputStream();
585               if (capture != null) {
586                   serverInput = new HttpCaptureInputStream(serverInput, capture);
587               }
588               serverInput = new BufferedInputStream(serverInput);
589               return (parseHTTPHeader(responses, pi, httpuc));
590           } catch (SocketTimeoutException stex) {
591               // We don't want to retry the request when the app. sets a timeout
592               // but don't close the server if timeout while waiting for 100-continue
593               if (ignoreContinue) {
594                   closeServer();
595               }
596               throw stex;
597           } catch (IOException e) {
598               closeServer();
599               cachedHttpClient = false;
600               if (!failedOnce && requests != null) {
601                   failedOnce = true;
602                   if (httpuc.getRequestMethod().equals("POST") && (!retryPostProp || streaming)) {
603                       // do not retry the request
604                   }  else {
605                       // try once more
606                       openServer();
607                       if (needsTunneling()) {
608                           httpuc.doTunneling();
609                       }
610                       afterConnect();
611                       writeRequests(requests, poster);
612                       return parseHTTP(responses, pi, httpuc);
613                   }
614               }
615               throw e;
616           }
617   
618       }

在第 600 - 614 行的代码中:

failedOnce 默认为false,表示是否失败过一次。这也限制了最多发送 2 个请求。 httpuc 是请求相关信息。 retryPostProp 的默认值为 true,可以通过命令行参数(-Dsun.net.http.retryPost=false)指定该值。 流式传输:默认为 false。如果我们处于流模式(固定长度或分块),则为 true。

使用Linux命令socat tcp4-listen:8080,fork,reuseaddr system:“sleep 1”!!stdout建立一个只接收请求不返回响应的HTTP服务器。 对于 POST 请求,在发送第一个请求后,解析响应会遇到流的提前结束,即 SocketException: Unexpected end of file from server。 parseHTTP 捕获后,发现满足以上条件后,会重试。服务器将收到第二个请求。

解决方案

  1. 禁用 HttpURLConnection 的重试机制。 通过启动器命令参数 -Dsun.net.http.retryPost=false 或代码设置 System.setProperty("sun.net.http.retryPost", "false")

  2. 使用 Apache HttpComponents 库。 默认情况下,HttpClient 会尝试从 I/O 异常中自动恢复。这种自动恢复机制仅限于少数被认为是安全的例外情况。

HttpClient 不会尝试从任何逻辑或 HTTP 协议错误中恢复; HttpClient 会自动重试被认为是幂等的方法; HttpClient 将自动重试仍然无法向目标服务器发送 HTTP 请求的方法。 (例如,请求还没有完全传送到服务器)。

【讨论】:

  • 问题是关于安卓的。但 Android 使用 OkHttp 而不是任何 Sun 私有 HTTP 协议实现。
  • 但是他使用HttpURLConnection来获取HTTP请求。请详细检查他的问题。谢谢!!! @rmunge
  • HttpURLConnection 是一个抽象类。问题中的代码使用 URL.openConnection() 来获取HttpURLConnection 的实例。对于 OpenJDK 和 Oracle Java 运行时引擎,这将返回一个使用 sun.net.www.http.HttpClientsun.net.www.protocol.http.HttpURLConnection 实例。但在 android 上,返回的对象是 com.android.okhttp.HttpHandler 的一个实例。因此,您的解释适用于在 OpenJDK 或 Oracle JRE 上运行的 Java 应用程序,但不适用于 Android。
【解决方案2】:

Android 使用OkHttp 来实现HttpURLConnection 的功能。 OkHttp 在某些情况下静默重试:

...OkHttp在网络麻烦时坚持:它会默默地 从常见的连接问题中恢复...

该库过去曾遇到过一些意外重试问题,包括 POST 请求(有关摘要,请参阅 here)。即使是最新的 Android 开发分支似乎也使用了 5 个以上!!!多年前的 OkHttp 版本(请参阅here)。因此,Android 版本很可能没有任何相关的错误修复。也没有简单的方法可以访问内部 OkHttp 客户端实例并明确禁用重试。

所以,你有两个选择:

  • A) 直接在您的应用程序中使用更新版本的 OkHttp,而不是依赖 HttpURLConnection
  • B) 尝试解决问题:

当 POST 请求的内容没有被 OkHttp 内部缓冲时,似乎没有执行某些重试。您可以通过setFixedLengthStreamingMode(long) 设置显式内容长度或通过setChunkedStreamingMode(0) 启用分块流式传输来避免内部缓冲。这些解决方法似乎至少可以避免一些重试(请参阅this 票证上的cmets),但是有不保证这会避免各种重试。

从安全角度来看,我强烈建议遵循方法 A) 并且不要使用 Android 的 Http(s)URLConnection。如前所述,Android 开源项目似乎使用了一个完全过时的 OkHttp 版本,甚至没有收到任何安全修复(至少官方 OkHttp 项目没有)。

请注意,大多数智能手机制造商都使用 Android 开源项目的闭源分支。他们可能使用不同版本的第三方库,甚至在底层使用不同的 HTTP 客户端库。

【讨论】:

  • 与 Web 服务器而不是数据库的连接不可靠。 OP 的代码不会尝试连接到数据库。
  • @user207421 问题提到 HTTP 请求最终触发插入数据库。很明显,HTTP 请求不会由数据库直接处理。但是这个问题也没有提到 HTTP 客户端和数据库之间有哪些组件(代理、Web 服务器、应用程序服务器、消息传递......)。 “与数据库的不可靠连接”是指 android 应用程序和数据库之间的所有内容。我修改了措辞以避免误解。
猜你喜欢
  • 2012-09-10
  • 2017-04-27
  • 2011-11-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-02
  • 1970-01-01
相关资源
最近更新 更多