網絡喚醒,即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; } }