返回

React Native 安卓模拟器 Fetch 本地失败? 原因与解决方法

Android

React Native 安卓模拟器 Fetch 请求偶发性失败?原因和解决办法来了!

搞 React Native 开发,尤其是需要和本地 API 对接时,安卓模拟器有时会给你整点“小惊喜”。比如,明明很简单的 fetch 或者 axios 网络请求,在模拟器上跑就是时灵时不灵,报个可恶的 Network Error,但在真机上却跑得欢快得很。就像下面这段常见的请求代码,在模拟器里可能 95% 的时间都在罢工:

// -- 省略部分代码,保留核心请求逻辑 --

const HOST = "localhost"; // 问题往往就出在这!
const PORT = "8069";
// ... 其他常量 ...

const webAuth = async () => {
  // ... 省略身份验证请求,也使用了 HOST 和 PORT ...
  // 这个请求可能也会不稳定
};

async function checkInOutSystem(latitude = 20, longitude = 20) {
  try {
    const sessionCookie = await webAuth(); // 先获取认证信息

    const locData = JSON.stringify({
      jsonrpc: "2.0",
      method: "call",
      params: { latitude, longitude },
    });

    // 这个请求在模拟器上特别容易失败
    const attendanRes = await axios.post(
      `http://${HOST}:${PORT}/hr_attendance/systray_check_in_out`,
      locData,
      {
        headers: {
          "Content-Type": "application/json",
          Cookie: sessionCookie,
          "Accept": "application/json",
        },
        // 使用 fetch 也会有同样的问题
      }
    );

    console.log("attendanceRes status:", attendanRes.status);
    if (attendanRes.status !== 200) {
        // 注意: axios 在 status 非 2xx 时可能会抛出异常,
        // 所以这里的检查可能不会执行,错误会在 catch 块中捕获
        throw new Error("Check-in/out request failed with status: " + attendanRes.status);
    }

    console.log("responseJson", attendanRes.data);
    return attendanRes.data;

  } catch (error) {
    // axios 的网络错误通常在这里捕获,error.message 可能包含 "Network Error"
    console.error("Check-in/out error:", error.message); 
    // 如果是 axios 错误,可以检查 error.response, error.request 等获取更多信息
    if (error.response) {
        // 请求发出去了,服务器返回了非 2xx 状态码
        console.error("Error Response Data:", error.response.data);
        console.error("Error Response Status:", error.response.status);
        console.error("Error Response Headers:", error.response.headers);
    } else if (error.request) {
        // 请求发出了,但没有收到响应(例如,网络中断、DNS 问题)
        // "Network Error" 常常落在这里
        console.error("Error Request:", error.request);
    } else {
        // 设置请求时或处理响应时发生了其他错误
        console.error('Error', error.message);
    }
    throw error; // 重新抛出,以便调用者处理
  }
}

遇到这种情况,是不是很抓狂?明明代码逻辑简单清晰,android:usesCleartextTraffic="true" 也乖乖设置了(允许 HTTP 请求),换了 fetchaxios 也都一样,为啥偏偏在模拟器里就“水土不服”?

别急,这通常不是 React Native 或者 fetch/axios 的锅,而是安卓模拟器网络环境的“小特性”在作祟。

一、问题分析:模拟器的“ localhost ”不是你想的“ localhost ”

这个问题的核心在于模拟器的网络环境和你的开发电脑(宿主机)的网络环境是隔离的

  • 在你的电脑(宿主机)上: localhost 或者 127.0.0.1 指向的是电脑本身。
  • 在安卓模拟器里: 模拟器是一个独立的虚拟设备,它也有自己的网络栈。在模拟器内部,localhost 或者 127.0.0.1 指向的是模拟器自己 ,而不是你的开发电脑!

所以,当你的 React Native 应用在模拟器里尝试请求 http://localhost:8069 时,它实际上是在尝试连接模拟器内部的 8069 端口,而不是你电脑上跑的那个后端服务的 8069 端口。你的后端服务根本没跑在模拟器里,自然就连接失败,报 Network Error 了。

那为什么偶尔会成功呢?

