使用 Jsoup 做網頁解析時,Jsoup.connect(url).get() 偶爾拋出 java.net.SocketTimeoutException: Read timed out,導致任務失敗。本文從異常成因、核心 API、底層網絡模型、生產級配置策略四個維度,給出徹底解決方案。
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(...)
at sun.net.www.protocol.http.HttpURLConnection$HttpInputStream.read(...)
at org.jsoup.helper.HttpConnection$Response.execute(HttpConnection.java:447)
at org.jsoup.helper.HttpConnection.get(HttpConnection.java:148)
at com.example.Crawler.main(Crawler.java:17)
關鍵點:
SocketInputStream.read() —— TCP 層面讀取數據超時。HttpURLConnection,未顯式設置超時時,採用 JVM 預設值(通常為無限大或極長)。// 單位:毫秒。5000 = 5 秒
Document doc = Jsoup.connect(url)
.timeout(5000) // ★ 關鍵:同時設置連接超時 和 讀取超時
.get();
Connection.timeout(int millis) 內部同時設置:
HttpURLConnection.setConnectTimeout(millis) — 建立 TCP 連接階段(三次握手)超時。HttpURLConnection.setReadTimeout(millis) — 連接建立後,讀取數據階段超時。注意: Jsoup 1.10.2+ 版本 timeout() 同時設置兩者;舊版本僅設置讀取超時,連接超時需單獨調用 .connectTimeout()(若存在)。
| 場景 | 未設置 timeout | 設置 timeout(5000) |
|---|---|---|
| 正常響應 (200ms) | ✅ 成功 | ✅ 成功 |
| 服務器緩慢響應 (8s) | ⏳ 無限等待 / 最终超時 | ❌ 5s 後拋 SocketTimeoutException,快速失敗 |
| 服務器掛了 / IP 被封 | ⏳ TCP 重傳指數退避,極久 | ❌ 5s 後快速失敗 |
| 網絡抖動丟包 | ⏳ 不確定 | ❌ 可控超時,便於重試 |
某些場景需差異化控制(如連接快但下載大文件慢):
Connection conn = Jsoup.connect(url)
.connectTimeout(3000) // 3 秒建立連接
.readTimeout(30000); // 30 秒讀取數據(大文件/慢速接口)
// 驗證方法是否存在(Jsoup 版本差異)
try {
Method m = Connection.class.getMethod("connectTimeout", int.class);
m.invoke(conn, 3000);
} catch (NoSuchMethodException e) {
// 老版本 fallback
conn.timeout(3000);
}
public class CrawlerTimeoutConfig {
// 快速 API / 列表頁
public static final int FAST_CONNECT = 2000;
public static final int FAST_READ = 5000;
// 詳情頁 / 大文件
public static final int SLOW_CONNECT = 5000;
public static final int SLOW_READ = 30000;
// 代理 / 海外站點
public static final int PROXY_CONNECT = 10000;
public static final int PROXY_READ = 60000;
}
public Document fetchWithRetry(String url, int maxRetry) {
int attempt = 0;
while (true) {
try {
return Jsoup.connect(url)
.timeout(CrawlerTimeoutConfig.FAST_READ)
.userAgent("Mozilla/5.0...")
.get();
} catch (SocketTimeoutException e) {
if (++attempt >= maxRetry) throw e;
long delay = (long) (Math.pow(2, attempt) * 1000 + Math.random() * 1000);
Thread.sleep(delay); // 1s, 3s, 7s, 15s...
} catch (IOException e) {
// 非超時錯誤(404, 500, DNS 失敗)直接拋出或按業務處理
throw e;
}
}
}
public CompletableFuture fetchAsync(String url) {
return CompletableFuture.supplyAsync(() -> {
try {
return Jsoup.connect(url).timeout(5000).get();
} catch (IOException e) {
throw new CompletionException(e);
}
}, crawlerExecutor).orTimeout(8, TimeUnit.SECONDS); // 總體預算 8s
}
SocketTimeoutException: connect timed out。InputStream.read()。Socket 已建立,應用層調用 read 等待數據到達。若毫秒數內無數據可讀,拋出 SocketTimeoutException: Read timed out。PlainSocketImpl / SocksSocketImpl)通過 setsockopt(SO_TIMEOUT) 實現,屬於內核級定時器,精度受 OS 調度影響(通常 10-100ms 粒度)。| 坑點 | 現象 | 解決 |
|---|---|---|
| timeout(0) 表示無限等待 | 文檔未明確說明,實測 0 視為無限 | 顯式設置 >0 值,勿用 0 |
| 代理環境下連接超時不生效 | 某些 JVM 版本代理隧道建立不受 connectTimeout 控制 | 升級 JDK / 使用 Apache HttpClient 替代 |
| HTTPS 握手時間計入 connectTimeout | SSL/TLS 握手較慢,易誤判為連接超時 | 適當放寬 connectTimeout (5-10s) |
| 多線程爬蟲共享 Connection 對象 | Connection 非線程安全,超時設置互相干擾 | 每次請求 new Connection() 或 ThreadLocal |
| 忽略 SocketTimeoutException 繼承關係 | catch (Exception e) 吞掉超時異常,導致無限重試 | 精確捕獲 SocketTimeoutException 區分處理 |
Jsoup 內部用 HttpURLConnection,功能受限。複雜場景建議:
// OkHttp 3.x - 更現代、支援 HTTP/2、連接池、攔截器
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
String html = response.body().string();
Document doc = Jsoup.parse(html); // 僅用 Jsoup 做解析
}
Jsoup.connect(url).timeout(5000).get() 是解決超時的「銀彈」,但生產系統需要:分層超時配置、指數退避重試、異步預算控制、精確異常分類。理解底層 connectTimeout/readTimeout 語義差異,避免「一個 timeout 走天下」的懶惰配置。對於高併發、高可靠爬蟲,建議將網絡層遷移至 OkHttp/HttpClient,Jsoup 專注做它最擅長的事——HTML 解析。