1. 程式人生 > >WiFi和熱點開發——tcp連線檢視實時日誌

WiFi和熱點開發——tcp連線檢視實時日誌

本人從事多年的Android智慧裝置開發,做過手機、MiFi、智慧門鎖等產品,除了手機之外,其他的產品在後期的維護及版本迭代過程中,經常需要通過分析檢視裝置的日誌來定位問題,比如智慧門鎖,遇到故障時,經常就抱著一臺筆記本,開啟裝置的usb除錯開關後,用usb線連線裝置進行問題重現和日誌分析。當然,獲取日誌的方式是多種多樣的,也可以通過網路上傳。但總免不了需要現場檢視,現場檢視就有個問題,筆記本並不是隨身攜帶,而手機確實隨身攜帶的,因此,能不能在手機端實時檢視裝置的日誌呢?答案是肯定的。
MiFi和智慧門鎖都開放了熱點,因此,除錯的手機可以連線到裝置的熱點,進行組網後實現實時日誌檢視。這種方式既不需要資料線,也不需要筆記本,甚至usb除錯開關也不用開啟。
先說服務端(裝置端),注意這裡用到了執行緒池,因此服務端是具備了一對多進行響應的基礎能力的:

    /**
     * @作用 開啟tcp服務
     */
    public void startTCP() {
        if (runnableTcp != null) {
            isStart = false;
            runnableTcp = null;
        }
        try {
            runnableTcp = new TcpReceive();
            threadTcp = new Thread(runnableTcp);
            isStart = true
; threadTcp.start(); } catch (Exception e) { logd("開啟TCP失敗:" + e.getMessage()); } try { mExe = Executors.newCachedThreadPool();// 建立一個執行緒池 } catch (Exception e) { logd("建立執行緒池失敗:" + e); } } private class TcpReceive
implements Runnable {
Socket socket = null; ServerSocket server = null; public void run() { try { //server = new ServerSocket(53858); if (server == null) { server = new ServerSocket(); server.setReuseAddress(true); server.bind(new InetSocketAddress(53858)); } allSockets = new HashSet<Socket>(); while (isRunning) { if (!server.isClosed()) { //logd("serverSocket "+server.hashCode()+" 監聽53858埠中..."); socket = server.accept(); } if (socket != null) { allSockets.add(socket); //logd("新的socket " + socket.hashCode() + " 加入,當前socket總數為:" + allSockets.size()); mExe.execute(new EchoThread(socket)); } } } catch (IOException e) { logd("serverSocket建立異常"+e); } } } public class EchoThread extends Thread { Socket socket; PrintWriter out; BufferedReader in; public EchoThread(Socket _socket){ socket = _socket; try { out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); } catch (IOException e) { logd("PrintWriter或BufferedReader異常"+e); } } public void run() { try { while (isStart) { if (socket == null || socket.isClosed() || !socket.isBound() || !socket.isConnected()) { break; } String line = in.readLine(); String ip = socket.getInetAddress().toString(); if (ip.contains("/")) { ip = ip.substring(1, ip.length()); } if (line == null){ logd("line請求為空"); break; } String temp[] = line.split("/"); if(temp == null || temp.length <2 ||!temp[0].equals(TcpConfig._HEAD_)){ return; }else{ line = temp[1]; } if (!line.equals(TcpConfig.GET_DEVICE_INFO) && !line.contains(TcpConfig.RESTART_TEST_GET_RES)){ logd("Tcp收到app請求訊息為:" + line + ";app對應手機的ip為:" + ip); } if (line.equals(TcpConfig.GET_REAL_TIME_LOGS)) { isRealTimeLogs = true; App.getInstance().setRealTimeLogQueque(mQueue); String logs = null; while (isRealTimeLogs) { try { logs = mQueue.take(); out.println(logs); Thread.sleep(20); } catch (Exception e) { e.printStackTrace(); logd(e); } } out.print("false,quit."); } if (!isCmds(line)) { out.println("Request Error"); } out.flush(); out.close(); socket.close(); allSockets.remove(socket); } } catch (IOException e) { e.printStackTrace(); logd("socket資訊互動異常:" + e); } finally { try { if (in != null) { // logd("BufferedReader關閉,hashCode:" + in.hashCode()); in.close(); } if (out != null) { // logd("PrintWriter關閉,hashCode:" + out.hashCode()); out.close(); } if (socket != null) { // logd("socket "+socket.hashCode()+" 關閉"); socket.close(); allSockets.remove(socket); } } catch (IOException e) { e.printStackTrace(); } } } }

程式碼中已經略去了其他的裝置控制介面,注意實時日誌檢視的介面和退出實時檢視的介面:

if (line.equals(TcpConfig.GET_REAL_TIME_LOGS)) {
    isRealTimeLogs = true;
    App.getInstance().setRealTimeLogQueque(mQueue);

    String logs = null;
    while (isRealTimeLogs) {
        try {
            logs = mQueue.take();
            out.println(logs);
            Thread.sleep(20);
        } catch (Exception e) {
            e.printStackTrace();
            logd(e);
        }
    }

    out.print("false,quit.");
}

if(line.equals(TcpConfig.CANCEL_REAL_TIME_LOGS)){
    isRealTimeLogs = false;
    mQueue.clear();
    App.getInstance().setRealTimeLogQueque(null);
    out.print(true);
}

其實就是從佇列裡面迴圈取日誌內容往對端傳送。
那麼日誌是在哪裡加入佇列的呢?我把記錄日誌的方法寫在了Application中,整個工程記錄日誌最終都會走到這個方法。那麼在記錄日誌的同時就加入到該佇列就可以了。

    /**
     * @作用 把傳遞訊息寫進Log日誌
     */
    public static void showTestInfo(final Object msg) {
        if (msg == null || msg.toString().isEmpty()) {
            return;
        }
        String appMsg = getDateToStringStyle("MM-dd HH:mm:ss,SSS", new Date()) + ":" + msg.toString();
        if (logger == null) {
            logger = Logger.getLogger(TGTConfig.TAG);
        }
        logger.warn(appMsg);

        if(mQueue != null){
            mQueue.add(appMsg);
        }
    }

這個佇列的在收到檢視實時日誌指令時初始化,在退出實時日誌檢視時置空(日誌不會加入佇列):

private static PriorityBlockingQueue<String> mQueue;

public void setRealTimeLogQueque(PriorityBlockingQueue queue){
        mQueue = queue;
}

服務端就是這樣,接下來看客戶端:

private void startRealTimeLog(){
        cmd = TcpConfig.GET_REAL_TIME_LOGS;
        new Thread(sendTcp).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                doloop();
            }
        }).start();
    }

    private void stopRealTimeLog(){
        cmd = TcpConfig.CANCEL_REAL_TIME_LOGS;
        new Thread(sendTcp).start();
    }

    private void doloop(){
        String res = null;
        while (isStart){
            try{
                res = mQueue.take();
                addIntoList(res);
                Thread.sleep(50);
            }catch (Exception e){
                e.printStackTrace();
                logd(e);
            }
        }
        logd("loop quit.");
    }

    private void addIntoList(String string){
        if (realTimeLogs.size() > MAX_LINE) {
            realTimeLogs.removeFirst();
        }
        realTimeLogs.add(string);
        uiHandler.removeMessages(MSG_REFRESH_UI);
        uiHandler.sendMessageDelayed(Message.obtain(uiHandler,MSG_REFRESH_UI,0,0,realTimeLogs),100);
    }

    Runnable sendTcp = new Runnable() {
        @Override
        public void run() {
            BufferedReader in = null;
            PrintWriter out = null;
            Socket socket = new Socket();
            try {
                socket.connect(new InetSocketAddress(TcpUtil.hostIP, TcpConfig.port), 10 * 1000);
                if (!cmd.isEmpty()) {
                    while (socket.isConnected() && !socket.isClosed()) {
                        try {
                            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
                            out.println(TcpConfig._HEAD_ + "/" + cmd);
                            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                            String line = null;
                            while ((line = in.readLine()) != null) {
                                logd(line);
                                mQueue.add(line);
                            }
                            break;
                        } catch (Exception e) {
                            e.printStackTrace();
                            logd(e);
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                logd(e);
            } finally {
                try {
                    if (in != null)
                        in.close();
                    if (socket != null)
                        socket.close();
                    if (out != null)
                        out.close();
                } catch (IOException e) {
                    logd(e);
                }
            }
        }
    };

其實你會發現,並麼有多複雜,一個執行緒阻塞地去讀取服務端返回的資料,另一個執行緒迴圈的從佇列讀取資料,併發送到UI執行緒進行UI重新整理。