☰
Android 上如何用 Apache HttpClient 稳定访问微 Web 站点
2026/10/15 12:46:30 网站建设 项目流程

简介:面向初学 Android 网络编程的开发者,这份压缩包演示了在安卓平台中借助 Apache HttpClient 访问 Web 站点的完整流程。示例分别基于 DefaultHttpClient 与 AndroidHttpClient 两个实现类展开,适合理解传统 HTTP 请求的写法、连接配置、响应处理以及不同客户端实现的差异。资源共 51 个文件,包含 5 个 Java 源码、11 个 XML 布局及配置、9 张 PNG 图片资源,同时还有 jar、dex、apk 等编译产物和工程配置文件;源码、资源、依赖库与编译输出在包内分层存放,目录结构清晰,方便对照查看。1.18MB 的体积便于直接下载后导入 Eclipse 查看代码与运行效果,目前已有 410 人学习下载。由于安卓高版本已不推荐 HttpClient,读者可结合示例中的请求封装思路迁移到 HttpURLConnection 或 OkHttp;对于做课程设计或毕业设计的学生,也能直接借鉴请求携带参数、解析响应、释放连接等基础写法,对梳理网络层代码很有参考价值。

1. Android 上为什么还要用 Apache HttpClient 访问微 Web 站点

把手机接进工厂内网,对着设备管理页点一下刷新,App 卡了三秒弹超时错误;同一个地址,电脑浏览器却秒开。这是我在维护一个老设备项目时反复遇到的情况:Android 上用 Apache 的 HttpClient 访问微 Web 站点,麻烦根本不在请求代码本身,而在目标站点太“小”。微 Web 站点通常跑在嵌入式网关、路由器或小型设备上,单线程处理、响应慢、对 HTTP 协议实现不完整,用常规的“连上就发请求、发完就等数据”的思路,很容易被读超时、连接被服务端静默断开这类问题缠住。这篇文章就围绕这套技术组合,讲清楚在 Android 6.0 之后系统已移除 HttpClient 的现状下,怎么把依赖恢复回来、怎么写最小可用的请求代码、要调哪些参数才能容忍慢响应,以及真正让人翻车的那几个坑。适合正在接手老代码,或者要做手机与边缘设备直连通信的 Android 开发者。

2. 微 Web 站点的连接特征与 HttpClient 选型判断

2.1 微 Web 站点到底是什么:三个容易被低估的特征

我口中的“微 Web 站点”,不是互联网上的小网站,而是跑在资源受限设备上的轻量 HTTP 服务。常见形态有:智能网关的状态页、路由器的管理接口、工控设备的参数配置页,甚至树莓派上用单文件脚本拉起的服务。这类服务在浏览器里操作时感觉还行,但 Android 客户端一接入,问题就暴露了。

第一个特征是响应慢。设备 CPU 性能弱,又要同时处理采集、控制逻辑和网络请求,一个 HTTP 请求从到达设备到真正返回数据,常常要几百毫秒甚至几秒。浏览器能等,移动端的网络库默认读超时却往往只有 3 到 5 秒,稍有波动就超时。

第二个特征是并发能力低。很多微 Web 服务是单线程实现的:处理完一个请求才读下一个请求。如果客户端开了连接池,同一时刻丢过去多个请求,后面的请求会在设备端排队,排队时间也会被计入你的读超时。

第三个特征是连接管理不完整。不少设备基于轻量协议栈实现,不认真处理 Keep-Alive,响应完就断 socket,或者不返回 Content-Length、不返回 Connection 头。这些行为在浏览器里无所谓,但对负责连接复用的客户端库来说,每一个都是坑。

这三个特征决定了,访问微 Web 站点的关键不是“怎么写 POST”,而是怎么管理超时、怎么避免连接复用踩到设备端的毛病。这也是我为什么坚持用 Apache HttpClient 来讲,而不是直接建议换主流网络库——老代码和存量设备交互时,你往往没得选。

2.2 Android 系统对 Apache HttpClient 的支持变迁:从内置到需要显式引入

