基于Java实现计算机远程唤醒(WOL)功能

      网络唤醒,即WOL。简单来讲就是电脑在关闭状态,可以通过网络发送特殊数据包给网卡,网卡收到指定包后,开启计算机。WOL要求有硬件支持该功能,目前市场上主流的以太网卡都支持WOL功能,而无线网卡查找了许多没找到支持该功能的无线网卡。

        我在家已经成功实现了网络唤醒功能,可如果我在公司需要操作家里电脑,而网络唤醒是基于局域网的,则无法办到。于是我想到了通过访问家里路由器的网络IP地址实现,但是家里的网络IP地址是变化的,每次重启路由器都会更换,自己总不能每次重启路由器都要记一遍网络IP地址吧。

        为了解决这个问题,我想到了花生壳。家里的路由是TP-LINK,集成了花生壳的DDNS功能,使用后发现小区宽带经过N重路由,花生壳获取到的网络IP地址根本不正确,纠结。几经波折发现可以为花生壳设置A记录,指定域名的IP,同时花生壳也提供了修改A记录的接口,于是我萌生了一个想法:每当路由器重启时,获取路由器网络IP地址,然后通过花生壳接口修改域名A记录,这样外网就可以通过域名发送网络唤醒包,实现远程开机,想法是多么的高大上啊,但过程很艰辛!


        废话太多了,先让我们了解一下花生壳的开放接口吧,我使用的是。

        地址:http://open.oray.com/wiki/doku.php

        看文档我们了解到,主要分DDNS协议和HTTP协议两种。DDNS协议需要保持心跳包,即每个一段时间发送相应数据给花生壳,显然不是我们想要的,所以我选择HTTP协议。

官网HTTP协议如下:

当客户端发现IP地址变化或是用户修改设置时,客户端应该进行更新。

所有的更新都基本于标准的HTTP请求发送。

服务器会传回一个返回代码,客户端需要解析。 

HTTP请求

主机名:ddns.oray.com

HTTP端口:80

HTTPS 端口:443

请求支持HTTP和基于SSL的HTTPS协议(HTTPS需要付费用户才能使用) 

所有客户端必须发送一个完整的User-Agent文件头,用于区分不同的设备,空值或非法参数将导致请求失败。

例子

1.使用URL验证 

适用于浏览器或应用程序(fetch, curl, lwp-request),可以在URL中包含验证信息。

http://username:[email protected]/ph/update?hostname=yourhostname&myip=ipaddress

2.原始HTTP GET请求 

实际的HTTP请求,类似下面的代码。 

其中 base-64-authorization 请使用 Base64 加密 username:password 后的字符替换。

GET /ph/update?hostname=yourhostname&myip=ipaddress HTTP/1.0

Host: ddns.oray.com

Authorization: Basic base-64-authorization

User-Agent: Oray

请注意必须使用GET请求,POST是不被允许的。

更新参数

目前仅允许提交以下参数

参数说明

hostname需要更新的域名,此域名必须是开通花生壳服务。多个域名使用,分隔,默认为空,则更新护照下所有激活的域名。例:hostname=test.oray.com,customtest.oray.com

myip需要更新的IP地址,可以不填。如果不指定,则由服务器获取到的IP地址为准。


起初看这个文档我是云里雾里的,经过几番折腾终于弄明白了。

  1.以GET方式发送HTTP请求到http://ddns.oray.com/ph/update?hostname=yourhostname&myip=ipaddress

  2.hostname值为要修改的域名

  3.myip值为要修改的IP地址,即家中路由器的公网IP地址

  4.请求头User-Agent为浏览器型号

  5.请求头Authorization为 花生壳登录名:密码 用BASE64方式加密内容

    比如账号为:1234;密码为12345,即1234:12345的BASE64加密结果为:MTIzNDoxMjM0NQ==

后面的问题就是如何获取公网IP了,发现http://www.ip138.com/这个网站获取的公网IP地址和路由器是一致的,再看http://www.ip138.com/其实是通过http://1111.ip138.com/ic.as接口GET方式获取的,那么只需要读取http://1111.ip138.com/ic.asp的IP地址就可以了。

package smile.heyi.html;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;

/**
 * 获取公网IP地址
 * */
public class GetInternetIP {
	private final static String url = "http://1111.ip138.com/ic.asp";
	
	public static String getIP(){
		String ip = "";
		String html = getHTML();
		int start = html.indexOf("[")+1;
		int end = html.indexOf("]");
		//截取IP地址字符串
		ip = html.substring(start, end);
		return ip;
	}
	
