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)等这样的方法都是阻塞的,也就是说,如果没有数据,线程会一直等待,程序会在这暂停,直到有消息到来。

  如果是单纯传输文件,则不必考虑粘包问题,因为即使粘了,也无所谓,反正都是写入,只不过粘包后每次写入的数据长度可能不相等而已。

 

posted @ 2012-12-27 12:57  杨元  阅读(15570)  评论(8编辑  收藏  举报