使用阿里雲物聯網平台可實現偽內網穿透,對無公網IP的樹莓派伺服器進行遠端控制。本文以實現基於樹莓派伺服器遠端控制為例,介紹偽內網穿透的實現流程,並提供開發程式碼範例。
背景資訊
假如您在公司或家裡使用樹莓派搭建一個伺服器,用於執行一些簡單的任務,如啟動某個指令碼,開始下載檔案等。但是,如果樹莓派沒有公網IP,您不在公司或家裡的情況下,您就無法控制該伺服器。如果使用其他內網穿透工具,也會經常出現斷線的情況。為解決以上問題,您可以使用阿里雲物聯網平台的RRPC(同步遠端程序呼叫)功能結合JSch庫來實現對樹莓派伺服器的遠端控制。
說明 本文以公用執行個體下產品和裝置為例,介紹遠端控制樹莓派伺服器的開發方法。
實現遠端控制的流程
通過物聯網平台遠端控制樹莓派伺服器的流程:
- 在電腦上調用物聯網平台RRPC介面發送SSH指令。
- 物聯網平台接收到指令後,通過MQTT協議將SSH指令下發給樹莓派伺服器。
- 伺服器執行SSH指令。
- 伺服器將SSH指令執行結果封裝成RRPC響應,通過MQTT協議上報到物聯網平台。
- 物聯網平台將RRPC響應回複給電腦。
說明 RRPC調用的逾時限制為5秒。伺服器5秒內未收到裝置回複,會返回逾時錯誤。如果您發送的指令操作耗時較長,可忽略該逾時錯誤資訊。
下載SDK和Demo
實現物聯網平台遠端控制樹莓派,您需先進行服務端SDK和裝置端SDK開發。
- 在電腦上安裝物聯網平台服務端SDK。您可以使用服務端Java SDK Demo來進行服務端開發。
- 在樹莓派上安裝物聯網平台裝置端SDK。您可以使用裝置端Java SDK Demo來進行裝置端開發。
以下章節中,將介紹服務端SDK和裝置端SDK的開發樣本。
說明 本文樣本中提供的代碼僅支援一些簡單的Linux命令,如uname、touch、mv等,不支援檔案編輯等複雜的指令,需要您自行實現。
裝置端SDK開發
下載、安裝裝置端SDK和下載SDK Demo後,您需添加專案依賴和增加以下Java檔案。
專案可以匯出成JAR包在樹莓派上運行。
- 在pom.xml檔案中,添加依賴。
<!-- 裝置端SDK --> <dependency> <groupId>com.aliyun.alink.linksdk</groupId> <artifactId>iot-linkkit-java</artifactId> <version>1.1.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> <scope>compile</scope> </dependency> <!-- SSH用戶端 --> <!-- https://mvnrepository.com/artifact/com.jcraft/jsch --> <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version> </dependency>
- 增加SSHShell.java檔案,用於執行SSH指令。
public class SSHShell { private String host; private String username; private String password; private int port; private Vector<String> stdout; public SSHShell(final String ipAddress, final String username, final String password, final int port) { this.host = ipAddress; this.username = username; this.password = password; this.port = port; this.stdout = new Vector<String>(); } public int execute(final String command) { System.out.println("ssh command: " + command); int returnCode = 0; JSch jsch = new JSch(); SSHUserInfo userInfo = new SSHUserInfo(); try { Session session = jsch.getSession(username, host, port); session.setPassword(password); session.setUserInfo(userInfo); session.connect(); Channel channel = session.openChannel("exec"); ((ChannelExec) channel).setCommand(command); channel.setInputStream(null); BufferedReader input = new BufferedReader(new InputStreamReader(channel.getInputStream())); channel.connect(); String line = null; while ((line = input.readLine()) != null) { stdout.add(line); } input.close(); if (channel.isClosed()) { returnCode = channel.getExitStatus(); } channel.disconnect(); session.disconnect(); } catch (JSchException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return returnCode; } public Vector<String> getStdout() { return stdout; } }
- 增加SSHUserInfo.java檔案,用於驗證SSH帳號密碼。
public class SSHUserInfo implements UserInfo { @Override public String getPassphrase() { return null; } @Override public String getPassword() { return null; } @Override public boolean promptPassphrase(final String arg0) { return false; } @Override public boolean promptPassword(final String arg0) { return false; } @Override public boolean promptYesNo(final String arg0) { if (arg0.contains("The authenticity of host")) { return true; } return false; } @Override public void showMessage(final String arg0) { } }
- 增加Device.java檔案,用於建立MQTT串連。
public class Device { /** * 建立串連 * * @param productKey 產品key * @param deviceName 裝置名稱 * @param deviceSecret 裝置密鑰 * @throws InterruptedException */ public static void connect(String productKey, String deviceName, String deviceSecret) throws InterruptedException { // 初始化參數 LinkKitInitParams params = new LinkKitInitParams(); // 設定 Mqtt 初始化參數 IoTMqttClientConfig config = new IoTMqttClientConfig(); config.productKey = productKey; config.deviceName = deviceName; config.deviceSecret = deviceSecret; params.mqttClientConfig = config; // 設定初始化裝置認證資訊,傳入: DeviceInfo deviceInfo = new DeviceInfo(); deviceInfo.productKey = productKey; deviceInfo.deviceName = deviceName; deviceInfo.deviceSecret = deviceSecret; params.deviceInfo = deviceInfo; // 初始化 LinkKit.getInstance().init(params, new ILinkKitConnectListener() { public void onError(AError aError) { System.out.println("init failed !! code=" + aError.getCode() + ",msg=" + aError.getMsg() + ",subCode=" + aError.getSubCode() + ",subMsg=" + aError.getSubMsg()); } public void onInitDone(InitResult initResult) { System.out.println("init success !!"); } }); // 確保初始化成功後才執行後面的步驟,可以根據實際情況適當延長這裡的延時 Thread.sleep(2000); } /** * 發布訊息 * * @param topic 發送訊息的topic * @param payload 發送的訊息內容 */ public static void publish(String topic, String payload) { MqttPublishRequest request = new MqttPublishRequest(); request.topic = topic; request.payloadObj = payload; request.qos = 0; LinkKit.getInstance().getMqttClient().publish(request, new IConnectSendListener() { @Override public void onResponse(ARequest aRequest, AResponse aResponse) { } @Override public void onFailure(ARequest aRequest, AError aError) { } }); } /** * 訂閱訊息 * * @param topic 訂閱訊息的topic */ public static void subscribe(String topic) { MqttSubscribeRequest request = new MqttSubscribeRequest(); request.topic = topic; request.isSubscribe = true; LinkKit.getInstance().getMqttClient().subscribe(request, new IConnectSubscribeListener() { @Override public void onSuccess() { } @Override public void onFailure(AError aError) { } }); } /** * 取消訂閱 * * @param topic 取消訂閱訊息的topic */ public static void unsubscribe(String topic) { MqttSubscribeRequest request = new MqttSubscribeRequest(); request.topic = topic; request.isSubscribe = false; LinkKit.getInstance().getMqttClient().unsubscribe(request, new IConnectUnscribeListener() { @Override public void onSuccess() { } @Override public void onFailure(AError aError) { } }); } /** * 中斷連線 */ public static void disconnect() { // 反初始化 LinkKit.getInstance().deinit(); } }
- 增加SSHDevice.java檔案。SSHDevice.java包含main方法,用於接收RRPC指令,調用
SSHShell
執行SSH指令,返回RRPC響應。SSHDevice.java檔案中,需要填寫裝置認證資訊(ProductKey、DeviceName和DeviceSecret)和SSH帳號密碼。public class SSHDevice { // ===================需要填寫的參數開始=========================== // 產品productKey private static String productKey = ""; // private static String deviceName = ""; // 裝置密鑰deviceSecret private static String deviceSecret = ""; // 訊息通訊的topic,無需建立和定義,直接使用即可 private static String rrpcTopic = "/sys/" + productKey + "/" + deviceName + "/rrpc/request/+"; // ssh 要訪問的網域名稱或IP private static String host = "127.0.0.1"; // ssh 使用者名稱 private static String username = ""; // ssh 密碼 private static String password = ""; // ssh 連接埠號碼 private static int port = 22; // ===================需要填寫的參數結束=========================== public static void main(String[] args) throws InterruptedException { // 下行資料監聽 registerNotifyListener(); // 建立串連 Device.connect(productKey, deviceName, deviceSecret); // 訂閱topic Device.subscribe(rrpcTopic); } public static void registerNotifyListener() { LinkKit.getInstance().registerOnNotifyListener(new IConnectNotifyListener() { @Override public boolean shouldHandle(String connectId, String topic) { // 只處理特定topic的訊息 if (topic.contains("/rrpc/request/")) { return true; } else { return false; } } @Override public void onNotify(String connectId, String topic, AMessage aMessage) { // 接收rrpc請求並回複rrpc響應 try { // 執行遠程命令 String payload = new String((byte[]) aMessage.getData(), "UTF-8"); SSHShell sshExecutor = new SSHShell(host, username, password, port); sshExecutor.execute(payload); // 擷取命令回顯 StringBuffer sb = new StringBuffer(); Vector<String> stdout = sshExecutor.getStdout(); for (String str : stdout) { sb.append(str); sb.append("\n"); } // 回複回顯到服務端 String response = topic.replace("/request/", "/response/"); Device.publish(response, sb.toString()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } @Override public void onConnectStateChange(String connectId, ConnectState connectState) { } }); } }
服務端SDK開發
下載、安裝服務端SDK和下載SDK Demo後,您需添加專案依賴和增加以下Java檔案。
- 在pom.xml檔案中,添加依賴。 重要 服務端SDK對應的新版本,請參見Java SDK使用說明。
<!-- 服務端SDK --> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-iot</artifactId> <version>7.38.0</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.6</version> </dependency> <!-- commons-codec --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.8</version> </dependency>
- 增加OpenApiClient.java檔案,用於調用物聯網平台開放介面。
public class OpenApiClient { private static DefaultAcsClient client = null; public static DefaultAcsClient getClient(String accessKeyID, String accessKeySecret) { if (client != null) { return client; } try { IClientProfile profile = DefaultProfile.getProfile("cn-shanghai", accessKeyID, accessKeySecret); DefaultProfile.addEndpoint("cn-shanghai", "cn-shanghai", "Iot", "iot.cn-shanghai.aliyuncs.com"); client = new DefaultAcsClient(profile); } catch (Exception e) { System.out.println("create OpenAPI Client failed !! exception:" + e.getMessage()); } return client; } }
- 增加SSHCommandSender.java檔案。SSHCommandSender.java包含main方法,用於發送SSH指令和接收SSH指令響應。SSHCommandSender.java中,需要填寫您的帳號AccessKey資訊、裝置認證資訊(ProductKey和DeviceName)、以及SSH指令。
public class SSHCommandSender { // ===================需要填寫的參數開始=========================== // 使用者帳號AccessKey private static String accessKeyID = ""; // 使用者帳號AccesseKeySecret private static String accessKeySecret = ""; // 產品Key private static String productKey = ""; // 裝置名稱deviceName private static String deviceName = ""; // ===================需要填寫的參數結束=========================== public static void main(String[] args) throws ServerException, ClientException, UnsupportedEncodingException { // Linux 遠程命令 String payload = "uname -a"; // 構建RRPC請求 RRpcRequest request = new RRpcRequest(); request.setProductKey(productKey); request.setDeviceName(deviceName); request.setRequestBase64Byte(Base64.encodeBase64String(payload.getBytes())); request.setTimeout(5000); // 擷取服務端請求用戶端 DefaultAcsClient client = OpenApiClient.getClient(accessKeyID, accessKeySecret); // 發起RRPC請求 RRpcResponse response = (RRpcResponse) client.getAcsResponse(request); // RRPC響應處理 // response.getSuccess()僅表明RRPC請求發送成功,不代表裝置接收成功和響應成功 // 需要根據RrpcCode來判定,參考文檔https://www.alibabacloud.com/help/doc-detail/69797.htm if (response != null && "SUCCESS".equals(response.getRrpcCode())) { // 回顯 System.out.println(new String(Base64.decodeBase64(response.getPayloadBase64Byte()), "UTF-8")); } else { // 回顯失敗,列印rrpc code System.out.println(response.getRrpcCode()); } } }