Apache HttpClient 在 Android 早期是系统内置的,开发者直接 import org.apache.http 就能用。后来官方逐步收紧:4.x 时期开始推荐 HttpURLConnection,6.0(API 23)直接把 Apache HttpClient 从系统里移除了。这个决定本身有理由,但对老项目是灾难——代码里铺天盖地的 DefaultHttpClient、HttpGet、BasicHttpParams,一夜之间全变成运行时崩溃。

如果只是编译不过,咬牙改就好;真正的痛点是运行时报错。App 在 Android 6.0 以下一切正常,升到高版本系统后,首次进入用到网络请求的页面,直接抛 NoClassDefFoundError: org.apache.http.util.EntityUtils。崩溃信息里能看到 Apache 包名,但系统里已经没有这套类了。

补回来的办法有两个,我都用过。第一个是在 build.gradle 里开启 legacy 库,这个方案依赖 Android 编译环境自带的 org.apache.http.legacy,改动最省事。第二个是直接把 Apache HttpClient 的 jar 包打进 APK,麻烦一些,但版本可控,不依赖系统是否预置。两者不能混用,否则会出现同一个类被加载两份的诡异问题。

搞清楚这条历史脉络,你才能理解为什么老项目“明明以前跑得好好的”,换台新手机就崩。这不是业务代码的问题,是平台裁剪的问题。

2.3 选型判断:什么时候值得用 HttpClient

既然系统都移除了,为什么还要用?我得诚实地说:新项目我不会选它,优先考虑 OkHttp;但以下场景我仍会保留 HttpClient,甚至主动为它封装一层。

一种是存量代码改造。项目里已经积累了规模不小的 org.apache.http 调用,包括自定义的拦截逻辑、Entity 子类、参数封装。整体替换成 OkHttp 不是不行,而是要重写测试、重新验证边界行为,对一个以稳定优先的维护型项目来说,成本不低。另一种是设备端协议行为特殊,需要精细控制连接细节。HttpClient 的连接管理 API 虽然老,但该有的都有,而且行为稳定,调明白之后很顺手。第三种是团队内多个老项目共享同一套网络基础代码,统一技术栈比追求新库更实际。

我做选型时会画这样一张对比表:

对比维度Apache HttpClient(org.apache.http)OkHttp
系统支持高版本需 extra 引入或 jar 打包纯 jar 依赖,无系统限制
HTTP/2不支持原生支持
连接池手动配置,较繁琐内建,默认调优较好
协议容错较保守,遇到不规范响应易出错容错强,见过更多“烂网络”
包体积较小较大一些
维护成本老代码稳定,新需求缺资料社区活跃,解法多

这套对比对新项目基本一边倒:OkHttp 更省心。老项目则要看风险承受能力。本文后面所有实践,都基于这个前提:你已经决定继续用 HttpClient,或者还没法立刻换掉它,那么怎么把它在 Android 上用好。

3. 用 HttpClient 跑通 GET/POST:最小代码与参数说明

3.1 依赖准备:编译期与运行期怎么把 HttpClient 加回来

先把环境搭好。以 Gradle 工程为例,最简单的方式是在 app 的 build.gradle 里加一行:

android { compileSdk 30 useLibrary 'org.apache.http.legacy' }

useLibrary 'org.apache.http.legacy'的作用是让编译期把系统预置的 legacy 包加入 classpath,同时生成对应的运行时依赖信息。加了这行之后,原来那些 import org.apache.http.* 的代码就能重新编译通过,运行在高版本系统上也不会再 NoClassDefFoundError。

这只适用于编译环境里还带 legacy 库的版本。如果你的编译版本较新,或者想彻底摆脱对系统库的依赖,就把 Apache HttpClient 的 jar 直接放进 libs:

dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) }

对应的两个 jar 是 httpclient 和 httpcore,版本要匹配。我会把 httpclient、httpcore 和 httpmime(如果用到 multipart)三件套一起放进去。用 jar 方案时有两点经验:一是不要和 useLibrary 同时用,二是把 libs 里的版本控制在团队统一的版本上,避免不同成员本地 jar 不一致。这两种方案跑通后的 API 是完全一样的,下面的代码两种环境都能用。

