网络唤醒,即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; } }