	private static String getHTML(){
		String s = "";
		try {
			HttpURLConnection conn = (HttpURLConnection) (new URL(url).openConnection());
			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:35.0) Gecko/20100101 Firefox/35.0");
			conn.setRequestMethod("GET");
			
			BufferedReader bf = new BufferedReader(new InputStreamReader(conn.getInputStream(),"GBK"));
			String tmp = "";
			
			while((tmp = bf.readLine()) != null){
				s += tmp+"\r\n";
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return s;
	}
}

下面在调用花生壳的接口

package smile.heyi.html;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import smile.heyi.util.Base64;

/**
 * 通过花生壳接口,修改域名A记录
 * */
public class ChangeDDNS {
	private String url = "http://ddns.oray.com/ph/update";
	private String loginName = "";
	private String password = "";
	
	public ChangeDDNS(String name, String pw){
		this.loginName = name;
		this.password = pw;
	}
	
	public String change(String domainName, String ip){
		String s = "";
		
		url += "?hostname="+domainName+"&myip="+ip;
		try {
			HttpURLConnection conn = (HttpURLConnection) (new URL(url).openConnection());
			conn.setRequestProperty("Host", "ddns.oray.com");
			//模拟以FireFox浏览器身份访问
			conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:35.0) Gecko/20100101 Firefox/35.0");
			//以 账号:密码 的BASE64加密结果身份登录
			conn.setRequestProperty("Authorization", "Basic "+Base64.getBASE64(loginName+":"+password));
			conn.setRequestProperty("Referrer", url);
			
			BufferedReader bf = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			String tmp = "";
			while((tmp = bf.readLine()) != null){
				s += tmp;
			}
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return s;
	}
	
}

剩下的就是BASE64加密登录信息

说明一下:网上看了一下算法,太复杂没懂,就从网上抄了一份。这份使用的sun.misc.BASE64Decoder和sun.misc.BASE64Encoder两个包,但是Eclipse会找不到,需要工程中删除JRE System Library然后重新添加才可以,究其原因貌似是因为这两个包是sun员工内部自己用的并没有对外发布(看名称就会觉得怪不是以java开头的),并不保证没问题,所以还是希望高手自己写吧。

package smile.heyi.util;

import java.io.IOException;
import sun.misc.BASE64Decoder;     
import sun.misc.BASE64Encoder;

/**
 * Base64加解密
 * */
public class Base64 {
	public static String getBASE64(String s){
		return (new BASE64Encoder()).encode(s.getBytes());
	}
	
	public static String deCodeBASE64(String key) throws IOException{
		byte[] b = (new BASE64Decoder()).decodeBuffer(key);
		return new String(b);
	}
}


最后的问题,就是怎么称才能在路由器重启的时候,执行这些操作了。

我们不能监测到路由器是否重启,但是可以监测到计算机网卡状态。每次路由器重启(开机状态下)网卡都会失去Internet连接然会回复Internet连接。那么我们可以做计划任务,每当网卡发生连接事件的时候,延迟10分钟左右执行以上的Java代码,延迟是为了防止网络连接未回复就去获取网络IP地址。

package smile.heyi.util;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

/**
 * 网络唤醒功能
 * */
public class WOL {
	private int port = 0;//端口号
	private String macAddress = ""; //MAC地址
	private String destIP = "";// 广播地址
	
	public WOL(String macAddress, String sendIP, int port){
		this.macAddress = macAddress;
		this.destIP = sendIP;
		this.port = port;
	}
	
	/**
	 * 发送开机指令
	 * */
	public boolean sendMagicPackage(){
		InetAddress destHost = null;
		try {
			destHost = InetAddress.getByName(destIP);
		} catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		//验证MAC地址并转换为二进制
		byte[] destMac = getMacBytes(macAddress);
		
		// 创建开机指令包
		byte[] magic = new byte[102];
		// 将数据包的前6位放入0xFF即 "FF"的二进制
		for (int i = 0; i < 6; i++)
			magic[i] = (byte) 0xFF;
        // 从第7个位置开始把MAC地址放入16次
		for (int i = 0; i < 16; i++) {
			for (int j = 0; j < destMac.length; j++) {
				magic[6 + destMac.length * i + j] = destMac[j];
			}
		}

		DatagramPacket dp = null;
		dp = new DatagramPacket(magic, magic.length, destHost, port);
		DatagramSocket ds;
		try {
			ds = new DatagramSocket();
			ds.send(dp);
	        ds.close();
		} catch (SocketException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
        
		
		return true;
	}

	/**
	 * 验证MAC地址并转换为二进制
	 * */
	private static byte[] getMacBytes(String macStr) throws IllegalArgumentException {
		byte[] bytes = new byte[6];
		String[] hex = macStr.split("(\\:|\\-)");
		if (hex.length != 6) {
			throw new IllegalArgumentException("无效的MAC地址");
		}
		try {
			for (int i = 0; i < 6; i++) {
				bytes[i] = (byte) Integer.parseInt(hex[i], 16);
			}
		} catch (NumberFormatException e) {
			throw new IllegalArgumentException("无效的MAC地址");
		}
		return bytes;
	}
}


你可能感兴趣的:(java,wol,网络唤醒)