WiFi和熱點開發——tcp連線檢視實時日誌
阿新 • • 發佈:2019-01-31
本人從事多年的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重新整理。