Java 使用Socket正確讀取數(shù)據(jù)姿勢(shì)
前言
平時(shí)日常開(kāi)發(fā)用得最多是Http通訊,接口調(diào)試也比較簡(jiǎn)單的,也有比較強(qiáng)大的框架支持(OkHttp)。
個(gè)人平時(shí)用到socket通訊的地方是Android與外設(shè)通訊,Android與ssl服務(wù)通訊,這種都是基于TCP/IP通訊,而且服務(wù)端和設(shè)備端協(xié)議都是不能修改的,只能按照相關(guān)報(bào)文格式進(jìn)行通信。
但使用socket通訊問(wèn)題不少,一般有兩個(gè)難點(diǎn):
1、socket通訊層要自己寫(xiě)及IO流不正確使用,遇到讀取不到數(shù)據(jù)或者阻塞卡死現(xiàn)象或者數(shù)據(jù)讀取不完整
2、請(qǐng)求和響應(yīng)報(bào)文格式多變(json,xml,其它),解析麻煩,如果是前面兩種格式都簡(jiǎn)單,有對(duì)應(yīng)框架處理,其它格式一般都需要自己手動(dòng)處理。
本次基于第1點(diǎn)問(wèn)題做了總結(jié),歸根結(jié)底是使用read()或readLine()導(dǎo)致的問(wèn)題
Socket使用流程
1、創(chuàng)建socket
2、連接socket
3、獲取輸入輸出流
字節(jié)流:
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);
至于實(shí)際使用字節(jié)流還是字符流,看實(shí)際情況使用。如果返回是字符串及讀寫(xiě)與報(bào)文結(jié)束符(/r或/n或/r/n)有關(guān),使用字符流讀取,否則字節(jié)流。
4、讀寫(xiě)數(shù)據(jù)
5、關(guān)閉socket
如果是Socket短連接,上面五個(gè)步驟都要走一遍;
如果是Socket長(zhǎng)連接,只需關(guān)注第4點(diǎn)即可,第4點(diǎn)使用不慎就會(huì)遇到上面出現(xiàn)的問(wèn)題。
實(shí)際開(kāi)發(fā)中,長(zhǎng)連接使用居多,一次連接,進(jìn)行多次收發(fā)數(shù)據(jù)。
特別注意:使用長(zhǎng)連接不能讀完數(shù)據(jù)后立馬關(guān)閉輸入輸出流,必須再最后不使用的時(shí)候關(guān)閉
Socket數(shù)據(jù)讀寫(xiě)
當(dāng)socket阻塞時(shí),必須設(shè)置讀取超時(shí)時(shí)間,防止調(diào)試時(shí),socket讀取數(shù)據(jù)長(zhǎng)期掛起。
mSocket.setSoTimeout(10* 1000); //設(shè)置客戶端讀取服務(wù)器數(shù)據(jù)超時(shí)時(shí)間
使用read()讀取阻塞問(wèn)題
日常寫(xiě)法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); } //處理數(shù)據(jù) output.close(); byte[] result = output.toByteArray();
上面看似沒(méi)有什么問(wèn)題,但有時(shí)候會(huì)出現(xiàn)mInputStream .read(buffer)阻塞,導(dǎo)致while循環(huán)體里面不會(huì)執(zhí)行
日常寫(xiě)法2:
mOutputStream.write(bytes); mOutputStream.flush(); int available = mInputStream.available(); byte[] buffer = new byte[available]; in.read(buffer);
上面雖然不阻塞,但不一定能讀取到數(shù)據(jù),available 可能為0,由于是網(wǎng)絡(luò)通訊,發(fā)送數(shù)據(jù)后不一定馬上返回。
或者對(duì)mInputStream.available()修改為:
int available = 0; while (available == 0) { available = mInputStream.available(); }
上面雖然能讀取到數(shù)據(jù),但數(shù)據(jù)不一定完整。
而且,available方法返回估計(jì)的當(dāng)前流可用長(zhǎng)度,不是當(dāng)前通訊流的總長(zhǎng)度,而且是估計(jì)值;read方法讀取流中數(shù)據(jù)到buffer中,但讀取長(zhǎng)度為1至buffer.length,若流結(jié)束或遇到異常則返回-1。
最終寫(xiě)法(遞歸讀?。?/strong>
/** * 遞歸讀取流 * * @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) {//超時(shí)退出 throw new SocketTimeoutException("超時(shí)讀取"); } } byte[] buffer = new byte[2048]; int read = inStream.read(buffer); output.write(buffer, 0, read); SystemClock.sleep(100);//需要延時(shí)以下,不然還是有概率漏讀 int a = inStream.available();//再判斷一下,是否有可用字節(jié)數(shù)或者根據(jù)實(shí)際情況驗(yàn)證報(bào)文完整性 if (a > 0) { LogUtils.w("========還有剩余:" + a + "個(gè)字節(jié)數(shù)據(jù)沒(méi)讀"); readStreamWithRecursion(output, inStream); } } /** * 讀取字節(jié) * * @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("本次讀取字節(jié)總數(shù):" + size); return output.toByteArray(); }
上面這種方法讀取完成一次后,固定等待時(shí)間,等待完不一定有數(shù)據(jù),若沒(méi)有有數(shù)據(jù),響應(yīng)時(shí)間過(guò)長(zhǎng),會(huì)影響用戶體驗(yàn)。我們可以再優(yōu)化一下:
/** * 遞歸讀取流 * * @param output * @param inStream * @return * @throws Exception */ public void readStreamWithRecursion(ByteArrayOutputStream output, InputStream inStream) throws Exception { long start = System.currentTimeMillis(); int time =500;//毫秒,間看實(shí)際情況 while (inStream.available() == 0) { if ((System.currentTimeMillis() - start) >time) {//超時(shí)退出 throw new SocketTimeoutException("超時(shí)讀取"); } } 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 + "個(gè)字節(jié)數(shù)據(jù)沒(méi)讀"); break; } } if (checkExist) { if (!checkMessage(buffer, read)) { readStreamWithRecursion(output, inStream, timeout); } } } /** * 讀取等待時(shí)間,單位毫秒 */ protected int readWait() { return 100; } /** * 讀取字節(jié) * * @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("本次讀取字節(jié)總數(shù):" + size); return output.toByteArray(); }
上面這種延遲率大幅降低,目前正在使用該方法讀取,再也沒(méi)有出現(xiàn)數(shù)據(jù)讀取不完整和阻塞現(xiàn)象。不過(guò)這種,讀取也要注意報(bào)文結(jié)束符問(wèn)題,何時(shí)讀取完畢問(wèn)題。
使用readreadLine()讀取阻塞問(wèn)題
日常寫(xiě)法:
mPrintWriter.print(sendData+ "\r\n"); mPrintWriter.flush(); String msg = mBufferedReader.readLine(); //處理數(shù)據(jù)
細(xì)心的你發(fā)現(xiàn),發(fā)送數(shù)據(jù)時(shí)添加了結(jié)束符,如果不加結(jié)束符,導(dǎo)致readLine()阻塞,讀不到任何數(shù)據(jù),最終拋出SocketTimeoutException異常
特別注意:
報(bào)文結(jié)束符:根據(jù)實(shí)際服務(wù)器規(guī)定的來(lái)添加,必要時(shí)問(wèn)后端開(kāi)發(fā)人員或者看接口文檔是否有說(shuō)明
不然在接口調(diào)試上會(huì)浪費(fèi)很多寶貴的時(shí)間,影響后期功能開(kāi)發(fā)。
使用readLine()注意事項(xiàng):
- 1、讀入的數(shù)據(jù)要注意有/r或/n或/r/n
這句話意思是服務(wù)端寫(xiě)完數(shù)據(jù)后,會(huì)打印報(bào)文結(jié)束符/r或/n或/r/n;
同理,客戶端寫(xiě)數(shù)據(jù)時(shí)也要打印報(bào)文結(jié)束符,這樣服務(wù)端才能讀取到數(shù)據(jù)。
- 2、沒(méi)有數(shù)據(jù)時(shí)會(huì)阻塞,在數(shù)據(jù)流異?;驍嚅_(kāi)時(shí)才會(huì)返回null
- 3、使用socket之類(lèi)的數(shù)據(jù)流時(shí),要避免使用readLine(),以免為了等待一個(gè)換行/回車(chē)符而一直阻塞
上面長(zhǎng)連接是發(fā)送一次數(shù)據(jù)和讀一次數(shù)據(jù),保證了當(dāng)次通訊的完整性,必須要時(shí)需要同步處理。
也有長(zhǎng)連接,客戶端開(kāi)線程循環(huán)阻塞等待服務(wù)端數(shù)據(jù)發(fā)送數(shù)據(jù)過(guò)來(lái),比如:消息推送。平時(shí)使用長(zhǎng)連接都是分別使用不同的命令發(fā)送數(shù)據(jù)且接收數(shù)據(jù),來(lái)完成不同的任務(wù)。
總結(jié)
實(shí)際開(kāi)發(fā)中,長(zhǎng)連接比較復(fù)雜,還要考慮心跳,丟包,斷開(kāi)重連等問(wèn)題。使用長(zhǎng)連接時(shí),要特別注意報(bào)文結(jié)束符問(wèn)題,結(jié)束符只是用來(lái)告訴客戶端或服務(wù)端數(shù)據(jù)已經(jīng)發(fā)送完畢,客戶端或服務(wù)端可以讀取數(shù)據(jù)了,否則客戶端或服務(wù)端會(huì)一直阻塞在read()或者readLine()方法。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
解析Spring RestTemplate必須搭配MultiValueMap的理由
本文給大家介紹Spring RestTemplate必須搭配MultiValueMap的理由,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11聊聊springboot靜態(tài)資源加載的規(guī)則
這篇文章主要介紹了springboot靜態(tài)資源加載的規(guī)則,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12spring boot整合RabbitMQ(Direct模式)
springboot集成RabbitMQ非常簡(jiǎn)單,如果只是簡(jiǎn)單的使用配置非常少,springboot提供了spring-boot-starter-amqp項(xiàng)目對(duì)消息各種支持。下面通過(guò)本文給大家介紹下spring boot整合RabbitMQ(Direct模式),需要的朋友可以參考下2017-04-04Mybatis傳遞多個(gè)參數(shù)的三種實(shí)現(xiàn)方法
這篇文章主要介紹了Mybatis傳遞多個(gè)參數(shù)的三種實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04詳述IntelliJ IDEA提交代碼前的 Code Analysis 機(jī)制(小結(jié))
本篇文章主要介紹了詳述IntelliJ IDEA提交代碼前的 Code Analysis 機(jī)制(小結(jié)),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-11-11通過(guò)實(shí)例解析synchronized和lock區(qū)別
這篇文章主要介紹了通過(guò)實(shí)例解析synchronized和lock區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12