3.2 GET 请求:最小代码骨架

一个最普通的 GET 请求,用 HttpClient 写起来是这样的:

// 1. 准备连接参数 HttpParams params = new BasicHttpParams(); // 连接超时:5 秒内连不上就直接失败 HttpConnectionParams.setConnectionTimeout(params, 5000); // 读取超时:给微站点留足 10 秒,避免响应稍慢就判负 HttpConnectionParams.setSoTimeout(params, 10000); // 关闭 Expect: 100-continue,减少一次握手开销 HttpProtocolParams.setUseExpectContinue(params, false); // 2. 用最简方式创建客户端 HttpClient client = new DefaultHttpClient(params); // 3. 发起 GET 请求 HttpGet get = new HttpGet("http://192.168.1.100/status.xml"); try { HttpResponse response = client.execute(get); int code = response.getStatusLine().getStatusCode(); // 设备端数据量小,直接读成字符串即可 String body = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (IOException e) { // 超时、连接中断、DNS 失败都走这里 } finally { get.abort(); // 释放连接,避免连接被占用 }

new DefaultHttpClient(params)是最简单的创建方式,内部会按参数构造连接管理器和 socket 工厂,适合单线程、低并发的场景。HttpConnectionParams.setConnectionTimeout设置 TCP 连接建立的超时,单位毫秒;HttpConnectionParams.setSoTimeout设置 socket 读取超时,也就是“连上了但一直等不到数据”时的上限。setUseExpectContinue(params, false)对微站点很重要,因为很多轻量设备不清楚 Expect 机制,遇到带 Expect 头的请求直接不响应,客户端就会白等。

EntityUtils.toString方便,但只适合响应体不大的情况。微站点返回的通常是几 KB 的 XML、JSON 或纯文本,所以直接用问题不大。finally里的abort()是一个容易忽略的细节:不 abort 的话,如果响应体没被完整消费,连接不会被正确释放,久了会耗尽连接。

3.3 POST 请求:表单提交与编码

POST 在老代码里多半用于提交配置或触发操作,比如重启设备、修改参数。常见的表单提交这样写:

// 复用上一节的 HttpClient 实例 HttpPost post = new HttpPost("http://192.168.1.100/config"); // 表单参数 List<NameValuePair> form = new ArrayList<NameValuePair>(); form.add(new BasicNameValuePair("reboot", "1")); form.add(new BasicNameValuePair("confirm", "true")); // 以 UTF-8 编码提交表单,设备端按同样编码解析 post.setEntity(new UrlEncodedFormEntity(form, "UTF-8")); try { HttpResponse response = client.execute(post); // 很多设备返回 200 不代表操作成功,要检查响应体里的状态字段 String body = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (IOException e) { // 设备处理慢时,这里抛的是 SocketTimeoutException }

UrlEncodedFormEntity会把参数拼接成 key=value&key2=value2 的形式,并设置 Content-Type 为 application/x-www-form-urlencoded。设备端如果是自己解析 HTTP body,往往只认这个格式。setEntity还可以塞入 StringEntity 或 ByteArrayEntity,用于提交 JSON 或二进制数据,但微站点对 JSON 的解析支持参差不齐,我一般优先用表单格式。

一个小经验:POST 之后设备端可能在做耗时操作,比如写 flash、重启服务,响应时间比 GET 更不稳定。所以 POST 的 SoTimeout 我会设得比 GET 更大一些,15 到 20 秒。

3.4 必调的两个超时参数:连接超时与读取超时

访问普通站点,很多人只调连接超时,忘了读超时。访问微 Web 站点,读超时才是最关键的那个。因为它慢,而且慢得没有规律。我习惯把这两个参数按下面的表格固定下来:

参数设置方法推荐值说明
连接超时HttpConnectionParams.setConnectionTimeout3~8 秒连不上再等也没意义,设备在线的话握手很快
读取超时HttpConnectionParams.setSoTimeout10~20 秒微站点处理慢的主要防线,宁可多等
关闭 ExpectHttpProtocolParams.setUseExpectContinuefalse避免轻量设备不理解 100-continue 而挂起

