java基於TCP的socket資料包拆分方法

來源:互聯網
上載者:User

 

最近在做socket傳送,遇到丟包的問題,困擾了好久,看到了這篇文章,原文地址:http://suwish.com/html/java-tcp-socket-stream-packet-split.html 好了,現在輕鬆許多。話說看到falcom官方的《空軌》動畫時間表,又看到崩壞的人設,我表示真的非常不能接受。當然了這個咱也管不著。

 

好了話歸正題,前不久寫的socket程式,伺服器是java的,用戶端是flex。一開始就想過所謂的拆分資料包的問題,因為資料包結構是自己定義的,也簡單的寫了幾行資料包的驗證。關鍵是測試中完全沒有發生什麼情況,但是發布到外網之後卻出現一些非常奇怪的問題,典型的就是通訊過一定時間之後,資料包驗證那塊就會出錯然後就拋棄了資料包,這就是所謂的“丟包”吧,但是我的是TCP的socket,所謂因為網路問題導致的資料包沒有發送與接收成功這種問題應該是不可能出現的。於是看了幾篇文正,發現這種現象被稱作“粘包”,我覺得還是挺貼切的。經過一定時間的思考、和測試,大概瞭解了其中的原理,按照現在的此時情況來看,應該是沒什麼問題。於是在此總結一下,如果哪天我發現一些新問題或更好的方法,還是會來繼續補充這篇文章的。當然各位路過的前輩覺得其中存在錯誤什麼的也請指出。首先在將程式之前,還是先說一下TCP的通訊。TCP和UDP的最大區別就是TCP維護了串連狀態,而這個狀態我們可以理解為一個暢通的流通道,即stream,當然流的傳輸內容歸根結底還是byte。於是將流的通訊進行假設,假設存在一條引水管道,從遠方輸水過來,我們在這邊等待水的到來,並使用容器接收流出來的水。此時存在一下幾種情況:
  • 假設這個輸水管道在操作過程中不會斷掉。
  • 先進先出,先流進管道的水一定是先到達。
  • 某一狀態,(在輸水管沒有斷掉是)流量無法保證,甚至某段時間沒有水。
  • 我們的容器(緩衝區)大小固定,即每次接收的水量存在最大值,超多將無法接收。
在以上情況作為前提,再迴歸編碼。TCP的socket可以通訊的前提是串連沒有斷開,串連斷開事件可以從兩種情況進行判斷。流斷開,這read()為-1,SocketException或者IoException。分包的前提是socket可以正常通訊,不論網路延時多麼嚴重,這些TCP協議會去處理。我們僅僅關心通訊既可。現在最優情況,即實驗室環境,或者是內網,伺服器與用戶端延時不會大於一毫秒,此時只要我們接收輸水的容器夠大,基本就可以完成正常通訊。但是互連網情況就非常複雜,資料包要經過無數網路軟硬體裝置,延時不可避免,但是TCP協議會像輸水管道那樣,保障資料包的順序和保證不會丟失。所以這時我們可以控制的只有接水的容器。查看一些簡單的TCP通訊的知識,網路資料在傳輸的時候存在緩衝現象,簡單的說,就是連續發送N個資料包。他們可能被緩衝起來一起被發送。這種情況就是粘包,當然對於接收端來說,我們不能保證 每次都能正好的完整的接收資料包,更多時候是x.5個資料包。再次回到輸水的模型,我們的容器等待水的到來,現在存在逾時時間即每次等待水來有一個最大時間,超過這個時間,即使沒有接到一滴水我們也要處理這個水桶。所以我們得到水桶的水理論上是大於等於0,小於等於水桶的容量。我想這樣說應該可以很清楚的表達清楚了吧。現在開始從代碼角度來說。現在我們有一個byte[] buffer = new byte[MAX_LEN],即資料包讀取緩衝區,int len = connection.read(buffer)。read方法使用buffer讀資料流,len為實際讀出的資料長度,此長度大於等於0,小於等於MAX_LEN。等於0自然不去處理,等於-1認為串連斷開,當然read方法會拋出異常,即當讀取資料過程中,串連出錯。現在我們獲得一個buffer,即緩衝區。裡面存在len長度的可用資料。我們要做的就是根據自己的協議結構將這個buffer轉化為遵循我們自己的協議的packet。進而交由後面的商務邏輯代碼處理。此時我們定義自己的通訊協定一個byte的包頭,用於資料吧合法性驗證,兩byte資料包長(一般用4byte,即一個int),剩下內容為可變長度的資料包體。現在我們拿到buffer,這時候就有分包(粘包),和組包(資料包沒有接收完整)兩種情況。感覺似乎比較頭疼,但是實際上獲得packet我們緊緊需要知道的是資料包的真實長度,即2byte的內容,轉為short後假設為PACKET_LEN。然後我們只要拆分和等待PACKET_LEN個長度的byte即可,那才是我們班真正需要的東西。當然,這個過程我曾經陷入過誤區,然後經人指點後才發現我關注了很多沒用的東西,結果增加了代碼的複雜度。之後就上代碼了,現在我的結構是伺服器使用nio,然後nio架構將buffer封裝為java.nio.ByteBuffer。其底層實現還是固定長度的byte[],它做的僅僅是封裝了一些byte操作的快捷方法而已。既然它封裝了,我們就要利用一下。
  • ByteBuffer.remaining(),此方法最給力,返回剩餘的可用長度,此長度為實際讀取的資料長度,最大自然是底層數組的長度。於是這樣看來這個ByteBuffer更像是一個可標記的流。
  • ByteBuffer.get(byte[]),從ByteBuffer中讀取byte[]。
