Socket传输文件时进行校验(简单解决TCP粘包问题)
2012-12-27 12:57
363 查看
本小菜最近频繁使用Socket技术,遇到不少问题,有时候会心烦意乱,因为这问题并不是那么容易解决。
就拿Socket传输文件来说,Socket无非就是TCP、UDP协议的封装,用它来传输文件,最正常不过了。但就是这么常用的东西,依然有非常多的麻烦事,而且没有太容易的解决方案。
本小菜尝试用Socket传输图片,就遇到了如下伟大的粘包问题。
先科普一下什么是粘包(确切的说是TCP传输粘包)。简单的说就是通过TCP协议发送了多条独立的数据,但接收的时候,有些数据不幸的合并成了一个。比如客户端向服务器发送两个命令:”Start”、”Parameter[x.x.x]”,第一个命令的含义是开始,第二个命令的含义是启动参数。但是服务器接收的时候,很可能不是分两次接收,而是一次接收到”StartParameter[x.x.x]”,这下全乱了。
造成粘包的原因有很多,大致就是TCP协议本身的缺陷或数据缓冲的问题。我也不是很懂,就不误导大家了。
小菜利用Socket传输图片时,想先发送一个初始化参数,这个参数大致就是说明图片名称、图片归属等信息。传输完成之后,服务器再向客户端发送图片的MD5值,在客户端校验图片信息是否完整,保证上传无误。思路如下图(一张图胜过千言万语):
但就是这么一个简单的过程,实现起来可真是困难重重,从上面说明可以看出,在传送图片之前要先传送命令,图片传完后又要传送命令,这就引来了伟大的粘包问题!命令和图片粘在一起!
从网上查到吐血,基本上都是回答自定义包结构,加上包头、包尾、错误重发等等。这些基于字节的操作,没有深厚的底层基础,是搞不定的,当然,我也搞不定,项目也没那么高的需求,果断放弃这种做法。
经过分析,发现粘包的主要原因是客户端连续向服务器发了三部分内容,导致数据混乱。既然是这样,就有了如下设计:
从上图可以看出,服务器收到初始化参数之后,先返回给客户端一个确认信息,然后客户端再传送图片,表面上看是麻烦了,但这避免了粘包问题,把命令和图片分离开,同时又增加了系统可靠性。
还可以发现,客户端没有向服务器发送结束命令,也就是说服务器要自己判断图片是否上传完成。怎么判断呢?小菜的思路是客户端获取文件的长度,作为初始化参数传给服务器,服务器根据接收的数据长度判断是否上传完成。
为什么要这样设计?因为服务器接收图片用的是一个阻塞循环,如果客户端不发送结束命令,这个循环将一直阻塞下去,但客户端一旦发送结束命令,就会和图片数据粘包。这个矛盾解不开。。。。
看下具体代码:
服务器核心代码(C#):
客户端核心代码(Java):
通过代码相信读者能明白小菜的意思,服务器通过判断接收数据的总长度,主动用break跳出while循环,跳出循环后服务器才可以向客户端发送图片MD5校验码。
稍加思考,会发现这样设计有一个小问题!假设一旦网络出现问题,导致数据包丢失,就会造成服务器端接收到的图片数据小于实际的长度,这样一来就没办法跳出while循环,也就无法向客户端发送MD5校验码,导致客户端一直阻塞。
考虑到这个问题,小菜在代码中设置了Receive超时,服务器端一旦超过指定时间没有收到数据,依然是阻塞状态,那么就抛出异常,抛出异常后断开和客户端的连接,代表传送图片失败。因为在正常传输的情况下,不可能很长时间都收不到数据。如果超时,除了传输过程中数据包丢失无法跳出while,就是网络异常,无论是哪种情况,都可以认为本次传输失败。
好啦,就讲到这,小菜水平有限,望高手勿喷。
PS:
写Socket程序一定要时刻清醒:Receive(C#)、read(Java)等这样的方法都是阻塞的,也就是说,如果没有数据,线程会一直等待,程序会在这暂停,直到有消息到来。
如果是单纯传输文件,则不必考虑粘包问题,因为即使粘了,也无所谓,反正都是写入,只不过粘包后每次写入的数据长度可能不相等而已。
就拿Socket传输文件来说,Socket无非就是TCP、UDP协议的封装,用它来传输文件,最正常不过了。但就是这么常用的东西,依然有非常多的麻烦事,而且没有太容易的解决方案。
本小菜尝试用Socket传输图片,就遇到了如下伟大的粘包问题。
先科普一下什么是粘包(确切的说是TCP传输粘包)。简单的说就是通过TCP协议发送了多条独立的数据,但接收的时候,有些数据不幸的合并成了一个。比如客户端向服务器发送两个命令:”Start”、”Parameter[x.x.x]”,第一个命令的含义是开始,第二个命令的含义是启动参数。但是服务器接收的时候,很可能不是分两次接收,而是一次接收到”StartParameter[x.x.x]”,这下全乱了。
造成粘包的原因有很多,大致就是TCP协议本身的缺陷或数据缓冲的问题。我也不是很懂,就不误导大家了。
小菜利用Socket传输图片时,想先发送一个初始化参数,这个参数大致就是说明图片名称、图片归属等信息。传输完成之后,服务器再向客户端发送图片的MD5值,在客户端校验图片信息是否完整,保证上传无误。思路如下图(一张图胜过千言万语):
但就是这么一个简单的过程,实现起来可真是困难重重,从上面说明可以看出,在传送图片之前要先传送命令,图片传完后又要传送命令,这就引来了伟大的粘包问题!命令和图片粘在一起!
从网上查到吐血,基本上都是回答自定义包结构,加上包头、包尾、错误重发等等。这些基于字节的操作,没有深厚的底层基础,是搞不定的,当然,我也搞不定,项目也没那么高的需求,果断放弃这种做法。
经过分析,发现粘包的主要原因是客户端连续向服务器发了三部分内容,导致数据混乱。既然是这样,就有了如下设计:
从上图可以看出,服务器收到初始化参数之后,先返回给客户端一个确认信息,然后客户端再传送图片,表面上看是麻烦了,但这避免了粘包问题,把命令和图片分离开,同时又增加了系统可靠性。
还可以发现,客户端没有向服务器发送结束命令,也就是说服务器要自己判断图片是否上传完成。怎么判断呢?小菜的思路是客户端获取文件的长度,作为初始化参数传给服务器,服务器根据接收的数据长度判断是否上传完成。
为什么要这样设计?因为服务器接收图片用的是一个阻塞循环,如果客户端不发送结束命令,这个循环将一直阻塞下去,但客户端一旦发送结束命令,就会和图片数据粘包。这个矛盾解不开。。。。
看下具体代码:
服务器核心代码(C#):
try { string removeMsg; SendBack sd = new SendBack(); skClient.ReceiveTimeout = 30; //设置接收超时,超时说明上传图片失败 //接收初始化数据(利用Receive的阻塞性等待初始化数据) receiveN = skClient.Receive(receiveData); //解析客户端消息 removeMsg = Encoding.UTF8.GetString(receiveData, 0, receiveN); //获取文件长度 long fileLength = Convert.ToInt64(removeMsg.Split(new char[] { '|' })[1]); //回发确认信息 sd.SendToClient(skClient, "T"); //写入图片处理 using (Stream pic = File.Create("E:\\" + removeMsg.Split(new char[] { '|' })[0])) { //临时长度变量 long tempLength = 0; //接收图片包(再次阻塞,接收图片) while ((receiveN = skClient.Receive(receiveData)) > 0)//接收 { tempLength += receiveN; //写入图片 pic.Write(receiveData, 0, receiveN); pic.Flush(); //判断文件是否接收完全 if (tempLength == fileLength) { //接收完全则退出循环 break; } } //释放文件流 pic.Close(); pic.Dispose(); } //回发图片MD5校验码 MD5Helper md5 = new MD5Helper(); sd.SendToClient(skClient, md5.md5_hash("E:\\" + removeMsg.Split(new char[] { '|' })[0])); } catch (SocketException se) { //关闭客户端连接 //超时有两种可能,一是发送数据包丢失,导致无法跳出循环而超时;二是网络或客户端异常。无论哪种情况,我们都有充分的理由断开连接,标志上传图片失败 skClient.Close(); skClient.Dispose(); } catch (Exception ex) { //异常掉线处理:得到掉线客户端的IP地址传递给接口实现类 iGetClientData.getClientIP(((IPEndPoint)skClient.RemoteEndPoint).Address + ex.ToString()); }
客户端核心代码(Java):
try { socket = new Socket(); socket.connect(new InetSocketAddress("192.168.24.177", 5522),10 * 1000); dos = new DataOutputStream(socket.getOutputStream()); File file = new File("D:\\1.jpg"); fis = new FileInputStream(file); sendBytes = new byte[1024]; /*发送初始化数据*/ String startMessage = "111111.jpg|" + file.length(); byte[] bytStartMessage = startMessage.getBytes("UTF-8"); dos.write(bytStartMessage,0,bytStartMessage.length); /*判断服务器是否收到初始化数据*/ String rtSingle = rsm.read(socket); if("T".equals(rtSingle)){ /*写入图片*/ while ((length = fis.read(sendBytes, 0, sendBytes.length)) > 0) { dos.write(sendBytes, 0, length); dos.flush(); } } /*发送结束信息*/ /*String endMessage = "End"; byte[] bytEndMessage = endMessage.getBytes("UTF-8"); dos.write(bytEndMessage,0,bytEndMessage.length);*/ /*获取本地图片的MD5校验码,转成大写形式*/ String localPicMD5 = MD5Helper.getFileMD5(file).toUpperCase(); /*接收回发的MD5校验码,转成大写形式*/ String backPicMD5 = rsm.read(socket).toUpperCase(); /*对比校验码,判断照片是否上传成功*/ if(localPicMD5.equals(backPicMD5)){ System.out.println("succes!"); }else{ System.out.println("fail!"); } } catch (SocketException se) { /*上传失败!*/ }catch(Exception e){ e.printStackTrace(); }finally { try{ if (dos != null) dos.close(); if (fis != null) fis.close(); if (socket != null) socket.close(); } catch (Exception e) { e.printStackTrace(); } }
通过代码相信读者能明白小菜的意思,服务器通过判断接收数据的总长度,主动用break跳出while循环,跳出循环后服务器才可以向客户端发送图片MD5校验码。
稍加思考,会发现这样设计有一个小问题!假设一旦网络出现问题,导致数据包丢失,就会造成服务器端接收到的图片数据小于实际的长度,这样一来就没办法跳出while循环,也就无法向客户端发送MD5校验码,导致客户端一直阻塞。
考虑到这个问题,小菜在代码中设置了Receive超时,服务器端一旦超过指定时间没有收到数据,依然是阻塞状态,那么就抛出异常,抛出异常后断开和客户端的连接,代表传送图片失败。因为在正常传输的情况下,不可能很长时间都收不到数据。如果超时,除了传输过程中数据包丢失无法跳出while,就是网络异常,无论是哪种情况,都可以认为本次传输失败。
好啦,就讲到这,小菜水平有限,望高手勿喷。
PS:
写Socket程序一定要时刻清醒:Receive(C#)、read(Java)等这样的方法都是阻塞的,也就是说,如果没有数据,线程会一直等待,程序会在这暂停,直到有消息到来。
如果是单纯传输文件,则不必考虑粘包问题,因为即使粘了,也无所谓,反正都是写入,只不过粘包后每次写入的数据长度可能不相等而已。
相关文章推荐
- Socket传输文件时进行校验(简单解决TCP粘包问题)
- Socket传输文件时进行校验(简单解决TCP粘包问题)
- socket简单实现ftp的文件传送(C++V2.0版,解决数据丢失问题)
- socket iOS 与java 后台进行socket通讯遇到的问题以及解决方法
- react最简单的新建方式--Create React App,(附文件路径问题解决的办法)
- 用socket实现简单的文件传输
- 解决Linux下多个进程或线程同时对一个文件进行写操作问题
- android c#.net使用socket进行局域网多文件传输
- 打通windows和Linux下的传输问题解决只能使用SSH协议下的22端口来传输文件(Openssh for windows)
- Linux下使用socket传输文件的C语言简单实现
- socket iOS 与java 后台进行socket通讯遇到的问题以及解决方法
- java和vb进行socket通信以及java与c/c++/oc 进行socket通信时汉字字符串编码问题解决
- 使用ajaxfileupload.js进行文件上传,出现的问题以及解决办法
- nginx lua redis解决saltstack下发传输文件慢的问题思路 推荐
- 简单的用socket传输文件程序。
- socket传输汉字截断为乱码问题的解决
- [毕业设计-基于android的手机网盘的设计与实现] java中文件的socket传输问题
- 使用TCP连续传输文件的问题解决
- 简单解决不同机器同帐号文件访问问题
- socket iOS 与java 后台进行socket通讯遇到的问题以及解决方法