Socket傳輸文件時進行校驗(簡單解決TCP粘包問題)


  本小菜最近頻繁使用Socket技術,遇到不少問題,有時候會心煩意亂,因為這問題並不是那么容易解決。

就拿Socket傳輸文件來說,Socket無非就是TCPUDP協議的封裝,用它來傳輸文件,最正常不過了。但就是這么常用的東西,依然有非常多的麻煩事,而且沒有太容易的解決方案。

本小菜嘗試用Socket傳輸圖片,就遇到了如下偉大的粘包問題。

先科普一下什么是粘包(確切的說是TCP傳輸粘包)。簡單的說就是通過TCP協議發送了多條獨立的數據,但接收的時候,有些數據不幸的合並成了一個。比如客戶端向服務器發送兩個命令:”Start””Parameter[x.x.x]”,第一個命令的含義是開始,第二個命令的含義是啟動參數。但是服務器接收的時候,很可能不是分兩次接收,而是一次接收到”StartParameter[x.x.x]”,這下全亂了。

造成粘包的原因有很多,大致就是TCP協議本身的缺陷或數據緩沖的問題。我也不是很懂,就不誤導大家了。

小菜利用Socket傳輸圖片時,想先發送一個初始化參數,這個參數大致就是說明圖片名稱、圖片歸屬等信息。傳輸完成之后,服務器再向客戶端發送圖片的MD5值,在客戶端校驗圖片信息是否完整,保證上傳無誤。思路如下圖(一張圖勝過千言萬語)

但就是這么一個簡單的過程,實現起來可真是困難重重,從上面說明可以看出,在傳送圖片之前要先傳送命令,圖片傳完后又要傳送命令,這就引來了偉大的粘包問題!命令和圖片粘在一起!

從網上查到吐血,基本上都是回答自定義包結構,加上包頭、包尾、錯誤重發等等。這些基於字節的操作,沒有深厚的底層基礎,是搞不定的,當然,我也搞不定,項目也沒那么高的需求,果斷放棄這種做法。

經過分析,發現粘包的主要原因是客戶端連續向服務器發了三部分內容,導致數據混亂。既然是這樣,就有了如下設計:

  從上圖可以看出,服務器收到初始化參數之后,先返回給客戶端一個確認信息,然后客戶端再傳送圖片,表面上看是麻煩了,但這避免了粘包問題,把命令和圖片分離開,同時又增加了系統可靠性。

  還可以發現,客戶端沒有向服務器發送結束命令,也就是說服務器要自己判斷圖片是否上傳完成。怎么判斷呢?小菜的思路是客戶端獲取文件的長度,作為初始化參數傳給服務器,服務器根據接收的數據長度判斷是否上傳完成。

為什么要這樣設計?因為服務器接收圖片用的是一個阻塞循環,如果客戶端不發送結束命令,這個循環將一直阻塞下去,但客戶端一旦發送結束命令,就會和圖片數據粘包。這個矛盾解不開。。。。

 

看下具體代碼:

 