这两个超时是 socket 层面的,即使你的代码放在后台线程,超时触发也一定会抛 IOException。实际线上我见过最典型的情况是:连接超时设了 30 秒,读超时没设,结果设备端一次重启操作把线程卡了 2 分钟。所以我的默认习惯是连接超时从严、读超时从宽,让“连不上”快速失败,让“处理慢”有足够余量。

4. 针对微 Web 站点调优:连接池、Keep-Alive 与慢响应容忍

4.1 连接池:为什么多线程必须换 ThreadSafeClientConnManager

默认的 DefaultHttpClient 用的是 BasicClientConnManager,它只管理单条连接,同一时刻只能有一个请求在用。老项目如果只有一个线程发请求,问题不明显;一旦改成多线程并发,或者界面刷新和后台定时任务同时访问同一个设备,就会抛 IllegalStateException。

正确的做法是显式创建连接管理器:

// 1. 基础参数 HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, 5000); HttpConnectionParams.setSoTimeout(params, 10000); // 2. 创建线程安全的连接管理器 ThreadSafeClientConnManager connMgr = new ThreadSafeClientConnManager(params); // 3. 调整连接池上限 // 所有目标主机合计最多 20 条连接 HttpConnectionManagerParams.setMaxConnectionsTotal(params, 20); // 同一个目标 IP 最多 4 条连接,够覆盖微站点的并发上限 HttpConnectionManagerParams.setMaxConnectionsPerRoute(params, new HttpHost("192.168.1.100", 80), 4); // 4. 用连接管理器构建客户端 HttpClient client = new DefaultHttpClient(connMgr, params);

理解这几个参数有一个前提:微 Web 站点的并发处理能力很低,连接池开得再大,设备端也是排队处理。所以 maxPerRoute 不需要很大,4 条已经能覆盖绝大多数设备页面的并发场景。把 maxTotal 调大意义也不大,因为你的目标可能就一个 IP。真正要控制的是“不要让池子里积压一堆处于 TIME_WAIT 的僵尸连接”。

这里还有个容易被忽略的点:连接池里的空闲连接是有生命周期的。设备端隔段时间没人访问,可能会主动断掉空闲连接。客户端不知道,下一次执行时复用了这条已经死亡的连接,就会报连接关闭。后面我会专门讲兜底策略。

4.2 慢响应站点的两个杀手:SoTimeout 与 Expect: 100-continue

慢响应站点的第一大杀手是 SoTimeout 设得太小。默认的读超时在某些实现里等于连接超时,也就是 3 到 5 秒,对微站点来说根本不够。设备处理一个带读写 flash 的操作,分分钟超过 5 秒。这个问题的现象很迷惑:超时抛的异常和“站点宕机”几乎一样,都是 SocketTimeoutException,排查时很容易往设备离线方向去查,查半天发现设备其实一直在慢慢处理。

第二大杀手是 Expect: 100-continue。HttpClient 在发送较大请求体时,会先问服务端“我要发 body 了,你准备好接收吗”,服务端返回 100 后客户端才真正发送内容。普通 Web 服务器都支持这套流程,但轻量设备经常不实现。设备端看到 Expect 头,不返回 100,也不响应,客户端就一直在等,直到读超时。对这类站点,直接关掉这个机制最稳妥:

HttpProtocolParams.setUseExpectContinue(params, false);

这一行代码就能避掉很多“设备明明开着,但 POST 就是不回”的诡异现场。我一般在构造 HttpParams 时就关掉,而不是等到具体请求才处理。

与 Expect 类似需要关注的还有 TCP_NODELAY。默认情况下 TCP 会启用 Nagle 算法,把小包合并后再发送,延迟略高。对局域网内的小数据量请求,影响不大,但如果设备端响应一个字节一个字节地往外蹦,关掉 Nagle 可以明显降低交互延迟:

HttpConnectionParams.setTcpNoDelay(params, true);

