网络唤醒 WOL
(Wake On LAN)可以让已经进入关机状态的设备通过局域网设备对其网卡发送命令,从关机状态唤醒转成开机状态。
1. 开启 WOL 功能
在 Windows
中,需要在 BIOS
中启用 WOL
功能,并进行相关配置。以华硕主板为例,配置过程如下。
1.1 在 BIOS 中启用 WOL 功能
根据华硕主板官方教程,开机并按 f2
或 del
进入华硕主板 BIOS
,进入高级选项
-> APM 配置
-> 开启 Power On By PCI-E
,如图 1.1 所示。
1.2 关闭 Windows 快速启动功能
打开 控制面板
->电源选项
-> 选择电源按钮功能
并关闭快速启动
功能,如图 1.2 所示。
1.3 允许网卡唤醒设备
打开 设备管理器
->网络适配器
找到有线网卡
并右键进入属性设置,如图 1.3 所示。
在电源管理
选项卡中勾选图 1.4 所示选项。
在高级
选项卡中开启有关网络唤醒的选项,关闭节能有关的选项,如图 1.5 所示。
1.4 网卡 MAC 地址和 IP 地址
在设置
-> 网络和 Internet
-> 以太网
中可查看有线网卡的 MAC 地址
和 IP 地址
以供后续使用。
2. 实现 WOL 功能
网络唤醒功能是通过局域网内的设备发送一个幻数据包 Magic Packet
到目标设备实现。幻数据包
是一个包含目标计算机 MAC 地址
的广播帧,其结构为 6 字节
的 0xff
开头,紧接着是目标计算机的 6 字节
的 MAC 地址
,重复 16 次
,最后是 可选
的 6 字节
密码,如下所示:
_________________________________________
| 字节1 | 字节2| 字节3 | 字节4 | 字节5 | 字节6|
_________________________________________
| 0xff | 0xff | 0xff | 0xff | 0xff | 0xff |
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
|MAC[0]|MAC[1]|MAC[2]|MAC[3]|MAC[4]|MAC[5]|
-----------------------------------------
可选的密码:
_________________________________________
|PASS0 |PASS1 |PASS2 |PASS3 |PASS4 |PASS5 |
-----------------------------------------
幻数据包
通常是使用 UDP
协议发送,发送 端口
并没有严格限制,通常是使用 端口 7
或 端口 9
,但 特权端口(0-1023)在某些网络环境(如企业防火墙、路由器ACL)可能默认阻止这些端口的 UDP
广播流量。
使用 Kotlin
实现一个唤醒设备应用,其核心的幻数据包的封装和发送代码如下所示。
/**
* 发送幻数据包唤醒局域网中的设备
* @param macAddress 目标设备的MAC地址(格式:"00:11:22:33:44:55")
* @param ipAddress 广播地址,通常是 "255.255.255.255" 或具体子网的广播地址
* @param port 发送端口,通常是 7 或 9,这里默认使用 4000
*/
suspend fun wakeOnLan(macAddress: String, ipAddress: String = "255.255.255.255", port: Int = 4000, onResult: ((Boolean) -> Unit)? = null) {
withContext (Dispatchers.IO) {
try {
// 6字节的目标 MAC 地址
val bytes = ByteArray(6)
val mac = macAddress.split(":")
for (i in 0 until 6) {
bytes[i] = mac[i].toInt(16).toByte()
}
// 构造幻数据包(Magic Packet)
// 1. 6 字节的 FF
val buffer = ByteArray(6 + 16 * 6)
for (i in 0 until 6) {
buffer[i] = 0xFF.toByte()
}
// 2. 重复目标 MAC 地址 16 次
for (i in 0 until 16) {
System.arraycopy(bytes, 0, buffer, 6 + i * 6, 6)
}
// 发送 UDP 数据包
val socket = DatagramSocket()
socket.setBroadcast(true)
val packet = DatagramPacket(
buffer,
buffer.size,
InetAddress.getByName(ipAddress),
port
)
socket.send(packet)
socket.close()
withContext(Dispatchers.Main) {
onResult?.invoke(true)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
onResult?.invoke(false)
}
}
}
}
为了使得应用更易于使用,可以使用快速访问设备控制器功能。下拉通知栏,点击设备控制器
其效果如图 2.1 所示。
创建服务并声明
<service
android:name=".WOLControlService"
android:label="Wake On Lan Controls"
android:permission="android.permission.BIND_CONTROLS"
android:exported="true">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
WOLControlService
完整代码如下:
/**
* 外部设备控制器
*/
class WOLControlService : ControlsProviderService() {
private val TAG = WOLControlService::class.java.simpleName
private var mGlobalJob: Job? = null
/**
* 添加控制器时系统会调用 createPublisherForAllAvailable 方法来获取可用的设备列表
* 使用 flowPublish 需要导入 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
*/
override fun createPublisherForAllAvailable(): Flow.Publisher<Control> = flowPublish {
createWolDeviceControls().forEach { send(it) }
}
/**
* 返回给定 controlId 的有效 Publisher
* 添加控制器后系统会调用 createPublisherFor 方法来创建指定设备的控制器
*/
override fun createPublisherFor(controlIds: List<String?>): Flow.Publisher<Control> = flowPublish {
controlIds.forEach {
it?.takeIf { it.isNotBlank() }?.let { controlId ->
try {
val devices = SharedPreferencesUtils.getWOLDeviceById(this@WOLControlService, controlId.toInt())
createControlByDevice(devices)?.let { send(it) }
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun performControlAction(controlId: String, action: ControlAction, consumer: Consumer<Int?>) {
SharedPreferencesUtils.getWOLDeviceById(this@WOLControlService, controlId.toInt())?.let {
mGlobalJob = GlobalScope.launch {
WOLUtils.wakeOnLan(it.macAddress, it.ipAddress, it.port) { result ->
if (result) {
Log.e(TAG, "唤醒成功: ${it.deviceName}")
}
}
consumer.accept(ControlAction.RESPONSE_OK)
}
} ?: run {
consumer.accept(ControlAction.RESPONSE_FAIL)
}
}
/**
* 创建设备管理列表, 用于告知系统可用的设备列表
*/
private fun createWolDeviceControls(): List<Control> {
val controls = mutableListOf<Control>()
val devices = SharedPreferencesUtils.getWOLDevices(this)
for (device in devices) {
createControlByDevice(device)?.let { controls.add(it) }
}
return controls
}
private fun createControlByDevice(device: WolDevices?): Control? {
return if (device == null) null else Control.StatefulBuilder(
device.id.toString(),
PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
).setTitle(device.deviceName)
.setSubtitle(device.macAddress)
.setDeviceType(DeviceTypes.TYPE_TV)
.setStatus(Control.STATUS_OK)
.setControlTemplate(StatelessTemplate(device.id.toString()))
.build()
}
override fun onDestroy() {
super.onDestroy()
mGlobalJob?.cancel()
}
}
3. WOL 常见问题
3.1 刚关机时能唤醒设备,时间一长就无法唤醒设备
- 检查唤醒 IP 是否使用的是设备 IP,路由器 IP 地址表租约一般为12小时,超过12小时后租约到期则无法通过设备 IP 地址唤醒,可使用本地广播地址 255.255.255.255 尝试是否能唤醒。
- 检查网卡属性 -> 高级选项卡中的节能模式是否关闭,若未关闭节能模式可能导致网卡休眠,无法唤醒设备。