服務器核心代碼(C#)

 

 1 try
 2 {
 3     string removeMsg;
 4     SendBack sd = new SendBack();
 5     skClient.ReceiveTimeout = 30; //設置接收超時,超時說明上傳圖片失敗
 6 
 7     //接收初始化數據(利用Receive的阻塞性等待初始化數據)
 8     receiveN = skClient.Receive(receiveData);
 9 
10     //解析客戶端消息
11     removeMsg = Encoding.UTF8.GetString(receiveData, 0, receiveN);
12 
13     //獲取文件長度
14     long fileLength = Convert.ToInt64(removeMsg.Split(new char[] { '|' })[1]);
15 
16     //回發確認信息
17     sd.SendToClient(skClient, "T");
18 
19     //寫入圖片處理
20     using (Stream pic = File.Create("E:\\" + removeMsg.Split(new char[] { '|' })[0]))
21     {
22         //臨時長度變量
23         long tempLength = 0;
24 
25         //接收圖片包(再次阻塞,接收圖片)
26         while ((receiveN = skClient.Receive(receiveData)) > 0)//接收
27         {
28             tempLength += receiveN;
29 
30             //寫入圖片
31             pic.Write(receiveData, 0, receiveN);
32             pic.Flush();
33 
34             //判斷文件是否接收完全
35             if (tempLength == fileLength)
36             {
37                 //接收完全則退出循環
38                 break;
39             }
40         }
41 
42         //釋放文件流
43         pic.Close();
44         pic.Dispose();
45     }
46 
47     //回發圖片MD5校驗碼
48     MD5Helper md5 = new MD5Helper();
49     sd.SendToClient(skClient, md5.md5_hash("E:\\" + removeMsg.Split(new char[] { '|' })[0]));
50 }
51 catch (SocketException se) 
52 {
53     //關閉客戶端連接
54     //超時有兩種可能,一是發送數據包丟失,導致無法跳出循環而超時;二是網絡或客戶端異常。無論哪種情況,我們都有充分的理由斷開連接,標志上傳圖片失敗
55     skClient.Close();
56     skClient.Dispose();
57 }
58 catch (Exception ex)
59 {
60     //異常掉線處理:得到掉線客戶端的IP地址傳遞給接口實現類
61     iGetClientData.getClientIP(((IPEndPoint)skClient.RemoteEndPoint).Address + ex.ToString());
62 }

 

 

客戶端核心代碼(Java)

 1 try {
 2   socket = new Socket();
 3   socket.connect(new InetSocketAddress("192.168.24.177", 5522),10 * 1000);
 4   dos = new DataOutputStream(socket.getOutputStream());
 5 
 6   File file = new File("D:\\1.jpg");
 7   fis = new FileInputStream(file);
 8   sendBytes = new byte[1024]; 
 9   
10   /*發送初始化數據*/
11   String startMessage = "111111.jpg|" + file.length();
12   byte[] bytStartMessage = startMessage.getBytes("UTF-8");
13   dos.write(bytStartMessage,0,bytStartMessage.length);
14   
15   /*判斷服務器是否收到初始化數據*/
16   String rtSingle = rsm.read(socket);
17   if("T".equals(rtSingle)){
18     /*寫入圖片*/
19     while ((length = fis.read(sendBytes, 0, sendBytes.length)) > 0) {
20         dos.write(sendBytes, 0, length);
21         dos.flush();
22     }
23   }
24 
25   /*發送結束信息*/
26   /*String endMessage = "End";
27   byte[] bytEndMessage = endMessage.getBytes("UTF-8");
28   dos.write(bytEndMessage,0,bytEndMessage.length);*/
29   
30   /*獲取本地圖片的MD5校驗碼,轉成大寫形式*/
31   String localPicMD5 = MD5Helper.getFileMD5(file).toUpperCase();
32   /*接收回發的MD5校驗碼,轉成大寫形式*/
33   String backPicMD5 = rsm.read(socket).toUpperCase();
34   /*對比校驗碼,判斷照片是否上傳成功*/
35   if(localPicMD5.equals(backPicMD5)){
36     System.out.println("succes!");
37   }else{
38     System.out.println("fail!");
39   }
40 } catch (SocketException se) {
41 /*上傳失敗!*/
42 }catch(Exception e){
43 e.printStackTrace();
44 }finally {
45   try{
46     if (dos != null)
47         dos.close();
48     if (fis != null)
49         fis.close();
50     if (socket != null)
51         socket.close();
52   } catch (Exception e) {
53     e.printStackTrace();
54   }
55 }

 

  通過代碼相信讀者能明白小菜的意思,服務器通過判斷接收數據的總長度,主動用break跳出while循環,跳出循環后服務器才可以向客戶端發送圖片MD5校驗碼。

  稍加思考,會發現這樣設計有一個小問題!假設一旦網絡出現問題,導致數據包丟失,就會造成服務器端接收到的圖片數據小於實際的長度,這樣一來就沒辦法跳出while循環,也就無法向客戶端發送MD5校驗碼,導致客戶端一直阻塞。

  考慮到這個問題,小菜在代碼中設置了Receive超時,服務器端一旦超過指定時間沒有收到數據,依然是阻塞狀態,那么就拋出異常,拋出異常后斷開和客戶端的連接,代表傳送圖片失敗。因為在正常傳輸的情況下,不可能很長時間都收不到數據。如果超時,除了傳輸過程中數據包丟失無法跳出while,就是網絡異常,無論是哪種情況,都可以認為本次傳輸失敗。

好啦,就講到這,小菜水平有限,望高手勿噴。

PS

Socket程序一定要時刻清醒:Receive(C#)read(Java)等這樣的方法都是阻塞的,也就是說,如果沒有數據,線程會一直等待,程序會在這暫停,直到有消息到來。

  如果是單純傳輸文件,則不必考慮粘包問題,因為即使粘了,也無所謂,反正都是寫入,只不過粘包后每次寫入的數據長度可能不相等而已。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM