【遊戲開發】網絡編程之淺談TCP粘包、拆包問題及其解決方案

引子

現現在手遊開發中網絡編程是必不可少的重要一環,若是使用的是TCP協議的話,那麼不可避免的就會碰見TCP粘包和拆包的問題,馬三以爲haifeiWu博主的 TCP 粘包問題淺析及其解決方案 這篇博客講得很不錯,所以轉載過來並稍做修改與你們分享,也留做本身時常溫習和查閱,文章的版權歸haifeiWu博主全部。html

做者: haifeiWujava

出處: http://www.hchstudio.cn/golang

關於做者:專一大後端,分佈式,高併發等領域,請多多賜教!編程

原文連接:http://www.javashuo.com/article/p-qsfvjrud-bb.html後端

TCP協議的簡單介紹

TCP是面向鏈接的運輸層協議數組

簡單來講,在使用TCP協議以前,必須先創建TCP鏈接,就是咱們常說的三次握手。在數據傳輸完畢以後,必須是釋放已經創建的TCP鏈接,不然會發生不可預知的問題,形成服務的不可用狀態。緩存

每一條TCP鏈接都是可靠鏈接,且只有兩個端點服務器

TCP鏈接是從Server端到Client端的點對點的,經過TCP傳輸數據,無差錯,不重複不丟失。微信

TCP協議的通訊是全雙工的markdown

TCP協議容許通訊雙方的應用程序在任什麼時候候都能發送數據。TCP 鏈接的兩端都設有發送緩衝區和接收緩衝區,用來臨時存放雙向通訊的數據。發送數據時,應用程序把數據傳送給TCP的緩衝後,就能夠作本身的事情,而TCP在合適的時候將數據發送出去。在接收的時候,TCP把收到的數據放入接收緩衝區,上層應用在合適的時候讀取數據。

TCP協議是面向字節流的

TCP中的流是指流入進程或者從進程中流出的字節序列。因此向Java,golang等高級語言在進行TCP通訊是都須要將相應的實體序列化才能進行傳輸。還有就是在咱們使用Redis作緩存的時候,都須要將放入Redis的數據序列化才能夠,緣由就是Redis底層就是實現的TCP協議。

TCP並不知道所傳輸的字節流的含義,TCP並不能保證接收方應用程序和發送方應用程序所發出的數據塊具備對應大小的關係(這就是TCP傳輸過程當中產生的粘包問題)。可是應用程序接收方最終受到的字節流與發送方發送的字節流是必定相同的。所以,咱們在使用TCP協議的時候應該制定合理的粘包拆包策略。

下圖是TCP的協議傳輸的整個過程:

TCP面向字節流

下面這個圖是從老錢的博客裏面取到的,很是生動
TCP傳輸動圖

TCP粘包問題復現

理論推敲

以下圖所示,出現的粘包問題一共有三種狀況

TCP粘包問題

第一種狀況:
如上圖中的第一根bar所示,服務端一共讀到兩個數據包,每一個數據包都是完成的,並無發生粘包的問題,這種狀況比較好處理,服務器只須要簡單的從網絡緩衝區去讀就行了,每次服務端讀取到的消息都是完成的,並不會出現數據不正確的狀況。

第二種狀況:
服務端僅收到一個數據包,這個數據包包含客戶端發出的兩條消息的完整信息,這個時候基於第一種狀況的邏輯實現的服務端就蒙了,由於服務端並不能很好的處理這個數據包,甚至不能處理,這種狀況其實就是TCP的粘包問題。

第三種狀況:
服務端收到了兩個數據包,第一個數據包只包含了第一條消息的一部分,第一條消息的後半部分和第二條消息都在第二個數據包中,或者是第一個數據包包含了第一條消息的完整信息和第二條消息的一部分信息,第二個數據包包含了第二條消息的剩下部分,這種狀況實際上是發送了TCP拆包問題,由於發生了一條消息被拆分在兩個包裏面發送了,一樣上面的服務器邏輯對於這種狀況是很差處理的。

爲何會發生TCP粘包、拆包

  1. 應用程序寫入的數據大於套接字緩衝區大小,這將會發生拆包。

  2. 應用程序寫入數據小於套接字緩衝區大小,網卡將應用屢次寫入的數據發送到網絡上,這將會發生粘包。

  3. 進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包。

  4. 接收方法不及時讀取套接字緩衝區數據,這將發生粘包。

如何處理粘包、拆包