setTcpNoDelay设置的是 socket 的 TCP_NODELAY 选项,取值 true 表示禁用 Nagle 算法。这个参数适合需要往返多次的协议,比如先认证再拿数据的场景,单次请求改成 true 收益不大。

4.3 服务端“爱断开连接”时的兜底策略:Connection: close 与重建连接池

微站点在连接管理上经常做得不地道,常见的表现是:响应完直接断开,不返回 Content-Length,不设置 Connection 头,甚至对 Keep-Alive 请求也熟视无睹。连接池里的连接复用逻辑在这种情况下会频繁失效。

最简单的兜底策略,是在请求头里直接声明不要保持连接:

HttpGet get = new HttpGet("http://192.168.1.100/status.xml"); get.setHeader("Connection", "close"); // 执行后连接不会被复用,下一次请求重新建连 HttpResponse response = client.execute(get);

Connection: close的含义是告诉服务端,这个请求用完后把连接关掉。服务端响应头里如果带 Connection: close,HttpClient 就会把这条连接从池子里移除而不是放回。代价是每次请求都要重新建立 TCP 连接,对局域网内的设备来说完全可接受,换来的是稳定。

如果某些请求必须复用连接,另一个策略是在捕获到连接关闭异常时主动清理:

try { HttpResponse response = client.execute(get); } catch (IOException e) { // 连接可能已失效,清掉空闲/过期连接再重试一次 client.getConnectionManager().closeExpiredConnections(); client.getConnectionManager().closeIdleConnections(1, TimeUnit.SECONDS); // 重试逻辑:重试前检查请求是否幂等 }

closeExpiredConnections只清理超过有效期上限的连接,closeIdleConnections把空闲超过指定时间的连接全部关掉。这里的“空闲 1 秒”是一个保守值,意思是距离上一次使用超过 1 秒的连接不再信任。这两个方法组合起来,是处理“第二次请求就连接失败”的最快止血方式。

一个更彻底的方案是把连接池换成“每次失败自动重建”。HttpClient 没有直接提供这个开关,我见过有人把 execute 包一层循环,失败后重新 new 连接管理器再执行一次。可行,但要小心:只有 GET 这种幂等请求可以无脑重试,POST 操作设备重启之类的请求重试两次就等于设备执行了两次,必须人工确认。

5. 避坑:Apache HttpClient 在 Android 上的 5 个常见问题

5.1 现象:Android 高版本直接 ClassNotFoundException

现象:App 在 Android 6.0 以下正常,换到高版本系统,一进入网络请求页面就崩溃,堆栈指向 org.apache.http 包下的类,典型的是 EntityUtils 或 DefaultHttpClient。原因很直接:系统把 Apache HttpClient 移除了,代码还在引用它。解决方式是前面说的二选一:在 build.gradle 里加useLibrary 'org.apache.http.legacy',或者把配套 jar 打进 APK。要注意的是,如果项目里同时存在多个 module,建议在最终 app 的 build.gradle 里统一处理,否则某些 module 编译过、打包后被系统裁剪,运行时照样崩。

5.2 现象:读超时 SocketTimeoutException,但站点明明能打开

现象:设备网页在电脑浏览器上秒开,App 里却频繁抛SocketTimeoutException: Read timed out,而且没有固定规律,有时连续成功几次,有时连续失败。原因多半是 SoTimeout 太小。设备端处理请求的耗时波动大,浏览器有自己的等待耐心,而你的代码把读超时设成了 3 秒。解决:把 SoTimeout 调到 10 到 15 秒,同时在日志里记录每次请求的总耗时,用实际数据校准。我见过把 SoTimeout 调到 30 秒依然超时的场景,那种情况基本可以断定设备端服务已经假死,调超时已经没有意义,应该去排查设备进程。

5.3 现象:第二次请求报 Connection closed,第一次明明成功

现象:第一次请求成功,紧接着第二次 execute 抛异常,提示连接已关闭或连接池里的连接无效。原因是微站点处理完第一次请求后主动断开了 TCP 连接,HttpClient 的连接池不知道,第二次直接复用了这条已失效的连接。解决:用前面 4.3 的策略,要么给请求加Connection: close头彻底放弃复用,要么在捕获 IOException 后先closeExpiredConnections和closeIdleConnections再决定是否重试。对这类设备,我的建议是直接放弃长连接,稳定优先。

5.4 现象:同一个 HttpClient 实例并发请求时抛 IllegalStateException

现象:代码里把 HttpClient 写成静态单例,多个线程同时调用 execute,报IllegalStateException: Connection already being used或者Invalid use of BasicClientConnManager。原因是默认的 DefaultHttpClient 内部使用 BasicClientConnManager,它只允许单条连接被一个线程占用。解决:换成 ThreadSafeClientConnManager,并按 4.1 的方式构造客户端。换完还要注意,单例模式下连接池是共享的,对设备的并发请求不要超过 maxPerRoute,否则多余的请求会排队等待空闲连接,表现为“无故变慢”。

5.5 现象:HTTPS 自签名证书握手失败,SSLHandshakeException

现象:微站点开启了 HTTPS,用的是设备出厂自带的证书,App 访问时抛SSLHandshakeException: Chain verification failed。原因是这个证书不是公共可信 CA 签发的,系统默认的信任链里没有它。解决:内网可信环境中可以临时信任所有证书,这是老项目里常见的做法:

SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); SSLSocketFactory sslFactory = SSLSocketFactory.getSocketFactory(); sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); registry.register(new Scheme("https", sslFactory, 443)); ThreadSafeClientConnManager connMgr = new ThreadSafeClientConnManager(params, registry); HttpClient client = new DefaultHttpClient(connMgr, params);