首先呢把ByteBuffer當做流來處理,即read(ByteBuffer)之後ByteBuffer.flip()。此時重設到流的前端。這個java代碼是按照最原始的思路寫的,寫的比較難看,但是比較清晰。有時間再最佳化下演算法,應該可以寫的再漂亮一點。public List<byte[]> getPacket(ByteBuffer buffer) throws Exception{
     pLink.clear();
     try{
     while(buffer.remaining() > 0){
      if(packetLen == 0){  //此時存在兩種情況及在資料包包長沒有獲得的情況下可能已經獲得過一次資料包
      if(buffer.remaining() + _packet.length < 3){
      byte[] temp = new byte[buffer.remaining()];
      buffer.get(temp);
      _packet = PacketUtil.joinBytes(_packet , temp);
      break;  //儲存包頭
      }else{if(_packet.length == 0){
      buffer.get();
      packetLen = PacketUtil.parserBuffer2ToInt(buffer);
      }else if(_packet.length == 1){
       packetLen = PacketUtil.parserBuffer2ToInt(buffer);
      } else if(_packet.length == 2){
       byte[] lenByte = new byte[2];
       lenByte[0] = _packet[1];
       lenByte[1] = buffer.get();
       packetLen = PacketUtil.parserBytes2ToInt(lenByte);
      } else{
       packetLen = PacketUtil.parserBytes2ToInt(_packet , 1);
      }
    
      }
     }
    
     if(_packet.length <= 3){   //此時_packet 沒有有用資料,所需資料都在緩衝區中
      if(buffer.remaining() < packetLen){
       _packet = new byte[buffer.remaining()];
       buffer.get(_packet);
      }else{
       byte[] p = new byte[packetLen];
       buffer.get(p);
       pLink.add(p);
       packetLen = 0;
       _packet = new byte[0];
      }
     }else {
      if(buffer.remaining() + _packet.length - 3 < packetLen){   //剩餘資料包不足一個完整包,儲存後等待寫一個
              byte[] temp = new byte[buffer.remaining()];
       buffer.get(temp);
       _packet = PacketUtil.joinBytes(_packet , temp);break;
      }else{            //資料包完整或者多出
       byte[] temp = new byte[packetLen - ( _packet.length - 3) ];
       buffer.get(temp);
       pLink.add(PacketUtil.subPacket(PacketUtil.joinBytes(_packet , temp)));
       _packet = new byte[0];
       packetLen = 0;
      }
     }
     }
     }catch(Exception e){
      System.out.println("..GETPACKET packetLen = " + packetLen + " _packet.length = " + _packet.length);
      throw e;
     }
     return pLink;
    }如果覺得不好看,可以先看下面的Flex首先方法,思路是一樣的,但是看起來非常簡單。 private function socketDataHandler(event:ProgressEvent):void{
     
     try{
     while(true){
      if(packet_len == 0){
      if(socket.bytesAvailable < 3) return ;
      var temp : ByteArray = new ByteArray();
      socket.readBytes(temp , 0 , 3);
      packet_len = PacketUtil.parserBytesToInt2(temp , 1);
      }
      if(socket.bytesAvailable < packet_len) return;
      var buffer : ByteArray = new ByteArray();
      socket.readBytes(buffer , 0 , packet_len);
      packet_len = 0;
      buffer.position = 0;
      packetArrive(buffer);
     }
    
     }catch(e : Error){
      trace(e.message);
     }
    }果然貼代碼太佔用篇幅了。首先拿Flex說,Flex庫和Flash實際是一樣的。flex中的socket中有自己的緩衝區,所以自己只管按時讀資料即可。所以我們就等packet的長度,等待長度之後等這個長度的位元組,簡明扼要。但是java就不同,java的底層緩衝區我們沒辦法控制,於是就需要自己寫一個東西緩衝沒有接收完整的資料。就是代碼中的_packet,他是一個初始化長度為0的byte[]。思想就是等我們需要的東西,等到就讀出來,剩下不完整的就存起來和下一次合并再判斷。當然這種東西都是有規律的,我覺得還沒有發現這個規律,如果發現的話,代碼長度應該會像Flex那麼簡明吧。規律這種東西真的很美妙,我們總結出規律之後就完全跳出了複雜和容易出錯的步驟,進而去關注更重要的事情。就像我獲得packet之後,剛開始算數組索引,由於是可變長度,裡面的內容也是定義的可變資料,所以算資料索引算的非常痛苦。之後我後來發現了所以規律,簡單的說就是index += packet[index] + n。然後就完全從資料結構裡面擺脫出來。嗯,差不多就是這個樣子了
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.