一般會有如下一些經常使用的方法:

  1. 使用帶消息頭的協議、消息頭存儲消息開始標識及消息長度信息,服務端獲取消息頭的時候解析出消息長度,而後向後讀取該長度的內容。

  2. 設置定長消息,服務端每次讀取既定長度的內容做爲一條完整消息,當消息不夠長時,空位補上固定字符。

  3. 設置消息邊界,服務端從網絡流中按消息編輯分離出消息內容,通常使用‘\n’。

  4. 更爲複雜的協議,例如樓主最近接觸比較多的車聯網協議808,809協議。

TCP粘包拆包的代碼實踐

下面代碼樓主主要演示了使用規定消息頭,消息體的方式來解決TCP的粘包,拆包問題。

server端代碼: server端代碼的主要邏輯是接收客戶端發送過來的消息,從新組裝出消息,並打印出來。

import java.io.*; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; /** * @author wuhf * @Date 2018/7/16 15:50 **/ public class TestSocketServer { public static void main(String args[]) { ServerSocket serverSocket; try { serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8089)); while (true) { Socket socket = serverSocket.accept(); new ReceiveThread(socket).start(); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } static class ReceiveThread extends Thread { public static final int PACKET_HEAD_LENGTH = 2;//包頭長度 private Socket socket; private volatile byte[] bytes = new byte[0]; public ReceiveThread(Socket socket) { this.socket = socket; } public byte[] mergebyte(byte[] a, byte[] b, int begin, int end) { byte[] add = new byte[a.length + end - begin]; int i = 0; for (i = 0; i < a.length; i++) { add[i] = a[i]; } for (int k = begin; k < end; k++, i++) { add[i] = b[k]; } return add; } @Override public void run() { int count = 0; while (true) { try { InputStream reader = socket.getInputStream(); if (bytes.length < PACKET_HEAD_LENGTH) { byte[] head = new byte[PACKET_HEAD_LENGTH - bytes.length]; int couter = reader.read(head); if (couter < 0) { continue; } bytes = mergebyte(bytes, head, 0, couter); if (couter < PACKET_HEAD_LENGTH) { continue; } } // 下面這個值請注意,必定要取2長度的字節子數組做爲報文長度,你懂得 byte[] temp = new byte[0]; temp = mergebyte(temp, bytes, 0, PACKET_HEAD_LENGTH); String templength = new String(temp); int bodylength = Integer.parseInt(templength);//包體長度 if (bytes.length - PACKET_HEAD_LENGTH < bodylength) {//不夠一個包 byte[] body = new byte[bodylength + PACKET_HEAD_LENGTH - bytes.length];//剩下應該讀的字節(湊一個包) int couter = reader.read(body); if (couter < 0) { continue; } bytes = mergebyte(bytes, body, 0, couter); if (couter < body.length) { continue; } } byte[] body = new byte[0]; body = mergebyte(body, bytes, PACKET_HEAD_LENGTH, bytes.length); count++; System.out.println("server receive body: " + count + new String(body)); bytes = new byte[0]; } catch (Exception e) { e.printStackTrace(); } } } } } 

client端代碼:客戶端代碼主要邏輯是組裝要發送的消息,肯定消息頭,消息體,而後發送到服務端。

import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; /** * @author wuhf * @Date 2018/7/16 15:45 **/ public class TestSocketClient { public static void main(String args[]) throws IOException { Socket clientSocket = new Socket(); clientSocket.connect(new InetSocketAddress(8089)); new SendThread(clientSocket).start(); } static class SendThread extends Thread { Socket socket; PrintWriter printWriter = null; public SendThread(Socket socket) { this.socket = socket; try { printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } @Override public void run() { String reqMessage = "HelloWorld! from clientsocket this is test half packages!"; for (int i = 0; i < 100; i++) { sendPacket(reqMessage); } if (socket != null) { try { socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public void sendPacket(String message) { try { OutputStream writer = socket.getOutputStream(); writer.write(message.getBytes()); writer.flush(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } 

小結

最近一直在寫一些框架性的博客,專門針對某些問題進行原理性的技術探討的博客還比較少,因此樓主想着怎樣能在本身學到東西的同時也能夠給一同在技術這條野路子上奮鬥的小夥伴們一些啓發,是樓主一直努力的方向。

參考文章

 

 

 

若是以爲本篇博客對您有幫助,能夠掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支持微信和支付寶喲!

       

 

做者:馬三小夥兒
出處:http://www.javashuo.com/article/p-exogzseh-ey.html 請尊重別人的勞動成果,讓分享成爲一種美德,歡迎轉載。另外,文章在表述和代碼方面若有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!

相關文章
相關標籤/搜索