Java 使用Socket正確讀取數據姿勢
前言
平時日常開發用得最多是Http通訊,接口調試也比較簡單的,也有比較強大的框架支持(OkHttp)。
個人平時用到socket通訊的地方是Android與外設通訊,Android與ssl服務通訊,這種都是基於TCP/IP通訊,而且服務端和設備端協議都是不能修改的,隻能按照相關報文格式進行通信。
但使用socket通訊問題不少,一般有兩個難點:
1、socket通訊層要自己寫及IO流不正確使用,遇到讀取不到數據或者阻塞卡死現象或者數據讀取不完整
2、請求和響應報文格式多變(json,xml,其它),解析麻煩,如果是前面兩種格式都簡單,有對應框架處理,其它格式一般都需要自己手動處理。
本次基於第1點問題做瞭總結,歸根結底是使用read()或readLine()導致的問題
Socket使用流程
1、創建socket
2、連接socket
3、獲取輸入輸出流
字節流:
InputStream mInputStream = mSocket.getInputStream(); OutputStream mOutputStream = mSocket.getOutputStream();
字符流:
BufferedReader mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream(), "UTF-8")); PrintWriter mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream(), "UTF-8")), true);
至於實際使用字節流還是字符流,看實際情況使用。如果返回是字符串及讀寫與報文結束符(/r或/n或/r/n)有關,使用字符流讀取,否則字節流。
4、讀寫數據
5、關閉socket
如果是Socket短連接,上面五個步驟都要走一遍;
如果是Socket長連接,隻需關註第4點即可,第4點使用不慎就會遇到上面出現的問題。
實際開發中,長連接使用居多,一次連接,進行多次收發數據。
特別註意:使用長連接不能讀完數據後立馬關閉輸入輸出流,必須再最後不使用的時候關閉
Socket數據讀寫
當socket阻塞時,必須設置讀取超時時間,防止調試時,socket讀取數據長期掛起。
mSocket.setSoTimeout(10* 1000); //設置客戶端讀取服務器數據超時時間
使用read()讀取阻塞問題
日常寫法1:
mOutputStream.write(bytes); mOutputStream.flush(); byte[] buffer = new byte[1024]; int n = 0; ByteArrayOutputStream output = new ByteArrayOutputStream(); while (-1 != (n = mInputStream .read(buffer))) { output.write(buffer, 0, n); } //處理數據 output.close(); byte[] result = output.toByteArray();
上面看似沒有什麼問題,但有時候會出現mInputStream .read(buffer)阻塞,導致while循環體裡面不會執行
日常寫法2:
mOutputStream.write(bytes); mOutputStream.flush(); int available = mInputStream.available(); byte[] buffer = new byte[available]; in.read(buffer);
上面雖然不阻塞,但不一定能讀取到數據,available 可能為0,由於是網絡通訊,發送數據後不一定馬上返回。
或者對mInputStream.available()修改為:
int available = 0; while (available == 0) { available = mInputStream.available(); }
上面雖然能讀取到數據,但數據不一定完整。
而且,available方法返回估計的當前流可用長度,不是當前通訊流的總長度,而且是估計值;read方法讀取流中數據到buffer中,但讀取長度為1至buffer.length,若流結束或遇到異常則返回-1。
最終寫法(遞歸讀取):
/** * 遞歸讀取流 * * @param output * @param inStream * @return * @throws Exception */ public void readStreamWithRecursion(ByteArrayOutputStream output, InputStream inStream) throws Exception { long start = System.currentTimeMillis(); while (inStream.available() == 0) { if ((System.currentTimeMillis() - start) > 20* 1000) {//超時退出 throw new SocketTimeoutException("超時讀取"); } } byte[] buffer = new byte[2048]; int read = inStream.read(buffer); output.write(buffer, 0, read); SystemClock.sleep(100);//需要延時以下,不然還是有概率漏讀 int a = inStream.available();//再判斷一下,是否有可用字節數或者根據實際情況驗證報文完整性 if (a > 0) { LogUtils.w("========還有剩餘:" + a + "個字節數據沒讀"); readStreamWithRecursion(output, inStream); } } /** * 讀取字節 * * @param inStream * @return * @throws Exception */ private byte[] readStream(InputStream inStream) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); readStreamWithRecursion(output, inStream); output.close(); int size = output.size(); LogUtils.i("本次讀取字節總數:" + size); return output.toByteArray(); }
上面這種方法讀取完成一次後,固定等待時間,等待完不一定有數據,若沒有有數據,響應時間過長,會影響用戶體驗。我們可以再優化一下:
/** * 遞歸讀取流 * * @param output * @param inStream * @return * @throws Exception */ public void readStreamWithRecursion(ByteArrayOutputStream output, InputStream inStream) throws Exception { long start = System.currentTimeMillis(); int time =500;//毫秒,間看實際情況 while (inStream.available() == 0) { if ((System.currentTimeMillis() - start) >time) {//超時退出 throw new SocketTimeoutException("超時讀取"); } } byte[] buffer = new byte[2048]; int read = inStream.read(buffer); output.write(buffer, 0, read); int wait = readWait(); long startWait = System.currentTimeMillis(); boolean checkExist = false; while (System.currentTimeMillis() - startWait <= wait) { int a = inStream.available(); if (a > 0) { checkExist = true; // LogUtils.w("========還有剩餘:" + a + "個字節數據沒讀"); break; } } if (checkExist) { if (!checkMessage(buffer, read)) { readStreamWithRecursion(output, inStream, timeout); } } } /** * 讀取等待時間,單位毫秒 */ protected int readWait() { return 100; } /** * 讀取字節 * * @param inStream * @return * @throws Exception */ private byte[] readStream(InputStream inStream) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); readStreamWithRecursion(output, inStream); output.close(); int size = output.size(); LogUtils.i("本次讀取字節總數:" + size); return output.toByteArray(); }
上面這種延遲率大幅降低,目前正在使用該方法讀取,再也沒有出現數據讀取不完整和阻塞現象。不過這種,讀取也要註意報文結束符問題,何時讀取完畢問題。
使用readreadLine()讀取阻塞問題
日常寫法:
mPrintWriter.print(sendData+ "\r\n"); mPrintWriter.flush(); String msg = mBufferedReader.readLine(); //處理數據
細心的你發現,發送數據時添加瞭結束符,如果不加結束符,導致readLine()阻塞,讀不到任何數據,最終拋出SocketTimeoutException異常
特別註意:
報文結束符:根據實際服務器規定的來添加,必要時問後端開發人員或者看接口文檔是否有說明
不然在接口調試上會浪費很多寶貴的時間,影響後期功能開發。
使用readLine()註意事項:
- 1、讀入的數據要註意有/r或/n或/r/n
這句話意思是服務端寫完數據後,會打印報文結束符/r或/n或/r/n;
同理,客戶端寫數據時也要打印報文結束符,這樣服務端才能讀取到數據。
- 2、沒有數據時會阻塞,在數據流異常或斷開時才會返回null
- 3、使用socket之類的數據流時,要避免使用readLine(),以免為瞭等待一個換行/回車符而一直阻塞
上面長連接是發送一次數據和讀一次數據,保證瞭當次通訊的完整性,必須要時需要同步處理。
也有長連接,客戶端開線程循環阻塞等待服務端數據發送數據過來,比如:消息推送。平時使用長連接都是分別使用不同的命令發送數據且接收數據,來完成不同的任務。
總結
實際開發中,長連接比較復雜,還要考慮心跳,丟包,斷開重連等問題。使用長連接時,要特別註意報文結束符問題,結束符隻是用來告訴客戶端或服務端數據已經發送完畢,客戶端或服務端可以讀取數據瞭,否則客戶端或服務端會一直阻塞在read()或者readLine()方法。
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java流形式返回前端的實現示例
- Java網絡編程TCP實現文件上傳功能
- Java基礎知識之ByteArrayOutputStream流的使用
- Java實現InputStream的任意拷貝方式
- Java網絡編程之入門篇