这确实有点奇怪。严格来说,直接用 localhost 就不应该成功。偶尔的成功可能是某些特殊网络配置下的巧合,或者是网络堆栈初始化过程中的短暂“错觉”。但根本原因在于,目标地址指错了。依赖这种“偶尔成功”是绝对不可靠的。

那为什么真机没问题?

当你在真实安卓设备上通过 ADB (Android Debug Bridge) 连接调试时,情况又不一样了:

  1. 如果你的手机和电脑在同一个局域网(WIFI),你可以直接使用电脑的局域网 IP 地址来访问服务。
  2. 更常见的是,如果手机通过 USB 连接电脑并开启了调试,React Native 的开发服务器 (Metro) 会自动设置一些反向代理,使得设备可以通过特定的方式(比如 localhost:8081 通常被映射到 Metro)访问到开发电脑上的服务。对于你自己启动的 localhost:8069 服务,虽然 Metro 不会自动代理,但因为手机和电脑间的连接通常更稳定,直接访问电脑的局域网 IP 是标准做法。或者,ADB 本身提供了一些端口转发能力。

关键在于,真机连接宿主机的网络路径和模拟器是不同的。

二、解决方案:让模拟器找到你的“真身”

既然知道了问题根源在于模拟器里的 localhost 指向不对,解决办法就很清晰了:我们需要给模拟器提供一个能访问到宿主机的正确地址或路径。

方案一:使用特殊 IP 10.0.2.2 (推荐)

原理:
安卓模拟器提供了一个特殊的 IP 地址 10.0.2.2,它就是模拟器内部用来代表宿主机 localhost/127.0.0.1 的“替身”。只要你的后端服务监听在 0.0.0.0 或者 127.0.0.1(也就是宿主机的本地地址)上,模拟器通过 10.0.2.2 就能访问到它。

操作步骤:
非常简单,只需要修改代码里 HOST 常量的值:

// 将 HOST 修改为模拟器的特殊回环地址
const HOST = "10.0.2.2";
const PORT = "8069";
// DB 和其他代码保持不变

// ... webAuth 和 checkInOutSystem 函数现在会使用正确的 IP 地址 ...

async function checkInOutSystem(latitude = 20, longitude = 20) {
  try {
    const sessionCookie = await webAuth(); // webAuth 内部也使用了 HOST

    // ... 其他代码 ...

    // 请求现在会指向 http://10.0.2.2:8069/...
    const attendanRes = await axios.post(
      `http://${HOST}:${PORT}/hr_attendance/systray_check_in_out`,
      // ... 请求体和配置 ...
    );
    
    // ... 后续处理 ...
    
  } catch (error) {
    console.error("Check-in/out error:", error.message);
    throw error;
  }
}

优点:

  • 简单直接,改动小。
  • 是官方推荐的模拟器访问宿主机的方式。

缺点:

  • 这个 IP 10.0.2.2 只在安卓模拟器里有效。如果你的代码需要同时在真机、iOS 模拟器或生产环境运行,你需要做平台判断或环境配置。

进阶使用技巧:根据环境动态设置 HOST

为了方便在不同环境(模拟器、真机、生产)切换,你可以使用环境变量或者平台判断来动态设置 HOST

import { Platform } from 'react-native';

// 假设你有一个环境变量或配置文件来区分环境
const isDevelopment = process.env.NODE_ENV === 'development';
const isAndroidEmulator = Platform.OS === 'android' && !Platform.isTV && !Platform.isPad; // 粗略判断,可能需要更精确方式

let API_HOST;

if (isDevelopment && isAndroidEmulator) {
  API_HOST = "10.0.2.2"; // 安卓模拟器开发环境
} else if (isDevelopment) {
  // 真机开发环境,或者 iOS 模拟器 (它通常能直接用 localhost 或宿主机局域网 IP)
  // 最好使用宿主机的局域网 IP,需要动态获取或配置
  API_HOST = "YOUR_COMPUTER_LAN_IP"; // 例如 "192.168.1.100"
  // 或者,如果 iOS 模拟器可以直接访问 localhost:
  // API_HOST = Platform.OS === 'ios' ? 'localhost' : 'YOUR_COMPUTER_LAN_IP';
} else {
  API_HOST = "your.production.api.com"; // 生产环境 API 地址
}

