做Unity项目,无论是单机游戏还是联网应用,总会遇到一个绕不开的基础需求:判断当前设备有没有联网。我最早做这块的时候特别天真,以为查一下Application.internetReachability就完事了,结果被各种“路由能通但外网不通”“连着Wi-Fi却拿不到数据”的bug折腾到怀疑人生。后来踩过几次坑,才把这块从“能用”磨到了“够稳”。
这篇文章就来聊聊Unity里判断网络状态的两种核心方式:一种是用Unity自带的Application.internetReachability做快速的本机网络接口判断,另一种是用Ping指令主动探测目标主机,真正验证“能不能连到外网”。两种方式各有各的适用场景,大多数项目其实需要把它们结合起来用。这篇内容适合正在做断线重连、登录鉴权、资源热更前检测等联网相关逻辑的开发者参考,看完可以直接抄代码,也能少踩我踩过的那几个坑。
1. 先搞清楚:判断网络状态到底要判断什么
很多人在这个功能上翻车,不是代码写错,而是压根没想清楚需求。同样是“判断网络状态”,在不同场景下指的可能是完全不同的东西。我先把这句话拆开讲,后面写代码才不会绕弯路。
1.1 三个层次的网络判断
我习惯把网络状态判断分成三个层次:
- 第一层:本机有没有可用的网络接口。也就是网卡有没有连上路由、Wi-Fi是不是开了,这是
Application.internetReachability能回答的问题。 - 第二层:当前网络能不能访问到目标服务器。接口虽然是通的,但路由器没拨号成功、运营商链路异常、目标服务器挂了,都会导致“有网但连不上”,这需要通过主动探测来验证。
- 第三层:网络质量如何。延迟高不高、丢包率大不大、带宽够不够,这属于弱网检测的范畴,通常要结合业务层的请求耗时、重试次数来做综合分析。
如果把这三层对应到实际业务里,你会发现不同模块的需求差异很大。比如启动画面的离线提示,只需要第一层就够了;登录逻辑必须做到第二层,否则玩家点在登录按钮上半天没反应,体验非常差;而实时对战里的断线重连、帧同步质量监控,那就得落到第三层。很多刚接触网络检测的开发者容易犯的错误,就是拿第一层的能力去硬扛第二层的需求,结果线上出了问题还找不到原因。
1.2 两种核心方式的能力边界
我把Unity里最常用的两种判断方式放在一起打了个表:
| 维度 | Application.internetReachability | Ping/主动探测 |
|---|---|---|
| 判断目标 | 本机网络接口 | 目标主机连通性 |
| 响应速度 | 即时返回 | 几百毫秒到数秒 |
| 网络开销 | 无 | 会产生实际探测流量 |
| 能否发现“有网但连不上” | 不能 | 能 |
| 实现复杂度 | 低 | 中 |
| 平台差异 | 小 | 大(尤其WebGL) |
从这个表能看出来,两种方式不是“谁替代谁”的关系,而是互补关系。internetReachability负责快速筛查,Ping负责最终确认。后面的章节我会分别把它们的原理、写法和坑讲透。
2. 第一种方式:Application.internetReachability快速判断
这应该是绝大多数Unity开发者接触到的第一个网络判断API。原因很简单:不用引任何库,不用配任何权限,一行代码就能拿到当前状态。
2.1 这个API背后到底返回了什么
Application.internetReachability返回的是一个NetworkReachability枚举,一共三个值:
NotReachable:当前设备没有任何可用的网络连接,也就是Wi-Fi没连、蜂窝网络没开,或者处于飞行模式。ReachableViaCarrierDataNetwork:当前处于移动蜂窝网络(4G/5G)连接状态。ReachableViaLocalAreaNetwork:当前处于Wi-Fi或有线局域网连接状态。
注意,这个API在部分平台上只是“系统网络接口是否可用”的映射,它不会真的发一个包去验证外网通不通。你把它当成“网卡状态”来理解最准确。它连的是路由器还是基站、路由器的外网通不通,它一概不管。
2.2 最小可用代码与基础封装
直接用的代码很简单:
using UnityEngine; public class QuickNetworkCheck : MonoBehaviour { private void Update() { if (Application.internetReachability == NetworkReachability.NotReachable) { Debug.Log("当前无网络"); } else if (Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork) { Debug.Log("当前使用蜂窝网络"); } else { Debug.Log("当前使用Wi-Fi或局域网"); } } }但在真实项目里,我建议包一层,方便以后替换实现,也方便给其他模块复用。一个比较通用的封装长这样:
public enum NetInterfaceType { None, Cellular, WifiOrLan } public static class NetworkStatusUtil { public static NetInterfaceType GetInterfaceType() { switch (Application.internetReachability) { case NetworkReachability.NotReachable: return NetInterfaceType.None; case NetworkReachability.ReachableViaCarrierDataNetwork: return NetInterfaceType.Cellular; default: return NetInterfaceType.WifiOrLan; } } public static bool HasNetworkInterface() { return GetInterfaceType() != NetInterfaceType.None; } }提示:在Android/iOS平台,
ReachableViaCarrierDataNetwork只在真实插了SIM卡并开启数据流量时才会出现。部分安卓平板没有蜂窝模块,永远不会进入这个状态,别到时候排查半天以为是代码问题。
2.3 这个方案的坑在哪里
这个API最明显的坑,就是它只告诉你“有没有网卡”,不告诉你“能不能上网”。
我遇到过最典型的例子就是路由器WAN口没拨上号,但设备连着Wi-Fi。这种情况下Application.internetReachability返回的是ReachableViaLocalAreaNetwork,看起来一切正常,实际上外网完全不通。如果你拿它做登录前的网络校验,用户会被卡在登录界面,而且你还不知道问题出在哪。
另一个坑是它在模拟器上的表现不一致。安卓模拟器里经常出现“本机有网但枚举结果却是NotReachable”的反向情况,尤其是早期版本。所以如果你们的测试环境里有模拟器,记得提醒QA同学别被这种误报干扰。
3. 第二种方式:用Ping主动探测目标主机
既然internetReachability不够用,那就要想办法做真正的连通性验证。最直接的方式就是Ping:给目标服务器发一个ICMP回显请求,能收到回应,说明从本机到目标主机的网络通路基本是通的。
3.1 为什么要主动探测:从“有网”到“真的能连”
拿开车来打比方。internetReachability只告诉你“油箱里有油”,Ping则是真正把车发动起来跑一段路,确认发动机、变速箱、轮胎都没问题。
主动探测能挡掉一大类“看似有网实则不通”的故障:运营商拨号失败、DNS解析异常、目标服务器宕机、防火墙策略拦截等。这些故障用internetReachability完全看不出来,但Ping能非常明确地反馈结果。
3.2 基于UnityEngine.Ping的最小实现
Unity引擎自带的Ping类用起来很直接:
using UnityEngine; using System.Collections; public class PingChecker : MonoBehaviour { public string target = "223.5.5.5"; [ContextMenu("Ping Now")] public void PingNow() { StartCoroutine(PingCoroutine()); } private IEnumerator PingCoroutine() { Ping ping = new Ping(target); float timeout = 3f; float elapsed = 0f; while (!ping.isDone && elapsed < timeout) { elapsed += Time.deltaTime; yield return null; } if (ping.isDone && ping.time >= 0) { Debug.Log($"Ping成功,延迟{ping.time}ms"); } else { Debug.LogWarning("Ping超时或失败,外网不可达"); } } }几个关键点说一下。
第一,构造Ping就会立刻发起探测,不需要再手动调别的接口。
第二,ping.isDone为true说明已经有结果了,但结果不一定代表成功。要判断是否真的连通,还得看ping.time是否大于等于0,失败时这个值会是-1。
第三,Ping类没有内置超时机制,必须自己用协程或Update做超时控制。上面代码里给了3秒超时,实际项目里可以根据业务容忍度调整。如果是登录前的检测,一般不超过5秒;如果是后台的静默探测,可以放到8到10秒。
第四,target最好传IP而不是域名。虽然Ping的构造函数也接受主机名,但DNS解析会带来额外延迟,而且解析失败时会直接抛异常,影响判断结果。所以稳定做法是:服务器配置下发的时候,同时下发IP和域名,优先用IP做连通性探测。
3.3 基于System.Net.NetworkInformation.Ping的异步实现
除了Unity自带的Ping,C#的System.Net.NetworkInformation命名空间里也有一个Ping类,支持异步方法,使用起来更接近现代C#风格。为避免和Unity的Ping冲突,一般给这个类起个别名:
using System.Net.NetworkInformation; using System.Threading.Tasks; using UnityEngine; using DotNetPing = System.Net.NetworkInformation.Ping; public static class AsyncPingChecker { public static async Task<bool> PingAsync(string host, int timeoutMs = 3000) { try { using (DotNetPing ping = new DotNetPing()) { PingReply reply = await ping.SendPingAsync(host, timeoutMs); return reply.Status == IPStatus.Success; } } catch (System.Exception e) { Debug.LogWarning($"Ping异常: {e.Message}"); return false; } } }调用时直接await:
private async void OnLoginButtonClick() { bool online = await AsyncPingChecker.PingAsync("223.5.5.5"); if (!online) { ShowToast("网络不可用,请检查网络后重试"); return; } StartLogin(); }3.4 两种Ping实现的取舍
| 对比项 | UnityEngine.Ping | System.Net.NetworkInformation.Ping |
|---|---|---|
| 使用方式 | 协程/轮询 | 异步/回调 |
| 并发能力 | 弱,推荐串行 | 支持并发 |
| WebGL支持 | 受限 | 不支持 |
| 异常抛出 | 构造阶段可能抛异常 | 方法内部已捕获 |
| 代码风格 | 偏Unity传统 | 偏现代C# |
如果项目还在用协程体系,或者目标平台包含手机和PC,UnityEngine.Ping足够用。如果代码风格已经升级到async/await,而且不需要考虑WebGL,那.NET版本的Ping用起来更顺手。
注意:早期版本的Unity中,
UnityEngine.Ping同一时间只允许一个实例存在,同时创建多个会收到“Ping already in progress”的报错。如果你的代码里有并发ping的需求,建议维护一个队列串行执行,或者换用.NET版本的Ping。
4. 实战选型:两种方式怎么组合最合理
讲完两种方式各自的实现,接下来是重头戏:在真实项目里到底怎么用。我的经验是不要二选一,而是组合出一个状态机,让它们各司其职。
4.1 推荐的项目级网络状态机
我比较喜欢把网络状态定义成三档:
public enum NetworkStatus { Unknown, // 未知,还没检测 NotReachable, // 完全没网 LocalOnly, // 有网卡但外网不通 Online // 外网可达 }检测流程可以这样设计:
- 先用
GetInterfaceType()查本机接口,如果返回None,直接置为NotReachable,不再做后面的主动探测。 - 如果接口存在,再用Ping探测目标主机。
- Ping通就是
Online,超时没通就是LocalOnly。
这样既能快速处理“飞行模式”这种一眼就知道没网的情况,又不会漏掉“路由器没拨号”这种隐蔽故障。伪代码如下:
public static NetworkStatus DetectNetwork(string probeHost = "223.5.5.5") { NetInterfaceType interfaceType = NetworkStatusUtil.GetInterfaceType(); if (interfaceType == NetInterfaceType.None) { return NetworkStatus.NotReachable; } bool internetReachable = AsyncPingChecker.PingAsync(probeHost).GetAwaiter().GetResult(); return internetReachable ? NetworkStatus.Online : NetworkStatus.LocalOnly; }这里要单独提醒一句:Unity主线程里用GetResult()同步阻塞异步方法有卡死风险,上面的伪代码只是为了表达流程,真实项目中建议包到协程或async方法里一步步await,别图省事直接用阻塞写法。
4.2 探测目标的选取原则
Ping的目标主机选择很讲究,我踩过几次坑之后总结出了几条原则:
- 优先选大厂公共DNS,比如223.5.5.5(阿里)、119.29.29.29(腾讯),这类IP稳定性好,不会轻易挂。
- 不要只配一个目标,至少要两个,一个挂了自动换另一个。很多玩家使用的是小区宽带,运营商链路本身的差异很大,一个IP没法代表“全网通不通”。
- 如果能拿到自己服务器的IP,而且服务器允许Ping,那优先ping自己的服务器,因为“玩家能连到你的服务器”才是业务上真正关心的连通性。
- 目标主机不要频繁更换,不然缓存下来的历史状态没有对比价值。
4.3 不同游戏类型的落地建议
单机游戏只在启动画面做个离线提示,用internetReachability就完全足够,成本最低,也不会给玩家带来额外等待。
需要登录才能玩的手游和网游,登录前必须做一次真正的连通性探测。我建议登录按钮点击后,先走一层Ping,Ping通再发登录请求,否则用户点完登录等半天超时,体验极差。这里要记得在UI上给出明确的“网络检测中”状态,不然玩家会以为按钮坏了。
有断线重连机制的实时对战游戏,光做启动检测不够,运行期间也要持续维护网络状态。这时候建议做一个NetworkMonitor组件,用低频率轮询(我一般2到5秒一次)持续检测,并且只在状态发生变化时对外发事件,避免频繁通知UI刷新。
using System; using System.Collections; using UnityEngine; public class NetworkMonitor : MonoBehaviour { public static event Action<bool> OnNetworkStateChanged; [SerializeField] private float checkInterval = 2f; private bool _lastOnline; private void Start() { _lastOnline = CheckNow(); StartCoroutine(CheckLoop()); } private IEnumerator CheckLoop() { while (true) { yield return new WaitForSeconds(checkInterval); bool online = CheckNow(); if (online != _lastOnline) { _lastOnline = online; OnNetworkStateChanged?.Invoke(online); } } } private bool CheckNow() { // 这里可以串一个完整的三档检测流程 return NetworkStatusUtil.HasNetworkInterface() && AsyncPingChecker.PingAsync("223.5.5.5").GetAwaiter().GetResult(); } }4.4 WebGL等特殊平台的替代思路
WebGL平台比较特殊,传统ICMP Ping在浏览器沙箱环境下基本不可用,UnityEngine.Ping的行为也不稳定。遇到这种情况,我通常改用UnityWebRequest发一个轻量的HTTP探测。
using UnityEngine; using UnityEngine.Networking; using System.Collections; public class HttpProbe : MonoBehaviour { public IEnumerator CheckServer(string url) { using (UnityWebRequest request = UnityWebRequest.Head(url)) { request.timeout = 3; yield return request.SendWebRequest(); bool success = request.result == UnityWebRequest.Result.Success; Debug.Log(success ? "HTTP探测成功" : "HTTP探测失败"); } } }注意:WebGL下跨域请求受CORS限制。如果服务器没配好跨域头,就算网络通的,探测请求也会报错。这种情况不算网络故障,但用HTTP探测时容易误判成“不通”,最好在逻辑里单独区分CORS错误和其他网络错误。
5. 常见问题与排查技巧实录
最后把做网络检测时踩过的坑集中列一下,很多是文档里查不到的细节。
5.1 明明连着Wi-Fi却提示无网络
遇到这种情况先别急着怀疑代码。先确认是不是路由器WAN口没拨号成功,直接拿手机浏览器试一下能不能上网。如果设备能上网,但internetReachability返回NotReachable,多半是系统网络状态缓存异常,重启Unity编辑器或设备通常能解决。如果是模拟器,那先换真机测,模拟器的网络枚举行为跟真机差异很大。
5.2 Ping超时但网络正常的情况
这大概率不是网络问题,而是目标主机禁止了ICMP。很多服务器出于安全考虑会屏蔽Ping请求,但HTTP服务是正常的。遇到这种情况,要么换一个允许Ping的探测目标,要么改用HTTP探测。我一般会在项目里配置多个探测目标,Ping不通就换下一个,全部超时才判定为网络不可用。另外,DNS解析失败也会导致Ping超时,所以用IP作为探测目标能减少一层干扰。
5.3 多平台打包后的行为差异
同一个检测逻辑,在iOS、Android、PC、WebGL上的表现可能完全不一样。原因主要有三个:一是系统网络接口获取方式不同,二是ICMP受限程度不同,三是浏览器沙箱限制不同。强烈建议在目标平台的真机上各跑一遍检测用例,别只看Unity编辑器的结果。编辑器里网络环境比较特殊,internetReachability基本默认返回ReachableViaLocalAreaNetwork,Ping也基本都能通,很多问题在编辑器里根本复现不出来。
5.4 性能与频率:轮询检测的正确姿势
不要在Update里每帧Ping,也不要在Update里每帧检查internetReachability。虽然internetReachability本身开销极小,但Ping是会产生实际网络流量的。我一般这样设计:启动时立即检测一次,然后每2秒检测一次,连续3次全是离线状态才对外报告离线,连续2次成功才对外报告在线,中间状态不要反复弹窗。这样既不会频繁刷新UI,也避免了单次抖动造成的误判。
再补充一个实现细节:UnityEngine.Ping失败后如果想立刻重试,需要重新new一个实例,不能复用旧对象。用完之后把引用置空,避免不必要的对象长期占用。如果项目里同时有几处地方都要做网络判断,最好通过单例或静态工具类统一走同一个检测通道,避免多个模块各查各的,导致状态不一致。
我在实际项目里最深刻的体会是,网络检测功能不是“有没有”的问题,而是“够不够稳”的问题。很多同学写几行internetReachability判断就交差了,但上线之后断线重连、登录超时、资源下载失败,全是在这一层栽跟头。如果你做的是需要联网的项目,建议从一开始就把“本机接口检测+主动探测”这个组合做进去,并且预留好缓存和状态事件,后面加功能会轻松很多。
最后再分享一个小技巧:探测目标一定不要只配一个,至少配两个IP轮流用,一个挂了还能自动切换,这个细节在线上环境能少挨好几次骂。