<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          由InputStream.available()引發(fā)的偶現(xiàn)bug

          共 5129字,需瀏覽 11分鐘

           ·

          2023-01-10 02:07

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來(lái),我們一起精進(jìn)!你不來(lái),我和你的競(jìng)爭(zhēng)對(duì)手一起精進(jìn)!

          編輯:業(yè)余草

          來(lái)源:blog.csdn.net/I_am_hardy

          推薦:https://www.xttblog.com/?p=5357

          自律才能自由

          InputStream.available()引發(fā)的偶現(xiàn)bug

          案情經(jīng)過(guò)

          需求是通過(guò)Feign下載一個(gè)文件,然后將下載接口得到的InputStream文件轉(zhuǎn)成MultipartFile類型然后再調(diào)另外一個(gè)接口。從Feign返回的InputStream中讀取文件流轉(zhuǎn)換成MultipartFile類型過(guò)程中會(huì)涉及到將InputStream轉(zhuǎn)成OutputStream的操作。由于懶得找所以直接使用了前輩寫的工具類,也懶得看實(shí)現(xiàn)細(xì)節(jié),先把功能實(shí)現(xiàn)其他再說(shuō)。

          代碼大概是這樣的

          Response response = xxxFeign.getFile(fileName);
          MultipartFile mulFile = 
          MultipartFileUtils.getMulFile(response.body().asInputStream());
          Response response1 = xxxFeign.xxx(mulFile);

          但是當(dāng)調(diào)試的時(shí)候發(fā)現(xiàn)這個(gè)功能時(shí)而好時(shí)而報(bào)錯(cuò),有時(shí)候什么都不返回,有時(shí)候只返回一個(gè)損壞的文件,還是隨機(jī)的,沒(méi)有規(guī)律。

          整個(gè)過(guò)程就做了三件事情,所以要么是下載的文件有問(wèn)題,要么是轉(zhuǎn)成MultipartFile 有問(wèn)題,接受MultipartFile 參數(shù)的接口有問(wèn)題。直接調(diào)用兩個(gè)Feign接口都沒(méi)問(wèn)題,那肯定是轉(zhuǎn)成MultipartFile 有問(wèn)題。深入看一下工具類,看到一個(gè)代碼和之前自己使用的不一樣的寫法,那當(dāng)然要研究一下是自己的方法好還是別人的方法好啦,如下。

          byte[] bytes = new byte[inputStream.available()];
          inputStream.read(bytes);
          outputStream.write(bytes);

          平時(shí)我讀取inputStream都是有循環(huán)的,這里竟然沒(méi)看見(jiàn)循環(huán),這么神奇?

          inputStream.available()這個(gè)方法貌似沒(méi)用過(guò)查一下,一翻源碼就發(fā)現(xiàn)了一段備注。

          「返回此輸入流下一個(gè)方法調(diào)用可以不受阻塞地從此輸入流讀?。ɑ蛱^(guò))的估計(jì)字節(jié)數(shù)。拿來(lái)當(dāng)分配長(zhǎng)度是不正確的?!?/strong>

          available

          通過(guò)咨詢知道,原來(lái)該工具類之前使用讀取本地文件的InputStream的,是沒(méi)問(wèn)題的。。。,經(jīng)過(guò)查閱資料可知「讀取本地文件的InputStream,使用available()方法總是返回總長(zhǎng)度的。這可能和本地文件流的實(shí)現(xiàn)有關(guān)。但是當(dāng)InputStream是通過(guò)網(wǎng)絡(luò)獲取的就有問(wèn)題了」。

          案情原因

          inputStream.available()有時(shí)候不會(huì)返回InputStream的總長(zhǎng)度。例如網(wǎng)絡(luò)請(qǐng)求的InputStream會(huì)因?yàn)榫W(wǎng)絡(luò)延遲等原因?qū)е伦x取流時(shí)會(huì)出現(xiàn)讀取阻塞,這時(shí)候available()返回的就不是總長(zhǎng)度。

          為什么網(wǎng)絡(luò)延遲會(huì)導(dǎo)致InputStream阻塞

          在之前的認(rèn)知里面,網(wǎng)絡(luò)通訊時(shí)進(jìn)行一次系統(tǒng)調(diào)用,參數(shù)是一個(gè)需要通訊數(shù)據(jù)指針,例如linux的 int write(int sockfd, char *buf, int len);(參考Linux網(wǎng)絡(luò)編程),那么一個(gè)HTTP請(qǐng)求的數(shù)據(jù)報(bào)文直接全部放到一個(gè)指針,然后調(diào)用該方法,操作系統(tǒng)會(huì)將整個(gè)HTTP數(shù)據(jù)報(bào)文分包發(fā)送,并且接收方操作系統(tǒng)接收完所有TCP包后會(huì)自動(dòng)再重組完成才返回給應(yīng)用層的,阻塞不是操作系統(tǒng)的事情嗎?操作系統(tǒng)阻塞等待TCP包全部到達(dá)的時(shí)候,因?yàn)閿?shù)據(jù)沒(méi)有完整所以不會(huì)返回給應(yīng)用層的,此時(shí)應(yīng)用層系統(tǒng)調(diào)用讀取是什么都沒(méi)讀到的,相當(dāng)于這個(gè)HTTP請(qǐng)求還沒(méi)到達(dá)應(yīng)用層,所以這時(shí)候還沒(méi)有輪到available()方法的執(zhí)行呢,接收完成后(即數(shù)據(jù)準(zhǔn)備好了),應(yīng)用才能系統(tǒng)調(diào)用讀取數(shù)據(jù),讀取到的數(shù)據(jù)就是整個(gè)發(fā)送方發(fā)送的所有數(shù)據(jù)了即整個(gè)HTTP數(shù)據(jù)包,這時(shí)候應(yīng)用為啥還會(huì)阻塞呢??不都拿到所有數(shù)據(jù)了嗎?

          不懂就去學(xué)習(xí),原來(lái)TCP的頭部長(zhǎng)度是是固定的也就是能分的包也是有數(shù)量限制的,所以 int write(int sockfd, char *buf, int len);傳入的指針指向的數(shù)據(jù)長(zhǎng)度是有限制的,所以一個(gè)HTTP請(qǐng)求的全部?jī)?nèi)容不會(huì)只有一次系統(tǒng)調(diào)用。草率了膚淺了。

          草率了

          「網(wǎng)絡(luò)延遲會(huì)導(dǎo)致InputStream阻塞是因?yàn)橐粋€(gè)HTTP請(qǐng)求對(duì)應(yīng)多個(gè)TCP請(qǐng)求,分割后的HTTP數(shù)據(jù)報(bào)文會(huì)依次到達(dá)應(yīng)用層,應(yīng)用層的HTTP協(xié)議處理程序會(huì)等待數(shù)據(jù)報(bào)文到達(dá)完成才會(huì)進(jìn)一步封裝成request,然后再往下處理即到我們寫的代碼處理。但是HTTP協(xié)議處理程序接受到請(qǐng)求頭所有數(shù)據(jù)后就將請(qǐng)求體body封裝成流,這個(gè)流對(duì)應(yīng)的請(qǐng)求體的TCP數(shù)據(jù)報(bào)文可能沒(méi)有全部到達(dá),之后就直接進(jìn)入下一步處理即我們寫的代碼,所以我們讀取流的時(shí)候可能數(shù)據(jù)沒(méi)到就會(huì)阻塞等待?!?/strong>

          如何判斷流全部到達(dá)

          TCP基于字節(jié)流傳輸?shù)模还軕?yīng)用層傳什么內(nèi)容都當(dāng)作無(wú)差別的字節(jié)流傳輸?shù)侥康牡?,而且為?huì)做緩存后發(fā)送,如果應(yīng)用層發(fā)過(guò)來(lái)的長(zhǎng)度沒(méi)到長(zhǎng)度緩存要求會(huì)超時(shí)等待下一個(gè)數(shù)據(jù)一起發(fā)送,所以會(huì)出現(xiàn)出現(xiàn)粘包問(wèn)題。HTTP通過(guò)一個(gè)請(qǐng)求頭Content-Length參數(shù)設(shè)置HTTP的長(zhǎng)度,做TCP傳輸?shù)慕缦薜?,確保不會(huì)粘包的。比如TCP發(fā)生粘包即通過(guò)Content-Length參數(shù)進(jìn)行切割區(qū)別是哪個(gè)HTTP請(qǐng)求的數(shù)據(jù)。所以是「通過(guò)Content-Length的值判斷當(dāng)前HTTP請(qǐng)求數(shù)據(jù)是否全部達(dá)到的,即流是否結(jié)束,是否需要繼續(xù)阻塞等待數(shù)據(jù)」

          源碼看不懂只能用過(guò)實(shí)驗(yàn)驗(yàn)證了。

          實(shí)驗(yàn)如下:

          通過(guò)springboot編寫一個(gè)post請(qǐng)求接口,方法里面循環(huán)讀取請(qǐng)求體的流,流結(jié)束即返回。用postman設(shè)置請(qǐng)求頭參數(shù)Content-Length設(shè)置超大,請(qǐng)求體隨便寫發(fā)送請(qǐng)求。因?yàn)镃ontent-Length設(shè)置比實(shí)際發(fā)送的數(shù)據(jù)多了,所以InputStream會(huì)一直阻塞,請(qǐng)求不會(huì)返回,因?yàn)榭蛻舳瞬粫?huì)發(fā)送Content-Length長(zhǎng)度的數(shù)據(jù)。點(diǎn)取消請(qǐng)求后報(bào)Unexpected EOF read on the socket錯(cuò)誤。達(dá)到預(yù)期。

          Content-Length
          @PostMapping("/post")
          public void post(HttpServletRequest httpServletRequest){

              InputStream inputStream = null;
              try {
                  inputStream = httpServletRequest.getInputStream();
                  for (int i = inputStream.read(); i != -1; i = inputStream.read()) {

                  }
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  if (inputStream != null) {
                      try {
                          inputStream.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          擴(kuò)展閱讀@RequestBody為什么不阻塞

          根據(jù)上面實(shí)驗(yàn),心想如果手動(dòng)設(shè)置Content-Length長(zhǎng)度,設(shè)置長(zhǎng)度比實(shí)際發(fā)送數(shù)據(jù)的長(zhǎng)度大豈不是會(huì)出現(xiàn)請(qǐng)求超時(shí)bug。去測(cè)試公司的接口,通過(guò)postman調(diào)接口把Content-Length設(shè)置超級(jí)長(zhǎng),預(yù)期會(huì)出現(xiàn)接口超時(shí)的情況,然后拿去問(wèn)該接口開(kāi)發(fā)同事讓他一臉懵逼,找不到bug,豈不是很……。

          邪惡的去操作發(fā)現(xiàn)沒(méi)超時(shí),驚訝,難道我的分析是錯(cuò)的???當(dāng)我第一次請(qǐng)求后沒(méi)超時(shí),再次請(qǐng)求時(shí)超時(shí)了,驚訝,同事沒(méi)有一臉懵逼,我自己倒是一臉懵逼。

          猜想:可能是spring boot做反序列化的時(shí)候不讀等讀完流的數(shù)據(jù)就返回了,spring boot是基于jackson做反序列化的,去翻一下源碼。翻了半天搞不懂,源碼太復(fù)雜了,實(shí)在是看得頭疼,通過(guò)實(shí)驗(yàn)的驗(yàn)證吧,以后看懂源碼了再通過(guò)源碼驗(yàn)證。

          既然是反序列化的時(shí)候不讀等讀完流的數(shù)據(jù),那么應(yīng)該是根據(jù)一些符號(hào)或者什么字符做為讀取結(jié)束的標(biāo)記,即使流中數(shù)據(jù)沒(méi)全部讀取完成也結(jié)束讀取,同事的接口是基于@RequestBody的,對(duì)應(yīng)的請(qǐng)求體結(jié)構(gòu)是JSON字符串,那么的結(jié)束標(biāo)記應(yīng)當(dāng)就是括號(hào)}

          那我就將請(qǐng)求體里面的JSON字符串的括號(hào)}去掉,將Content-Length設(shè)置比實(shí)際長(zhǎng)度大,請(qǐng)求超時(shí)了,Content-Length設(shè)置正常就報(bào)jackson反序列化錯(cuò)誤。果然不出我所料。使用MultipartFile接收文件也類似,文件在請(qǐng)求體里面會(huì)有符號(hào)標(biāo)志開(kāi)始和結(jié)束,spring mvc會(huì)讀取完文件流完成后轉(zhuǎn)成MultipartFile類型傳遞到用戶編碼部分,所以如果使用MultipartFile.getInputStream().available()也是會(huì)返回正確的結(jié)果,因?yàn)閙vc等待文件流到達(dá)完成才進(jìn)入用戶編碼的部分。

          「結(jié)論:@RequestBody不阻塞是因?yàn)閖ackson做反序列化的時(shí)候只讀取第一個(gè)括號(hào){}里面的數(shù)據(jù),然后反序列化后返回,不讀取完整的流,但是流沒(méi)釋放,如果Content-Length設(shè)置比實(shí)際長(zhǎng)度大,因?yàn)樯弦粋€(gè)沒(méi)讀完數(shù)據(jù)就返回了,所以同一個(gè)客戶端下一次請(qǐng)求數(shù)據(jù)會(huì)當(dāng)作是這個(gè)流的數(shù)據(jù),所以這次請(qǐng)求的流不會(huì)有數(shù)據(jù)或者數(shù)據(jù)不完整所以要么超時(shí)要不報(bào)400錯(cuò)誤?!?/strong>

          實(shí)際上,不同的 InputStream 在實(shí)現(xiàn) available 方法時(shí),略有不同。所以,并不是使用了 inputStream.available() 都會(huì)出問(wèn)題。另外,inputStream.available() 出問(wèn)題是偶發(fā)的,也給排查問(wèn)題帶來(lái)了一定的困難。搞懂此問(wèn)題,讓你在關(guān)鍵時(shí)刻出類拔萃,也能讓你在跑路時(shí),惡心惡心其他人!

          瀏覽 71
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    99无码人妻一区二区三区色 | 91久久国产综合久久91 | 欧美 亚洲 日韩 国产 高清 | 加勒比无码久久综合 | IPX-811桃乃木かな无码破解 |