const PORT = "8069"; // 端口通常不变,除非环境不同

// 在你的请求代码中使用 API_HOST 和 PORT
const endpoint = `http://${API_HOST}:${PORT}/path/to/resource`;

注意: 获取宿主机局域网 IP 可能需要手动查找或使用脚本。

方案二:使用 ADB 反向代理 (adb reverse)

原理:
adb reverse 命令可以在你的开发电脑和模拟器/连接的设备之间建立一个反向端口转发通道。命令 adb reverse tcp:<device-port> tcp:<host-port> 的意思是:让设备(模拟器)上的 <device-port> 端口的所有 TCP 连接,都转发到宿主机上的 <host-port> 端口。

这样设置后,你的应用代码就不需要 修改 HOST10.0.2.2 了,可以直接继续使用 localhost。因为模拟器访问自己的 localhost:<device-port> 时,ADB 会自动把流量转给你电脑上的 <host-port>

操作步骤:

  1. 确保 ADB 可用: 你的电脑需要安装 Android SDK Platform Tools,并且 adb 命令在终端/命令行里能用。
  2. 运行模拟器或连接设备。
  3. 打开终端或命令行,执行以下命令:
    adb reverse tcp:8069 tcp:8069
    
    这个命令的意思是,把模拟器(或设备)的 8069 端口映射到宿主机的 8069 端口。
  4. 保持这个终端窗口打开,或者确保转发规则生效。 现在,你在模拟器应用里请求 http://localhost:8069 就能成功访问到宿主机的服务了。

优点:

  • 应用代码无需修改,可以直接使用 localhost,对跨平台代码更友好。

缺点:

  • 每次启动模拟器(或断开重连 ADB)后,都需要重新执行 adb reverse 命令 。这有点麻烦。
  • 如果 <device-port> 在模拟器内部已经被其他服务占用,命令会失败。

如何移除转发规则?

adb reverse --remove tcp:8069

或者移除所有规则:

adb reverse --remove-all

进阶使用技巧:脚本化

你可以将 adb reverse 命令写到一个启动脚本里,每次启动开发环境时自动执行,减少手动操作。

方案三:检查网络和防火墙(辅助排查)

虽然 10.0.2.2adb reverse 是最常见的解决方案,但如果这两个方法都试了还不行(理论上不太可能),或者你想彻底排查,可以检查以下几点:

  • 宿主机防火墙: 确认你电脑的防火墙没有阻止来自模拟器的连接。尝试暂时关闭防火墙(注意安全风险! )测试一下,如果能通,说明需要为你的后端服务端口(例如 8069)添加入站规则。完成后务必重新开启防火墙。
  • 杀毒软件: 有些杀毒软件的网络防护功能也可能干扰连接,检查其设置。
  • 后端服务绑定地址: 确保你的本地后端服务监听在 0.0.0.0:8069127.0.0.1:8069 (或 localhost:8069)。如果它只监听了特定的 IP 地址(比如以太网卡 IP),那么 10.0.2.2adb reverse 转发过来的请求可能也无法被接受。监听 0.0.0.0 通常是最兼容的,表示接受来自任何网络接口的连接。
  • 模拟器网络设置: 检查模拟器的网络配置(虽然一般不需要动),确保它能访问外部网络(例如,用模拟器里的浏览器访问百度试试)。

安全建议:

  • 绝对不要 在生产环境中硬编码 10.0.2.2 或者你本地开发电脑的 IP 地址。生产环境应该使用正式的 API 域名。
  • 使用 adb reverse 时,相当于把宿主机的一个端口暴露给了模拟器,确保你的本地服务本身是安全的,不会被模拟器内的潜在恶意应用利用。
  • 谨慎关闭防火墙 ,即使是临时测试,也要记得及时恢复。

通常情况下,将 HOST"localhost" 改为 "10.0.2.2" 是解决 React Native 安卓模拟器访问宿主机 localhost 服务时网络请求不稳定的最快、最直接的方法。如果想保持代码的统一性,adb reverse 也是个不错的选择,只是需要点额外的设置步骤。希望这些方法能帮你摆脱模拟器网络问题的困扰!