- 起因
最近剛剛結束考研,開始有時間寫文章了。在複習的時候中,經常忍不住折騰各種東西,於是有一天看中了我手上的華為路由器。什麼?華為路由器,你可能有這樣的疑問,華為路由器不是自研的晶片嗎,就像我手上這臺華為路由器,是華為自己研發的凌霄晶片,沒有對外開放,怎麼折騰呢?於是就有了以下的研究歷程。
- 折騰什麼
首先,能夠折騰什麼呢?就像我手上的樹莓派一樣,刷個OpenWrt系統輕而易舉。可能有些人會有一些疑問,OpenWrt系統是什麼?其實這就是一個開源的路由器作業系統,很多路由器的系統都是在此基礎上進行開發的,這個系統的可玩性很高。但是華為路由器連韌體下載都沒有開放,折騰系統這條路就不太合適了。開發路由器外掛呢?貌似可行,但此時我只知道路由器外掛只能在華為路由器專用的市場上安裝,而且路由器買了幾年了,也就那麼幾個外掛,主要都是IOT家電控制類的應用,但是這條路理論上可行,於是決定折騰路由器外掛開發。
- 申請Debug版本韌體
目前華為路由器只要是有外掛應用市場的,理論上都支援路由器外掛開發,其它品牌的路由器很多也是支援的,不過每種路由器開發的方式都不一樣,可以參考官方提供的文件。目前我手上只有華為的路由器,型號是榮耀路由Pro2,這是幾年前的一個路由器,已經都下市了,韌體也不更新了,通過華為官網的文件,我傳送路由器序列號給華為聯絡郵箱,等待路由器適配完成,更新一下韌體,就轉到了Debug版本。
- 瞭解外掛系統
華為路由器執行了OpenEE開發平臺,外掛就是在此基礎上進行開發,同時路由器硬體通過OSGI介面對外提供呼叫能力,外掛執行在JVM上。JVM?沒錯,就是我們Java程式設計師喜歡的JVM。Debug版本可以直接用root使用者登入到路由器執行的後臺,基本Linux的命令都是支援的。然後我找到了路由器上的JVM研究了一下,其實就是研究了一下rt.jar的原始碼,這個JVM是極度精簡的版本,很多和路由器執行無關類都去掉了,並且加了很多華為自己寫的類,不過我們編寫程式最常用的類還是沒有精簡的。
外掛開發分為前端和後端,後端可以基於JVM開發API介面供前端呼叫,前端可以直接使用HTML等任意前端技術進行開發,不過需要呼叫後端的API只能使用特定的函式,最後上傳開發好的應用到路由器即可執行,同時應用也可以在路由器市場直接開啟執行、解除安裝。
- 跑通Demo
可以根據官方文件進行操作,在這裡我就不貼出連結了,大家如果有開發的需求,可以直接在華為開發者官網去搜尋路由器開發文件即可,也可以和我討論。首先,需要準備開發環境,JDK1.8、Maven基本就夠了,然後執行官方指令碼向Maven本地庫匯入幾個華為自己的Jar包即可。
Demo專案是Maven型別的專案,熟悉Java開發的應該很熟悉了,可以用自己喜歡的軟體進行開發,比如我就喜歡使用idea進行開發。執行mvn install,就生成好了對應的Jar包,然後通過官方提供的指令碼打包成Apk檔案,沒錯,就是Apk檔案,不過不是安卓上的Apk,而是華為路由器對應的Apk檔案,然後官方還提供了上傳應用的工具,直接上傳即可。
就這樣,一個Hello Word應用就跑到路由器上了。只不過官方提供的Demo專案沒有前端,只能在後臺控制檯上檢視對應的輸出。如果需要開發前端,需要將對應的前端檔案上傳到公網伺服器上,通過IP進行呼叫。
- 實現路由器上跑Web伺服器
Demo應用跑通了,接下來準備做些什麼了。既然路由器執行著JVM,那麼跑Web應用應該是沒什麼問題的,而且我這個路由器還有512M的記憶體,低負載的Web應用應該沒有問題。這個基礎上,我們能夠做我們想做的任何事情,比如做個NAS伺服器,當內部部落格伺服器等等,當然如果你有公網條件,也可當小型部落格伺服器使用,這裡只討論內網應用。JDK1.8本來內建一個簡單的HttpServer類,可惜路由器JVM把這個類精簡了,於是我編寫了以下的類檔案。
package ml.minli.tool.util;
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.*;
public class HttpServer extends Thread {
private final int port;
private ServerSocket serverSocket;
private static final MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
public HttpServer(int port) {
this.port = port;
}
@Override
public void run() {
try {
serverSocket = new ServerSocket(port);
while (true) {
Socket socket = serverSocket.accept();
HttpRequestHandler httpRequestHandler = new HttpRequestHandler(socket);
httpRequestHandler.handle();
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (serverSocket != null && !serverSocket.isClosed()) {
try {
serverSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static class HttpRequestHandler {
private final Socket socket;
public HttpRequestHandler(Socket socket) {
this.socket = socket;
}
public void handle() {
try {
StringBuilder stringBuilder = new StringBuilder();
InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
char[] chars = new char[1024];
int mark;
while ((mark = inputStreamReader.read(chars)) != -1) {
stringBuilder.append(chars, 0, mark);
if (mark < chars.length) {
break;
}
}
if (stringBuilder.length() == 0) {
return;
}
//擷取每行請求
String[] lines = stringBuilder.toString().split("\r\n");
if (!lines[0].isEmpty()) {
//擷取URL
String[] infos = lines[0].split(" ");
String info = URLDecoder.decode(infos[1], "UTF-8");
File file;
if (info.equals("/")) {
file = new File(USBInfo.usbPath + "/index.html");
} else {
file = new File(USBInfo.usbPath + info);
}
//檔案不存在返回404
if (!file.exists()) {
socket.getOutputStream().write(("HTTP/1.1 404 Not Found\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"\r\n").getBytes());
return;
}
String contentType = mimetypesFileTypeMap.getContentType(file);
socket.getOutputStream().write(("HTTP/1.1 200 OK\r\n" +
"Content-Type: " + contentType + "; charset=utf-8\r\n" +
"\r\n").getBytes());
FileInputStream fileInputStream = new FileInputStream(file);
byte[] bytes = new byte[1024];
int length;
while ((length = fileInputStream.read(bytes)) != -1) {
socket.getOutputStream().write(bytes, 0, length);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null && !socket.isClosed()) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
由於我只準備先實現靜態網頁伺服器的解析,於是準備這樣實現:U盤裡面存放前端檔案,比如HTML、CSS、JS等,然後伺服器解析檔案返回,所以這就簡單多了。只需要拿到請求進行解析就行了,不過返回需要合適的Content-Type,這個就需要對檔案型別進行判斷了,於是用到了javax.activation這個包,本來這個包也是JDK1.8自帶的,可惜,路由器JVM裡面精簡了。不過可以通過Maven外掛將檔案打包進去。
<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>5.1.2</version>
<extensions>true</extensions>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
<instructions>
<bundleName>{project.name}</bundleName>
<bundleDescription>{project.description}</bundleDescription>
<bundleVendor>minli</bundleVendor>
<Bundle-Activator>ml.minli.tool.Activator</Bundle-Activator>
<Service-Component>OSGI-INF/USBInfo.xml</Service-Component>
<Embed-Dependency>*;scope=compile|runtime;inline=false</Embed-Dependency>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
最後,在啟動的例項類裡面呼叫即可。這裡使用22222埠進行測試,值得注意的是,一些埠被路由器本身佔用了,所以我們只能使用其它埠。
package ml.minli.tool;
import ml.minli.tool.util.HttpServer;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
public Activator() {
}
@Override
public void start(BundleContext bundleContext) {
new HttpServer(22222).start();
}
@Override
public void stop(BundleContext bundleContext) {
}
}
同時,我們還要注意許可權的問題,很多地方,沒有許可權程式是會拋異常的。許可權配置如下所示。
(org.osgi.framework.PackagePermission "*" "import")
(java.util.logging.LoggingPermission "control")
(org.osgi.framework.ServicePermission "com.huawei.hilink.rest.RESTResource" "register")
(java.io.FilePermission "/mnt/-" "read")
(com.huawei.hilink.coreapi.perm.USBPermission "*" "list")
(org.osgi.framework.ServicePermission "com.huawei.hilink.openapi.usbstorage.USBStorage" "get")
(java.net.SocketPermission "*" "accept,connect,listen,resolve")
(java.util.PropertyPermission "*" "read")
最終,成功實現了路由器跑Web應用的功能,可以執行任意的網頁,同時如果是一些普通的檔案,通過URL訪問相當於是下載,所以做個簡單的NAS伺服器好像很容易。
- 後續
折騰到這樣,當時就暫時告一段落了,因為那個時候還在準備考研,到現在考研結束才整理寫下了這些內容,不過現在我又可以折騰了,看看有什麼應用可以做的,如果有進展,我會繼續分享的。