用SOCKET发送文件是一个不太好处理的问题,网上的例子也都是很简单的,我准备写一个比较完善的例子,这个就算是开始吧,以后的都会在这个例子的基础上进行修改,准备实现多线程传输、断点传输、穿越防火墙和文件传输的完备性检测。
例子中,分别定义了文件发送管理类(SendFileManager),文件接收管理类(ReceiveFileManager),文件发送类(UdpSendFile)和文件接收类(UdpRecieveFile),以便实现尽量简单的就可以使用它们。在文件发送类(UdpSendFile)和文件接收类(UdpRecieveFile)中都是直接用(UdpClient)来进行发送和接收(UdpClient的使用MSDNDemo详见后文)。在通信协议的设计中为了提供以后对断点续传的支持,以及网络上准确的传输文件,文件传输方式采用了问答方式,即发送方先发送一段信息,等待接收方的应答信息,只有得到接收方的应答信号后才继续发送下一段信息。由于是第一个例子,所以实际的通信协议定的很简单被封装到了(SendCell)类中,包含消息标识符和传输的数据内容。数据内容被划分为两种情况,一种分是(TraFransfersFileStart)文件传输前的文件头,包含文件的名,大小,分块数,分块大小基本信息;另一种分是(TraFransfersFile)传输文件的具体内容,包含一个索引号(原子操作)即这是第几快,和文件块。当然序列化与反序列化是必不可少的内容,交由(BufferHelper)来处理不再多说。例子中的发送和读写文件都是基于异步的(异步读取文件时要用到原子操作,详见后文),以免同步情况下阻塞造成的麻烦,实现了对大文件的分块发送。为了方便发送端读取本地要发送的文件,在收到接收方应答信号后能立即给予回复,触发事件的方法就放在接收到信号的方法内部,例子中添加了一个事件类(ReadFileBufferEvent)以及自定义的委托参数类型(ReadFileBufferEventArgs)格式与封包类(SendCell)一致。例子中还提供了一个发送文件端和接收文件端,都是用前面的几个类实现了文件的发送和接受。接收的文件默认放在接受文件端得根目录下,文件名以下划线开始。
虽然在这个例子中实现了文件的基本传输,但是传输过程中的信息是看不到的,下一篇会对此进行了一些改进,并且可以了解传输的信息(加入了Log),还将加接收或者拒绝接收文件功能。
MSDN示例UdpClient的使用
下面的示例在端口 11000上使用主机名 www.contoso.com建立 UdpClient连接。 将很短的字符串消息发送到两个单独的远程主机。 Receive 方法在接收消息前阻止执行。 使用传递给 Receive的 IPEndPoint可以显示响应主机的标识。
// This constructorarbitrarily assigns the local port number.
UdpClient udpClient =new UdpClient(11000);
try{
udpClient.Connect("www.contoso.com", 11000);
// Sends a message to the host towhich you have connected.
Byte[] sendBytes =Encoding.ASCII.GetBytes("Is anybody there?");
udpClient.Send(sendBytes,sendBytes.Length);
// Sends a message to a differenthost using optional hostname and port parameters.
UdpClient udpClientB =new UdpClient();
udpClientB.Send(sendBytes,sendBytes.Length,"AlternateHostMachineName", 11000);
//IPEndPoint object will allow usto read datagrams sent from any source.
IPEndPoint RemoteIpEndPoint =newIPEndPoint(IPAddress.Any, 0);
// Blocks until a message returnson this socket from a remote host.
Byte[] receiveBytes =udpClient.Receive(ref RemoteIpEndPoint);
string returnData =Encoding.ASCII.GetString(receiveBytes);
// Uses the IPEndPoint object todetermine which of these two hosts responded.
Console.WriteLine("This is themessage you received " +
returnData.ToString());
Console.WriteLine("This messagewas sent from " +
RemoteIpEndPoint.Address.ToString() +
" on theirport number " +
RemoteIpEndPoint.Port.ToString());
MSDN示例Interlocked的说明
此类的方法可以防止可能在下列情况发生的错误:计划程序在某个线程正在更新可由其他线程访问的变量时切换上下文;或者当两个线程在不同的处理器上并发执行时。此类的成员不引发异常。
Increment 和Decrement 方法递增或递减变量并将结果值存储在单个操作中。 在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1. 将实例变量中的值加载到寄存器中。
2. 增加或减少该值。
3. 在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。 然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。
Exchange 方法自动交换指定变量的值。CompareExchange 方法组合了两个操作:比较两个值以及根据比较的结果将第三个值存储在其中一个变量中。 比较和交换操作按原子操作执行。
本程序中发送端对文件的异步读操作极有可能使得对文件索引号Index的错误操作,即被挂起的读线程未及时完成Index自增而被下一个文件读线程覆盖,造成分块数编号错误,接收端无法收到正确的文件。
代码展示:
UdpSendFile.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;
namespace CSharpWin
{
/*文件发送类
* 具体实现数据的发送与接收
*/
public class UdpSendFile
{
private UdpClient _udpClient;
private string _remoteIP = "127.0.0.1";
private int _remotePort = 8900;
private int _port = 8899; //本机端口号
private bool _started; //是否启动监听标志
private string _fileName; //要发送的文件名
SendFileManager _sendFileManage; //声明文件发送管理类
//构造函数
public UdpSendFile(
string remoteIP,
int remotePort,
int port)
{
_remoteIP = remoteIP;
_remotePort = remotePort;
_port = port;
}
//传入要发送的文件名,启动文件发送方法
public string FileName
{
get { return _fileName; }
set
{
_fileName = value;
//释放先前实例化对象
if (_sendFileManage != null)
{
_sendFileManage.Dispose();
}
_sendFileManage = new SendFileManager(_fileName);
//订阅事件,将委托添加到源对象的事件中
//委托目标即事件处理函数为SendFileManageReadFileBuffer
_sendFileManage.ReadFileBuffer += new ReadFileBufferEventHandler(
SendFileManageReadFileBuffer);
//初始化文件头
TraFransfersFileStart ts = new TraFransfersFileStart(
new FileInfo(_fileName).Name,
_sendFileManage.Length,
_sendFileManage.PartCount,
_sendFileManage.PartSize);
//发送第一包数据,即文件的信息数据
Send(0, ts);
}
}
//设置只读属性
public UdpClient UdpClient
{
get
{
if (_udpClient == null)
{
//实用本地端口号建立UdpClient
_udpClient = new UdpClient(_port);
//用于正常关闭UDP连接,防止异常终止
//Convert.ToByte(false),null字段为固定值
uint IOC_IN = 0x80000000;
uint IOC_VENDOR = 0x18000000;
uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
UdpClient.Client.IOControl(
(int)SIO_UDP_CONNRESET,
new byte[] { Convert.ToByte(false) },
null);
}
return _udpClient;
}
}
//设置远程主机
public IPEndPoint RemoteEP
{
get { return new IPEndPoint(IPAddress.Parse(_remoteIP), _remotePort); }
}
//启动监听
public void Start()
{
if (!_started)
{
_started = true;
//创建异步监听,从远程主机异步接收数据
UdpClient.BeginReceive(
new AsyncCallback(ReceiveCallback),
null);
}
}
//读取文件缓冲事件处理函数
private void SendFileManageReadFileBuffer(
object sender, ReadFileBufferEventArgs e)
{
//
TraFransfersFile ts = new TraFransfersFile(
e.Index, e.Buffer);
//发送文件真实数据
Send(1, ts);
}
//数据发送函数
public void Send(int messageID, object data, IPEndPoint remoteIP)
{
//打包要发送的数据到buffer
SendCell cell = new SendCell(messageID, data);
byte[] buffer = cell.ToBuffer();
//将数据异步发送
UdpClient.BeginSend(
buffer,
buffer.Length,
remoteIP,
new AsyncCallback(SendCallback),
null);
}
//调用上面的发送函数
public void Send(int messageID, object data)
{
Send(messageID, data, RemoteEP);
}
//结束挂起的异步发送
private void SendCallback(IAsyncResult result)
{
UdpClient.EndSend(result);
}
//异步监听委托对象,接收到消息后处理方法
private void ReceiveCallback(IAsyncResult result)
{
//设置远程主机连接,接收来自任意主机端口号的数据
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any,0);
//结束挂起的异步接收线程,引用的remoteEP包含发送方IP,Port
byte[] buffer = UdpClient.EndReceive(result, ref remoteEP);
//启动新的异步接收,循环往复
UdpClient.BeginReceive(
new AsyncCallback(ReceiveCallback),
null);
//解析收到的数据
SendCell cell = new SendCell();
cell.FromBuffer(buffer);
switch (cell.MessageID)
{
//消息标识符MessageID若为0,1
//则继续读取文件发送
//若为2则,释放文件发送管理类
case 0:
case 1:
//启动文件发送管理类的读文件流方法
_sendFileManage.Read();
break;
case 2:
//释放文件发送管理类
_sendFileManage.Dispose();
break;
}
}
}
}
SendFileManager.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading; //用到Interlocked,为多个线程共享的变量提供原子操作
namespace CSharpWin
{
/* 文件发送管理类
* 创建文件流,定义数据包大小,块数,文件长度等
*/
//定义一种释放分配资源的方法
public class SendFileManager : IDisposable
{
private FileStream _fileStream;
private long _partCount; //总块数
private long _length; //文件总大小
private int _partSize = 1024 * 5; //文件块大小
private int _index = 0; //文件块索引号,初始化为0
public SendFileManager(string fileName)
{
//创建要发送的文件信息
Create(fileName);
}
public SendFileManager(string fileName, int partSize)
{
_partSize = partSize;
Create(fileName);
}
//读取文件缓冲事件声明
public event ReadFileBufferEventHandler ReadFileBuffer;
public long PartCount
{
get { return _partCount; }
}
public long Length
{
get { return _length; }
}
public int PartSize
{
get { return _partSize; }
}
internal FileStream FileStream
{
get { return _fileStream; }
}
//创建要发送的文件信息
private void Create(string fileName)
{
//异步方式创建文件流,缓冲区大小_partSize
_fileStream = new FileStream(
fileName,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
_partSize,
true);
//文件总长度
_length = _fileStream.Length;
//计算总分块数
_partCount = _length / _partSize;
if (_length % _partSize != 0)
{
_partCount++;
}
}
//文件发送管理类的读文件流方法
public void Read(int index)
{
int size = _partSize;
if (Length - _partSize * index < _partSize)
{
size = (int)(Length - _partSize * index);
}
byte[] buffer = new byte[size];
//自定义的一个与TraFransfersFile类相对应的类
ReadFileObject obj = new ReadFileObject(index, buffer);
//定位文件读取位置
FileStream.Position = index * _partSize;
//实现异步读取文件
FileStream.BeginRead(
buffer,
0,
size,
new AsyncCallback(EndRead),
obj);
}
//调用上面的Read方法
public void Read()
{
/*Increment 和 Decrement 方法递增或递减变量并将结果值存储在单个操作中。
在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
1.
将实例变量中的值加载到寄存器中。
2.
增加或减少该值。
3.
在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。
然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,
它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。*/
//添加自由锁,以原子操作的形式递增指定变量的值并存储结果
int index = Interlocked.Increment(ref _index);
Read(index - 1);
}
//异步读取文件流
private void EndRead(IAsyncResult result)
{
//结束挂起的异步读取文件流,返回读取到的大小length
int length = FileStream.EndRead(result);
ReadFileObject state = (ReadFileObject)result.AsyncState;
//获得读取到的文件块索引号
int index = state.Index;
//将读取的文件装入缓冲区
byte[] buffer = state.Buffer;
//重置事件参数
ReadFileBufferEventArgs e = null;
//若读到的数据小于分块数,则为最后一包数据
if (length < _partSize)
{
//把实际读取到的数据复制到realBuffer
byte[] realBuffer = new byte[length];
Buffer.BlockCopy(buffer, 0, realBuffer, 0, length);
//实例化事件参数
e = new ReadFileBufferEventArgs(index, realBuffer);
}
//否则直接发送定义好的数据块
else
{
//实例化事件参数
e = new ReadFileBufferEventArgs(index, buffer);
}
//触发事件ReadFileBuffer
OnReadFileBuffer(e);
}
//接收到消息后触发的事件,即调用SendFileManageReadFileBuffer发送数据
protected void OnReadFileBuffer(ReadFileBufferEventArgs e)
{
if (ReadFileBuffer != null)
{
ReadFileBuffer(this, e);
}
}
//释放分配的资源
#region IDisposable 成员
public void Dispose()
{
if (_fileStream != null)
{
_fileStream.Close();
_fileStream.Dispose();
_fileStream = null;
}
}
#endregion
}
}
ReadFileBufferEventHandler.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpWin
{
/* 声明一个读取文件事件的委托
*/
//声明委托
public delegate void ReadFileBufferEventHandler(
object sender,
ReadFileBufferEventArgs e);
//定义委托事件的参数类型,继承自system.EventArgs
public class ReadFileBufferEventArgs : EventArgs
{
private int _index;
private byte[] _buffer;
//构造函数
public ReadFileBufferEventArgs(int index, byte[] buffer)
: base()
{
_index = index;
_buffer = buffer;
}
public int Index
{
get { return _index; }
}
public byte[] Buffer
{
get { return _buffer; }
}
}
}
UdpRecieveFile.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;
namespace CSharpWin
{
/* 文件接收类
* 具体实现数据的发送与接收
*/
public class UdpRecieveFile
{
private UdpClient _udpClient;
private int _port = 8900;
private bool _started; //是否启动监听标志
private string _path; //文件存储位置
private ReceiveFileManager _receiveFileManager;
//构造函数
public UdpRecieveFile(string path, int port)
{
_path = path;
_port = port;
}
//设置只读属性
public UdpClient UdpClient
{
get
{
if (_udpClient == null)
{
_udpClient = new UdpClient(_port);
//用于正常关闭UDP连接,防止异常终止
//Convert.ToByte(false),null字段为固定值
uint IOC_IN = 0x80000000;
uint IOC_VENDOR = 0x18000000;
uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12;
UdpClient.Client.IOControl(
(int)SIO_UDP_CONNRESET,
new byte[] { Convert.ToByte(false) },
null);
}
return _udpClient;
}
}
//启动监听
public void Start()
{
if (!_started)
{
//创建异步监听,从远程主机异步接收数据
UdpClient.BeginReceive(
new AsyncCallback(ReceiveCallback),
null);
_started = true;
}
}
//数据发送函数
public void Send(int messageID, object data, IPEndPoint remoteIP)
{
SendCell cell = new SendCell(messageID, data);
byte[] buffer = cell.ToBuffer();
UdpClient.BeginSend(
buffer,
buffer.Length,
remoteIP,
new AsyncCallback(SendCallback),
null);
}
//结束挂起的异步发送
private void SendCallback(IAsyncResult result)
{
UdpClient.EndSend(result);
}
//异步监听委托对象,接收到消息后处理方法
private void ReceiveCallback(IAsyncResult result)
{
//设置远程主机连接,接收来自任意主机端口号的数据
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
//结束挂起的异步接收线程,引用的remoteEP包含发送方IP,Port
byte[] buffer = UdpClient.EndReceive(result, ref remoteEP);
//启动新的异步接收,循环往复
UdpClient.BeginReceive(
new AsyncCallback(ReceiveCallback),
null);
//解析收到的数据
SendCell cell = new SendCell();
cell.FromBuffer(buffer);
switch (cell.MessageID)
{
//消息标识符MessageID若为0
//则向发送方通知开始发送数据
//消息标识符MessageID若为
//则向发送方通知继续发送数据
case 0:
OnStartRecieve((TraFransfersFileStart)cell.Data, remoteEP);
break;
case 1:
OnRecieveBuffer((TraFransfersFile)cell.Data, remoteEP);
break;
}
}
//接收到发送方数据后处理接收数据
private void OnRecieveBuffer(
TraFransfersFile traFransfersFile,
IPEndPoint remoteEP)
{
_receiveFileManager.ReceiveBuffer(
traFransfersFile.Index,
traFransfersFile.Buffer);
if (_receiveFileManager.PartCount == traFransfersFile.Index + 1)
{
Send(2, "OK", remoteEP);
}
else
{
Send(1, "OK", remoteEP);
}
}
//向发送方通知开始发送数据
private void OnStartRecieve(
TraFransfersFileStart traFransfersFileStart,
IPEndPoint remoteEP)
{
_receiveFileManager = new ReceiveFileManager(
_path,
traFransfersFileStart.FileName,
traFransfersFileStart.PartCount,
traFransfersFileStart.PartSize,
traFransfersFileStart.Length);
Send(0, "OK", remoteEP);
}
}
}
ReceiveFileManager.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace CSharpWin
{
/* 文件接收管理类
* 创建文件流,定义数据包大小,块数,文件长度等
*/
//定义一种释放分配资源的方法
public class ReceiveFileManager : IDisposable
{
private string _path; //文件存储位置
private string _tempFileName; //接收文件路径名
private string _fileName;
private long _partCount;
private int _partSize;
private long _length;
private FileStream _fileStream;
//将创建一个线程安全的同步流以写入接收数据
private Stream _syncStream;
private int _interval = 5000;
//记录上次接收到的时间
private DateTime _lastReceiveTime;
public ReceiveFileManager(
string path,
string fileName,
long partCount,
int partSize,
long length)
{
_path = path;
_fileName = fileName;
_partCount = partCount;
_partSize = partSize;
_length = length;
//创建要接收的文件信息
Create();
}
public long PartCount
{
get { return _partCount; }
}
//SyncStream属性只能在ReceiveFileManager类中使用
internal Stream SyncStream
{
get { return _syncStream; }
}
//创建要发送的文件信息
private void Create()
{
//接收文件名以下划线开头
_tempFileName = string.Format("{0}\\_{1}", _path, _fileName);
//异步方式创建文件流,缓冲区大小_partSize
_fileStream = new FileStream(
_tempFileName,
FileMode.OpenOrCreate,
FileAccess.Write,
FileShare.None,
_partSize,
true);
/*此方法返回一个类,该类包装指定的 Stream 对象并限制从多个线程访问它
对 Stream 对象的所有访问都是线程安全的*/
//在指定的 Stream 对象周围创建线程安全同步包装
_syncStream = Stream.Synchronized(_fileStream);
//记录文件创建的时间
_lastReceiveTime = DateTime.Now;
}
//接收到发送方数据后处理接收数据
public void ReceiveBuffer(int index, byte[] buffer)
{
_fileStream.Position = index * _partSize;
_fileStream.BeginWrite(
buffer,
0,
buffer.Length,
new AsyncCallback(EndWrite),
index);
//记录上次接收到的时间
_lastReceiveTime = DateTime.Now;
}
//结束挂起的异步写操作,并判断是否接收完成
private void EndWrite(IAsyncResult result)
{
_fileStream.EndWrite(result);
int index = (int)result.AsyncState;
if (index == _partCount - 1)
{
Dispose();
}
}
//释放分配的资源
#region IDisposable 成员
public void Dispose()
{
_fileStream.Flush();
_fileStream.Close();
_fileStream.Dispose();
_fileStream = null;
}
#endregion
}
}
SendCell.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpWin
{
/* 用于封装发送的数据
* MessageID用于标识发送的数据类型
* 0表示数据头,
* 1表示数据
* 2表示最后一包数据
* 数据包内容Data为TraFransfersFile或TraFransfersFileStart
*/
[Serializable]
public class SendCell
{
private int _messageID;
public int MessageID
{
get { return _messageID; }
set { _messageID = value; }
}
private object _data;
public object Data
{
get { return _data; }
set { _data = value; }
}
public SendCell() { }
public SendCell(
int messageID,
object data)
{
_messageID = messageID;
_data = data;
}
//打包数据,定义数据类型
public byte[] ToBuffer()
{
byte[] data = BufferHelper.Serialize(_data);
//显式转换,将基础数据类型与字节数组相互转换
//以字节数组的形式返回指定的 32 位有符号整数值
byte[] id = BitConverter.GetBytes(MessageID);
byte[] buffer = new byte[data.Length + id.Length];
Buffer.BlockCopy(id, 0, buffer, 0, id.Length);
Buffer.BlockCopy(data, 0, buffer, id.Length, data.Length);
return buffer;
}
//解包数据,解析数据类型
public void FromBuffer(byte[] buffer)
{
//从字节数组起始位置返回一个int型数据
_messageID = BitConverter.ToInt32(buffer, 0);
//int型的MessageID占用4字节
_data = BufferHelper.Deserialize(buffer, 4);
}
}
}
BufferHelper.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
namespace CSharpWin
{
/* 实现对数据的序列化与反序列化方法
*/
public class BufferHelper
{
public static byte[] Serialize(object obj)
{
BinaryFormatter bf = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
bf.Serialize(stream, obj);
byte[] datas = stream.ToArray();
stream.Dispose();
return datas;
}
public static object Deserialize(byte[] datas, int index)
{
BinaryFormatter bf = new BinaryFormatter();
//从内存流指定起始位置返回指定长度
MemoryStream stream = new MemoryStream(datas, index, datas.Length - index);
object obj = bf.Deserialize(stream);
stream.Dispose();
return obj;
}
}
}
ReadFileObject.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpWin
{
/*
自定义的一个与TraFransfersFile类相对应的类
*/
internal class ReadFileObject
{
private int _index;
private byte[] _buffer;
public ReadFileObject(int index, byte[] buffer)
{
_index = index;
_buffer = buffer;
}
public int Index
{
get { return _index; }
set { _index = value; }
}
public byte[] Buffer
{
get { return _buffer; }
set { _buffer = value; }
}
}
}