基於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;
	}
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章