Socket 与 HTTP 看起来是本科就该掌握的基础知识,恰恰因为如此,面试里很难靠背书过关。真正被追问的是:TCP 连接池为什么这么设计、HTTP/1.1 的队头阻塞到底阻塞了什么、短连接发到几千次为什么会把端口打满、TLS 握手放在哪个阶段。这类问题在 Android 上高频出现的场景是弱网、接口超时、文件上传,答不上来就很难说服人。
先把结论放在前面:Socket 是传输层能力,提供了"进程间字节流双向通信"这一件最基础的事;HTTP 是跑在 Socket 之上的应用层协议,负责定义请求/响应的语义、状态码、头字段、缓存与管线化。两者是协作关系而非替代关系——HTTP 的性能与稳定性问题,根因常常落在 Socket 这一层(连接复用、TCP 拥塞、TLS 握手次数)。
机制拆解
一次完整的 HTTP 请求在 Android 侧的路径大致是:HttpURLConnection 或 OkHttp 拿到 URL → 解析成 host/port/path → 从连接池里找一个可复用的 TCP 连接(没有就新建并做 TLS 握手)→ 拼装请求报文(请求行 + 头 + 体)→ 通过 socket 的输出流写出 → 由操作系统按 MSS 切成 TCP 段发送 → 对端回 SYN/ACK 握手(仅新建时)→ 服务端响应 → 客户端从输入流读回字节 → 按 Content-Length 或 chunked 解码 → 交给业务层。
这条路径里有三处性能关键。第一,连接复用:HTTP/1.1 默认支持 keep-alive,同一个 TCP 连接可以承载多个请求,省掉握手与慢启动的代价;HTTP/2 更是把并发请求多路复用到一条连接上。第二,缓冲与背压:OkHttp 有 Dispatcher 控制并发数、有 ConnectionPool 设定最大空闲连接数,都是为了不让连接数无限膨胀。 第三,超时分级:连接超时、读超时、写超时、调用超时是四件独立的事,只设一个往往兜不住。
再看 Socket 本身。阻塞式 Socket 的模型是一连接一线程,简单可靠但线程数随连接数线性上升,Android 里开几百个 socket 就等于几百个线程,内存和调度都吃不消。NIO 用一个 Selector 在单线程里监听多个 channel 的可读/可写事件,把"一连接一线程"换成"少量线程管大量连接",SelectableChannel + Selector 是它的核心。 Android 里可以直接用 NioSocketImpl 这套,也可以在业务侧用 OkHttp 的异步接口间接享受。
这些坑的正确绕法
最常见的坑是短连接高频请求,把 TIME_WAIT 状态堆到端口耗尽。 表现是跑一段时间后开始报 java.net.SocketException: connect failed: EMFILE 或 Cannot assign requested address,重启能好一阵。 根因是客户端主动关闭连接时会进入 TIME_WAIT(默认 2MSL,60 秒左右),这个状态下 socket 仍占着本地端口,短连接频率一高,四万多的本地端口很快被打满。修法就是连接池化:复用长连接,让同一批 host 只保持少量连接,TIME_WAIT 自然被摊薄到几个端口上。这也是 OkHttp 默认提供连接池的原因。
其次是超时只设一个维度,弱网下请求会长期挂住占用线程。 常见写法是 OkHttpClient 里只放一个 timeout(10, TimeUnit.SECONDS),它约束的是整个调用,而服务端迟迟不回应用时线程会一直占着。 正确做法是分层设:connectTimeout 卡建连(含 DNS 与 TLS),readTimeout 卡两次读之间的间隔,writeTimeout 卡写入,callTimeout 兜住总时长;再配合 Dispatcher 的 maxRequestsPerHost 与连接池上限,防止突发流量把资源打满。
还有一个更隐蔽的坑:把 Content-Length 与实际写入的字节数对不上,服务端在按长度读取时挂起。 表现是请求发出后服务端没有日志,客户端超时;用抓包工具看,body 的长度字段与实际字节数不符。这类问题在文件上传、自定义 RequestBody 时最常见——contentLength() 返回了 -1 或返回了估算值,而流里实际吐出的是别的长度。 对策是自定义 RequestBody 时准确实现 contentLength(),或者显式声明 Transfer-Encoding: chunked。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS) // 建连阶段:DNS + TCP + TLS
.readTimeout(10, TimeUnit.SECONDS) // 两次读之间允许的空闲
.writeTimeout(10, TimeUnit.SECONDS) // 单次写入上限
.callTimeout(20, TimeUnit.SECONDS) // 整次调用兜底
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) // 最多 5 个空闲连接
.retryOnConnectionFailure(true) // 仅重试幂等请求
.addInterceptor(chain -> {
// 统一打点与日志
Request req = chain.request();
long start = System.nanoTime();
Response resp = chain.proceed(req);
Log.d("net", req.method() + " " + req.url() + " -> "
+ resp.code() + " in " + (System.nanoTime() - start) / 1_000_000 + "ms");
return resp;
})
.build();
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(64); // 全局并发上限
dispatcher.setMaxRequestsPerHost(8); // 单 host 并发上限,防被打爆
client.dispatcher().setMaxRequests(64);
client.dispatcher().setMaxRequestsPerHost(8);
这段代码值得盯三处:第一处,四个超时各管一段,缺一个就有一类场景兜不住;第二处,ConnectionPool(5, 5, MINUTES) 明确限制空闲连接数,直接压掉 TIME_WAIT 堆积;第三处,retryOnConnectionFailure 对非幂等请求(POST)需要业务侧自行判断,否则会出现重复下单这类问题,这一条能主动说出来就显出读过源码。
这题在面试里怎么问、怎么答
"Socket 和 HTTP 有什么区别,为什么不能直接用 Socket 请求?"分两层:Socket 只提供字节流,路径、方法、状态码、头字段、超时、重定向全要自己实现;HTTP 在字节流之上加了这些语义,并借助连接复用、管线化、多路复用提升效率。补一句实战对比:自定义二进制协议在极端场景(比如高频心跳、极端省流量)确实更快,但可读性、调试成本、跨端兼容的代价都高,多数业务不值得。
"HTTP/1.1、HTTP/2、HTTP/3 的区别? "HTTP/1.1 引入持久连接、管线化与 Host 头,但同一连接的请求必须按序返回,存在队头阻塞;HTTP/2 在一个 TCP 连接上开多个 stream 多路复用,并把头压成 HPACK 消除了冗余,还支持流优先级与流量控制,队头阻塞被移到传输层——一旦丢包所有流都得等;HTTP/3 把传输层换成基于 UDP 的 QUIC,在用户态做重传与拥塞控制,单个流丢包不影响其他流。 Android 侧 OkHttp 4+ 默认 HTTP/2,配 ConnectionSpec 可启用 HTTP/3。
"弱网下接口成功率下降,你会怎么排查?"给路径而不是结论:先看客户端埋点,区分是 DNS 失败、连接超时、读超时还是服务端返回错误;再对比同一接口在不同网络(WiFi/4G/弱 4G)下的表现差异;然后看服务端日志确认请求是否到达;最后按层次归因——如果服务端没收到,问题在连接与重试策略;如果收到了但慢,问题在服务端或中间链路。 Android 侧可以补一个细节:ConnectivityManager 判断的网络类型与实际链路能力并不等价,标记为 4G 不代表带宽够。
"使用中遇到过什么问题?"案例一:行情类接口每 3 秒轮询一次,跑半小时后开始出现连接失败;定位到每轮新建连接,TIME_WAIT 累积;修复为复用同一个 OkHttpClient 实例并开启连接池。案例二:上传图片偶发超时;定位到自定义 RequestBody 的 contentLength() 与实际字节数不一致,服务端按声明长度等待剩余数据;修复为按文件长度准确返回。
再补一个工程上值得讲清的点:连接池不是越大越好。空闲连接越多,客户端同时持有的 socket 越多,对系统 fd 额度、网络带宽、以及服务端并发能力都是压力。Android 应用通常把空闲连接控制在 5~10 个、单 host 并发控制在 6~8 比较稳妥,具体值要结合机型与服务端能力。面试里能说出"连接池大小需要压测和服务端一起定",比背默认值更像做过真实项目。
给正在准备面试的你
把这条链路画成一张时序图:从上到下依次是应用层(OkHttp API)、协议层(HTTP 请求/响应语义)、传输层(TCP 可靠字节流)、网络层(IP + 路由)。在 TCP 层旁边标两个数字——MSS(每个 TCP 段能带的数据上限,通常 1460 字节)和 RTT(往返时延),说明"传输大文件耗时 ≈ 文件大小 / (MSS / RTT)"这个估算式。 再在 HTTP/1.1 与 HTTP/2 之间标出队头阻塞出现的位置差异。面试中关于网络的问题,考的都是这张图上能不能指对位置。
再补工程案例与踩坑——应用落点是把项目里散落的 new OkHttpClient() 收成单例(每新建一个实例就等于多一套连接池),补齐四类超时与单 host 并发上限,并为文件上传补上 contentLength() 的往返单测。
复习时别孤立刷题:Serializable 与 Parcelable——跨进程传输的数据最终要写进字节流,写得太大会直接撑破 Binder 缓冲区。
划两句重点:Socket 给字节流,HTTP 给语义与复用;短连接高频很容易踩 TIME_WAIT,靠连接池化解;超时要分层设置,连接池大小要压测,不是越大越好。
下一篇聊 Lambda 与函数式接口:Android 开发的日常语法——沿着今天这条主线继续往前走。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:Serializable-与-Parcelable:Android-为什么偏爱后者
下一篇预告:Lambda-与函数式接口:Android-开发的日常语法
有任何问题欢迎在评论区留言交流。