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

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

  • 相关阅读:
    linux 程序安装目录/opt目录和/usr/local目录的区别
    Linux文件目录结构详解
    Jenkins卸载方法(Windows/Linux/MacOS)
    Jmeter案例demo
    idea打包java可执行jar包
    查看端口状态
    轻松掌握mongodb
    sphinx和coreseek
    redis
    redis默认端口6379以其名命名,是我孤陋寡闻了,是名性感美女(梅尔兹)
  • 原文地址:https://www.cnblogs.com/iyangyuan/p/2835509.html
Copyright © 2020-2023  润新知