UDP模拟TCP滑动窗口实现数据安全可靠传输(C#)
(本文网址:http://www.jellon.cn/index.php/archives/194)
Jellon 发表于 2009-11-27 16:23
最近需要实现P2P也就是需要做NAT穿透,原来写的TCP传输就出现问题了,因为TCP不能很好的实现内网的穿透,因此最好用UDP来实现传输。
可是UDP存在一些可靠性上的问题,主要是UDP是面向无连接的协议,传输中数据包丢失时没有重传,而且由于网络环境因素可能会出现数据包的乱序的情况。UDP的特点导致其不能方便的应用于需要保证数据可靠性的场合,比如文件传输等。现在一般P2P软件的做法应该是在应用层包装一下UDP协议,实现UDP的可靠传输。
在网上搜了下,本以为应该很多有现成的东西的,结果发现太少了。先搜到了一份Delphi版的,网址http://www.2ccc.com/article.asp?articleid=3154,可惜我基本不懂Delphi;然后还在sourceforge上发现一个叫UDT的开源项目,官方的网址是http://udt.sf.net,这个用C++写的,封装成了DLL,但研究了下觉得我用C#还是很难调用。最后决定自己动手写一个了,无非是模拟一下TCP嘛
此次实现采用了TCP的滑动窗口原理。在解释滑动窗口前先看看ACK的应答策略,一般来说,发送端发送一个TCP数据报,那么接收端就应该发送一个ACK数据报。但是事实上却不是这样,发送端将会连续发送数据尽量填满接受方的缓冲区,而接受方对这些数据只要发送一个ACK报文来回应就可以了,这就是ACK的累积特性,这个特性 大大减少了发送端和接收端的负担。滑动窗口本质上是描述接受方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接受方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接受方发送窗口大小不为0的数据报的到来。
原理基本上就是这样,在本程序里,接收放和发送方要事先约定好传输的每个包的数据大小及传输一组包的包数,也就是窗口大小。发送方发送一组数据后等待接收方的ACK确认,确认超时后重发,重发次数超过额定次数后视为连接中断。另外需要对每个数据包加了一个包的标识字节,我把这个字节放到了每个包的最后面,因此发送方发送给接收方的每个包含传输数据的包内容长度肯定大于等于2字节,接收方收到1个字节的包会丢弃,这样设计正好可以利用发送一个字节的包来保持连接不中断,可以应用在某些暂时不需要数据传输但需要保持此次连接以便随时可以继续传输的情况(比如我的程序里)。
然后说一个我的数据包标识字节的设计吧,一个byte共8bit,前四位(0-15)来表示组序号,中间三位表示组内包序号(0-9),最后一位是发送完毕标记
1234 567 8
|||| ||| |
组序号 组内包序号 发送完毕标记
接收端收到一个包后提取标识字节,如果组序号不是当前组序号则丢弃,否则根据组内包号记录该包,然后检查该组是否已经发送完毕,即窗口是否塞满,是的话发送ACK确认包(确认包内容为当前组序号),然后当前组序号=(当前组序号+1)%16。
基本就是这样了,测试了一下,本地传输的速度还行,传了个200M+的文件用了6.3秒。。。然后就是代码了,我把这个类封装成DLL来调用了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Net; using System.Net.Sockets; using System.IO; namespace Transfer { /******************************************************************************** Secured UDP Transfer Class -- UDP安全传输类 采用TCP滑动窗口原理在应用层解决UDP协议传输的丢包,乱序等问题,实现UDP可靠传输; 要求传入的UdpClient实例为已连接的实例,即调用此类的方法前请先执行初始化实例并连接; 发送接收的数据类采用Stream,可应用到其子类FileStream,MemoryStream等 ********************************************************************************/ public class SUdpTransfer { private const int defaultPacketSize = 513, defaultGroupSize = 7;//默认UDP包数据内容大小、组(窗口)大小 private const int confirmTimeOut = 1000;//确认超时时间 private const int maxResendCount = 4;//最大重发次数(超出则认为连接断开) private const int receiveTimeOut = 4000;//接收超时时间 private UdpClient client;//已连接的UdpClient实例 private AutoResetEvent confirmEvent = new AutoResetEvent(false);//等待确认回复用的事件 private int groupSeq;//当前传送的组序号 private Thread thListen;//发送端确认包接收线程 private bool error;//出错标志 public SUdpTransfer(UdpClient client) { this.client = client; } //数据发送函数,返回值0:发送成功,-1:发送失败 public int Send(Stream stream) { return Send(stream, defaultPacketSize, defaultGroupSize); } public int Send(Stream stream, int packetSize, int groupSize) { error = false; int dataSize = packetSize - 1; int i, read, readSum; byte flag = 0;//UDP包中的标志字节,包含组序号,包序号(即组内序号),发送结束标志 int resendCount = 0;//重发次数标记 try { //启动确认包接收线程 thListen = new Thread(new ThreadStart(Listen)); thListen.IsBackground = true; thListen.Start(); groupSeq = 0; stream.Seek(0, SeekOrigin.Begin); confirmEvent.Reset(); while (true) { if (error) return -1; readSum = 0;//记录读取字节数以便重发时Stream的回退 for (i = 0; i < groupSize; i++) { flag = (byte)(((byte)groupSeq) << 4 | (((byte)i) << 1)); byte[] buffer = new byte[packetSize]; read = stream.Read(buffer, 0, dataSize); readSum += read; if (read == dataSize) { if (stream.Position == stream.Length)//已经读完 { flag |= 0x01;//结束标记位置1 buffer[read] = flag;//数据包标志字节放于每个包的最后 client.Send(buffer, read + 1); break; } buffer[read] = flag; client.Send(buffer, read + 1); } else if (read > 0)//已经读完 { flag |= 0x01;//结束标记位置1 buffer[read] = flag;//数据包标志字节放于每个包的最后 client.Send(buffer, read + 1); break; } else { break; } } if (error) return -1; if (confirmEvent.WaitOne(confirmTimeOut))//收到确认 { if ((int)(flag & 0x01) == 1)//发送完毕 break; groupSeq = (groupSeq + 1) % 16; resendCount = 0; } else//未收到确认 { if (resendCount >= maxResendCount)//超出最大重发次数 { thListen.Abort(); return -1; } //重发 stream.Seek(-1 * readSum, SeekOrigin.Current); resendCount++; } } //发送完毕,关闭确认包接收线程 thListen.Abort(); } catch//异常 { thListen.Abort(); return -1; } return 0; } //确认包接收线程函数 private void Listen() { IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 0); try { while (true) { byte[] confirm = client.Receive(ref ipep); if ((int)confirm[0] == groupSeq)//收到确认 { confirmEvent.Set();//激活接收确认事件 } else if (confirm[0] == 0xFF)//传输中断命令 { error = true; break; } } } catch//异常 { error = true; } } //数据接收函数,返回值0:接收成功,-1:接收失败 public int Receive(ref Stream stream) { return Receive(ref stream, defaultPacketSize, defaultGroupSize); } public int Receive(ref Stream stream, int packetSize, int groupSize) { IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 0); int[] groupFlag = new int[groupSize]; byte[][] groupData = new byte[groupSize][]; byte flag;//UDP包中的标志字节,包含组序号,包序号(即组内序号),发送结束标志 int groupSeq, packetSeq, myGroupSeq; int dataSize = packetSize - 1; int i, endFlag, currentGroupSize; int socketRecieveTimeOut = client.Client.ReceiveTimeout;//保存原来的接收超时时间 try { client.Client.ReceiveTimeout = receiveTimeOut;//设置接收超时时间 currentGroupSize = groupSize; myGroupSeq = 0; while (true) { byte[] data = client.Receive(ref ipep); { if (data.Length < 2)//最小数据长度为2字节 { if (data.Length == 1 && data[0] == 0xFF)//传输中断命令 { client.Client.ReceiveTimeout = socketRecieveTimeOut;//恢复原来的接收超时时间 return -1; } continue; } flag = data[data.Length - 1];//数据包标志字节在每个数据包的最后 groupSeq = flag >> 4;//前四位:组序号 packetSeq = (flag & 0x0F) >> 1;//中间三位:包序号 endFlag = flag & 0x01;//最后一位:发送结束标记 if (groupSeq != myGroupSeq)//不属于希望接受的数据包组 { if ((groupSeq + 1) % 16 == myGroupSeq)//上一组回复的确认未收到 { byte[] confirmData = new byte[] { (byte)groupSeq }; client.Send(confirmData, confirmData.Length);//回复确认 } continue; } if (groupFlag[packetSeq] == 1)//已接收该包则丢弃 { continue; } groupFlag[packetSeq] = 1;//记录 groupData[packetSeq] = data;//暂存数据 if (endFlag == 1)//收到含结束标记的包 { currentGroupSize = packetSeq + 1;//改变当前组的窗口大小 } //检测是否该组包已全部接收 for (i = 0; i < currentGroupSize; i++) { if (groupFlag[i] == 0) break; } if (i == currentGroupSize)//已全部接收 { //写入数据 for (i = 0; i < currentGroupSize; i++) { stream.Write(groupData[i], 0, groupData[i].Length - 1); } byte[] confirmData = new byte[] { (byte)groupSeq }; client.Send(confirmData, confirmData.Length);//回复确认 client.Send(confirmData, confirmData.Length);//回复两次,确保收到 if (endFlag == 1)//已收到结束包则退出 { break; } myGroupSeq = (myGroupSeq + 1) % 16; currentGroupSize = groupSize; Array.Clear(groupFlag, 0, groupData.Length); } } } } catch//异常 { client.Client.ReceiveTimeout = socketRecieveTimeOut;//恢复原来的接收超时时间 return -1; } client.Client.ReceiveTimeout = socketRecieveTimeOut; return 0; } } }