转自:http://apps.hi.baidu.com/share/detail/31497239
所谓的断点续传就是指:文件在传输过程式中被中断后,在重新传输时,可以从上次的断点处开始传输,这样就可节省时间,和其它资源。
实现关键在这里有两个关键点:
其一是检测本地已经下载的文件长度和断点值;
其二是在服务端调整文件指针到断点处
实现方法:
我们用一个简单的方法来实现断点续传的功能:在传输文件的时候创建一个临时文件用来存放文件的断点位置
在每次发送文件时,先检查有没有临时文件;如果有的话,就从临时文件中读取断点值,并把文件指针移动到断点位置开始传输,这样便可以做到断点续传了。
实现流程:
首次传输其流程如下:
1. 服务端向客户端传递文件名称和文件长度;
2. 根据文件长度计算文件块数(文件分块传输请参照第二篇文章);
3. 客户端将传输的块数写入临时文件(做为断点值);
4. 若文件传输成功则删除临时文件;
首次传输失败后将按以下流程进行:
1. 客户端从临时文件读取断点值并发送给服务端;
2. 服务端与客户端将文件指针移至断点处;
3. 从断点处传输文件;
接收端和发送端的通信过程如下:
编码实现:
接收端代码如下:
//监听线程回调函数
private void lstnThreadProc()
{
Console.WriteLine("监听线程开始执行");
using (Socket lstnSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
lstnSock.Bind(m_lstnAddr);
lstnSock.Listen(m_lstnNum);
while(true)
{
Socket commSock = lstnSock.Accept();
Console.WriteLine("接收到连接请求,并建立连接");
Thread recvThread=new Thread(new ParameterizedThreadStart(recvThreadProc));
recvThread.SetApartmentState(ApartmentState.STA);
recvThread.Start(commSock);
}
}
每监听到一个连接,则创建一个新的线程来负责与发送端的通信和文件的传输
//接收线程
private void recvThreadProc(object argument)
{
//for check
Console.WriteLine("接收线程开始执行");
try
{
Socket commSock=(Socket)argument;
byte[] recvBuf = new byte[128];
int nRecv = commSock.Receive(recvBuf);
if (nRecv <= 0)
{
Console.WriteLine("接收到字节{0}", nRecv);
return;
}
//第一次接收到的信息为文件信息
TransFileInfo recvFileInfo = (TransFileInfo)StructTranslate.BytesToStruct(recvBuf, typeof(TransFileInfo));
//获取文件信息
string fileName = new string(recvFileInfo.fileName).TrimEnd('\0');
string fileExtension = new string(recvFileInfo.fileExtension).TrimEnd('\0');
long fileSize = int.Parse(new string(recvFileInfo.fileSize).TrimEnd('\0'));
Console.WriteLine("是否接收文件,信息如下:\n文件名:{0}\t文件长度:{1}\n", fileName, fileSize);
DialogResult result = MessageBox.Show("是否接收文件"+fileName, "白氏文件传输软件", MessageBoxButtons.YesNo);
//只有用户选择接收文件,才进行,否则直接断开连接
if (result == DialogResult.Yes)
{
//读取传输断点值
int nBlockNum = readTransPos(fileName);
byte[] sendBuf = BitConverter.GetBytes(nBlockNum);
//发送给对方断点值
commSock.Send(sendBuf);
//接收文件
//弹出选择保存路径的对话框
SaveFileDialog saveFileDlg = new SaveFileDialog();
saveFileDlg.FileName = fileName;
saveFileDlg.Filter = string.Format("默认类型|*{0}|所有文件(*.*)|*.*", fileExtension);
if (saveFileDlg.ShowDialog() != DialogResult.OK)
{
//关闭套接字,并返回
commSock.Close();
return;
}
//保存路径
string fileFullName = saveFileDlg.FileName;
recvFile(commSock, fileFullName, fileSize, nBlockNum);
//断开连接
commSock.Close();
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
接收线程负责与发送端通信:发送端发送来文件信息,在进行解析后,在XML文件中读取是否有其临时文件,以读取已传输的块数。通信过程结束后,开始接收文件
//具体的接收过程
private void recvFile(Socket recvSock, string fileFullName, long fileSize, int nBlockNum)
{
//打开文件,并指到传输位置
using (FileStream writeFile = new FileStream(fileFullName, FileMode.OpenOrCreate, FileAccess.Write))
{
int nOffset = nBlockNum * m_nBlockSize;
writeFile.Seek(nOffset, 0);
//接收传输过来的文件
byte[] recvBuf = new byte[m_nBlockSize];
int nRecv = 0;
while (true)
{
try
{
nRecv = recvSock.Receive(recvBuf);
writeFile.Write(recvBuf, 0, nRecv);
//如果接收到的数据不够缓存大小,表示传输结束
if (nRecv <= 0)
{
RecvProgressEvent(100);
writeTransPos(fileFullName, fileSize, nBlockNum);
writeFile.Close();
Console.WriteLine("文件{0}传输完成", fileFullName);
return;
}
nBlockNum++;
RecvProgressEvent((int)(nBlockNum*m_nBlockSize/fileSize)*100);
}
catch (Exception e)
{
//写到外存中
writeTransPos(fileFullName, nBlockNum);
writeFile.Close();
Console.WriteLine("传输未完成,错误信息为:" + e.Message);
return;
}
}
}
}
发送端代码如下:
//创建一个线程,用于发送文件
public void sendThreadProc(object argument)
{
string fileFullName = (string)argument;
//获取文件信息
FileInfo sendFileInfo = new FileInfo(fileFullName);
//for check
Console.WriteLine("所选择文件信息如下:\n文件名:{0}\t文件扩展名:{1}\t文件大小(字节数):{2}", sendFileInfo.Name, sendFileInfo.Extension, sendFileInfo.Length);
using(Socket commSock=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
//与对方建立连接
commSock.Connect(m_remoteAddr);
Console.WriteLine("成功与接收方连接");
//发送文件信息(文件名,扩展名,文件大小)
TransFileInfo fileInfo = new TransFileInfo(sendFileInfo.Name, sendFileInfo.Extension, sendFileInfo.Length);
byte[] sendBuf = StructTranslate.StructToBytes((object)fileInfo);
commSock.Send(sendBuf);
//接收已传输的文件块个数
byte[] recvBuf = new byte[128];
commSock.Receive(recvBuf);
int nBlockNum = BitConverter.ToInt32(recvBuf, 0);
//只负责文件的发送
SendFile(commSock, fileFullName, sendFileInfo.Length, nBlockNum);
commSock.Close();
}
}
每发送一个文件,就创建一个线程,用于与接收端通信(发送文件信息,接收已传输的块数),以及发送文件
//具体的传送函数(参数分别为:文件名(包含绝对路径名),已传块数)
public void SendFile(Socket sendSock, string fileFullName, long fileSize, int nBlockNum)
{
//打开文件,并指到已传输的位置的下一处
using (FileStream readFile = new FileStream(fileFullName, FileMode.Open, FileAccess.Read))
{
int nOffset = m_nBlockSize * nBlockNum;
readFile.Seek(nOffset, 0);
byte[] sendBuf = new byte[m_nBlockSize];
while (true)
{
//从文件中读取到发送缓存中
int nRead = readFile.Read(sendBuf, 0, m_nBlockSize);
//发送数据
int nSend = sendSock.Send(sendBuf, nRead, SocketFlags.None);
if (nRead <= 0)
{
SendProgressEvent(100);
Console.WriteLine("传输完毕");
readFile.Close();
break;
}
SendProgressEvent((int)((nOffset+nSend)*100/fileSize));
}
}
}
注意:
(1) 在while(true)循环中,不能用nSend<m_nBlockSize来判断是否终结。因为sendBuf[ ]是m_nBlockSize大小的字节数组,所以发送的时候发送了m_nBlockSize,即nSend=m_nBlockSize
(2) sendSock.send()方法中要注意,如果直接使用Send(sendBuf); 的话,会使sendBuf[]中不足m_nBlockSize的部分用'\0'来补充,这些内容也会发送给接收端,从而导致一些莫名的问题。