ALLOW_ALL_HOSTNAME_VERIFIER表示不校验证书里的域名与访问地址是否匹配。这个做法只应该出现在内网联调和自建实验室环境,拿到生产环境等于裸奔,任何中间人都能冒充你的设备。生产对接应换用正规证书或固定证书指纹。这条我在项目评审时一定会单独标出来。

6. 进阶:把 HttpClient 封装成一个可维护的访问层

如果你的项目确定要继续用 HttpClient,我强烈建议把上面散落的逻辑收拢成一个简单的访问层,而不是散落各处。核心思路是:全局只创建一个带线程安全连接管理器的客户端,所有请求排队走同一个池子,超时参数集中定义。

先做一个单例客户端:

public class DeviceHttpClient { private static HttpClient client; public static synchronized HttpClient get() { if (client == null) { HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, 5000); HttpConnectionParams.setSoTimeout(params, 15000); HttpProtocolParams.setUseExpectContinue(params, false); HttpConnectionParams.setTcpNoDelay(params, true); ThreadSafeClientConnManager connMgr = new ThreadSafeClientConnManager(params); // 同一台设备并发不要超过 4 条连接 HttpConnectionManagerParams.setMaxConnectionsPerRoute(params, new HttpHost("192.168.1.100", 80), 4); HttpConnectionManagerParams.setMaxConnectionsTotal(params, 10); client = new DefaultHttpClient(connMgr, params); } return client; } }

再封装一个带超时、带连接异常兜底的请求方法。核心是拿到响应后立刻检查状态码,然后读 body。所有调用方都只面对字符串和异常,不需要关心连接池细节。这样一个好处是:将来真要迁移到 OkHttp,只改这一层就够了,业务代码不动。

验证调优效果我有一个固定习惯:在封装层里记录每次请求的“建连耗时”和“总耗时”,连续跑几十次,看 p50 和 p95。微站点网络调优的终点不是“偶尔不报错”,而是“最慢的 5% 请求也不超过预期区间”。如果 p95 还在涨,那就不是客户端参数的问题,要去查设备端的处理逻辑。这套方法帮我避过很多次“看着像是网络库问题、实际是设备端假死”的误判。

最后说一个教训:不要把连接池参数抄得很大。我早期做过一个项目,maxTotal 配了 80,结果二十多个线程同时去刷设备的配置页,设备直接卡死,还连累了其他功能。微站点就要用小连接池加宽超时,这个组合是我踩了坑之后才定下来的。现在每对接一台新设备,我都会先跑一遍小并发压测再上线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询