全部產品
Search
文件中心

IoT Platform:遠端控制樹莓派伺服器

更新時間:Jun 30, 2024

使用阿里雲物聯網平台可實現偽內網穿透,對無公網IP的樹莓派伺服器進行遠端控制。本文以實現基於樹莓派伺服器遠端控制為例,介紹偽內網穿透的實現流程,並提供開發程式碼範例。

背景資訊

假如您在公司或家裡使用樹莓派搭建一個伺服器,用於執行一些簡單的任務,如啟動某個指令碼,開始下載檔案等。但是,如果樹莓派沒有公網IP,您不在公司或家裡的情況下,您就無法控制該伺服器。如果使用其他內網穿透工具,也會經常出現斷線的情況。為解決以上問題,您可以使用阿里雲物聯網平台的RRPC(同步遠端程序呼叫)功能結合JSch庫來實現對樹莓派伺服器的遠端控制。

說明 本文以公用執行個體下產品和裝置為例,介紹遠端控制樹莓派伺服器的開發方法。

實現遠端控制的流程

樹莓派串連物聯網平台

通過物聯網平台遠端控制樹莓派伺服器的流程:

  1. 在電腦上調用物聯網平台RRPC介面發送SSH指令。
  2. 物聯網平台接收到指令後,通過MQTT協議將SSH指令下發給樹莓派伺服器。
  3. 伺服器執行SSH指令。
  4. 伺服器將SSH指令執行結果封裝成RRPC響應,通過MQTT協議上報到物聯網平台。
  5. 物聯網平台將RRPC響應回複給電腦。
說明 RRPC調用的逾時限制為5秒。伺服器5秒內未收到裝置回複,會返回逾時錯誤。如果您發送的指令操作耗時較長,可忽略該逾時錯誤資訊。

下載SDK和Demo

實現物聯網平台遠端控制樹莓派,您需先進行服務端SDK和裝置端SDK開發。

以下章節中,將介紹服務端SDK和裝置端SDK的開發樣本。

說明 本文樣本中提供的代碼僅支援一些簡單的Linux命令,如uname、touch、mv等,不支援檔案編輯等複雜的指令,需要您自行實現。

裝置端SDK開發

下載、安裝裝置端SDK和下載SDK Demo後,您需添加專案依賴和增加以下Java檔案。

專案可以匯出成JAR包在樹莓派上運行。

  1. 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>
  2. 增加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;
        }
    
    }
  3. 增加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) {
        }
    
    }
  4. 增加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();
        }
    
    }
  5. 增加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檔案。

  1. 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>
  2. 增加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;
        }
    
    }
  3. 增加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());
    
            }
    
        }
    
    
    }