您的位置:首页 > 理论基础 > 计算机网络

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#)

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

  如果是单纯传输文件,则不必考虑粘包问题,因为即使粘了,也无所谓,反正都是写入,只不过粘包后每次写入的数据长度可能不相等而已。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: