源码 (为节省空间,不包含通信框架源码,通信框架源码请另行下载)
文件传送在TCP通信中是经常用到的,本文针对文件传送进行探讨
经过测试,可以发送比较大的文件,比如1个G或者2个G
本文只对文件传送做了简单的探讨,示例程序可能也不是很成熟,希望本文起到抛砖引玉的作用,有兴趣的朋友帮忙补充完善
首先看一下实现的效果
服务器端:
客户端(一次只能发送一个文件):
服务器端收到的文件,存放到了D盘根目录下(存放的路径可以根据情况修改)
本程序基于开源的networkcomms2.3.1通信框架
下面来看一下实现的步骤:
1、客户端
(1): 先连接服务器:
//给连接信息对象赋值 connInfo = new ConnectionInfo(txtIP.Text, int.Parse(txtPort.Text)); //如果不成功,会弹出异常信息 newTcpConnection = TCPConnection.GetConnection(connInfo); TCPConnection.StartListening(connInfo.LocalEndPoint); button1.Enabled = false; button1.Text = "连接成功";
(2)发送大文件(分段发送)
private void SendFileButton_Click(object sender, EventArgs e) { //打开对话框,获取文件 if (openFileDialog1.ShowDialog() == DialogResult.OK) { //暂时禁用发送按钮 sendFileButton.Enabled = false; //获取对话框中选择的文件的名称 string filename = openFileDialog1.FileName; //设置进度条显示为0 UpdateSendProgress(0); try { //创建一个文件流 FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read); //创建一个线程安全流 ThreadSafeStream safeStream = new ThreadSafeStream(stream); //获取不包含路径的文件名称 string shortFileName = System.IO.Path.GetFileName(filename); //每次发送的字节数 可根据实际情况进行设定 long sendChunkSizeBytes = 40960; //已发送的字节数 long totalBytesSent = 0; do { //检查剩余的字节数 小于 上面指定的字节数 则发送"剩余的字节数" 否则发送"指定的字节数" long bytesToSend = (totalBytesSent + sendChunkSizeBytes < stream.Length ? sendChunkSizeBytes : stream.Length - totalBytesSent); //包装一个ThreadSafeStream 使之可以分段发送 StreamSendWrapper streamWrapper = new StreamSendWrapper(safeStream, totalBytesSent, bytesToSend); //顺序号 long packetSequenceNumber; //发送指定数据 newTcpConnection.SendObject("PartialFileData", streamWrapper, customOptions, out packetSequenceNumber); //发送指定的数据相关的信息 newTcpConnection.SendObject("PartialFileDataInfo", new SendInfo(shortFileName, stream.Length, totalBytesSent, packetSequenceNumber), customOptions); totalBytesSent += bytesToSend; UpdateSendProgress((double)totalBytesSent / stream.Length); //两次发送之间间隔一定时间 System.Threading.Thread.Sleep(30); } while (totalBytesSent < stream.Length); } catch (CommunicationException) { } catch (Exception ex) { NetworkComms.LogError(ex, "SendFileError"); } } }
2:服务器端接收文件:
(1)开始监听
//服务器开始监听客户端的请求 //开始监听某T端口 IPEndPoint thePoint = new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text)); TCPConnection.StartListening(thePoint, false); button1.Text = "监听中"; button1.Enabled = false; //此方法中包含服务器具体的处理方法。 StartListening();
(2)添加接收文件处理方法
//处理收到的文件字节数据 NetworkComms.AppendGlobalIncomingPacketHandler<byte[]>("PartialFileData", IncomingPartialFileData); //处理收到的文件信息数据 NetworkComms.AppendGlobalIncomingPacketHandler<SendInfo>("PartialFileDataInfo", IncomingPartialFileDataInfo);
//处理收到的文件字节数据 private void IncomingPartialFileData(PacketHeader header, Connection connection, byte[] data) { try { SendInfo info = null; ReceivedFile file = null; lock (syncRoot) { //获取顺序号 long sequenceNumber = header.GetOption(PacketHeaderLongItems.PacketSequenceNumber); if (incomingDataInfoCache.ContainsKey(connection.ConnectionInfo) && incomingDataInfoCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //如果已经收到此部分 “文件字节数据” 对应的 “文件信息数据” info = incomingDataInfoCache[connection.ConnectionInfo][sequenceNumber]; incomingDataInfoCache[connection.ConnectionInfo].Remove(sequenceNumber); if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo)) receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>()); //如果当前收到字节数据,还没有对应的ReceivedFile类,则创建一个 if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename)) { receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes)); } file = receivedFilesDict[connection.ConnectionInfo][info.Filename]; } else { if (!incomingDataCache.ContainsKey(connection.ConnectionInfo)) incomingDataCache.Add(connection.ConnectionInfo, new Dictionary<long, byte[]>()); incomingDataCache[connection.ConnectionInfo].Add(sequenceNumber, data); } } if (info != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); file = null; data = null; } else if (info == null ^ file == null) throw new Exception("Either both are null or both are set. Info is " + (info == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed.")); } catch (Exception ex) { NetworkComms.LogError(ex, "IncomingPartialFileDataError"); } }
//处理收到的文件信息数据 private void IncomingPartialFileDataInfo(PacketHeader header, Connection connection, SendInfo info) { try { byte[] data = null; ReceivedFile file = null; lock (syncRoot) { //获取顺序号 long sequenceNumber = info.PacketSequenceNumber; if (incomingDataCache.ContainsKey(connection.ConnectionInfo) && incomingDataCache[connection.ConnectionInfo].ContainsKey(sequenceNumber)) { //如果当前文件信息类对应的文件字节部分已经存在 data = incomingDataCache[connection.ConnectionInfo][sequenceNumber]; incomingDataCache[connection.ConnectionInfo].Remove(sequenceNumber); if (!receivedFilesDict.ContainsKey(connection.ConnectionInfo)) receivedFilesDict.Add(connection.ConnectionInfo, new Dictionary<string, ReceivedFile>()); if (!receivedFilesDict[connection.ConnectionInfo].ContainsKey(info.Filename)) { receivedFilesDict[connection.ConnectionInfo].Add(info.Filename, new ReceivedFile(info.Filename, connection.ConnectionInfo, info.TotalBytes)); } file = receivedFilesDict[connection.ConnectionInfo][info.Filename]; } else { if (!incomingDataInfoCache.ContainsKey(connection.ConnectionInfo)) incomingDataInfoCache.Add(connection.ConnectionInfo, new Dictionary<long, SendInfo>()); incomingDataInfoCache[connection.ConnectionInfo].Add(sequenceNumber, info); } } if (data != null && file != null && !file.IsCompleted) { file.AddData(info.BytesStart, 0, data.Length, data); file = null; data = null; } else if (data == null ^ file == null) throw new Exception("Either both are null or both are set. Data is " + (data == null ? "null." : "set.") + " File is " + (file == null ? "null." : "set.") + " File is " + (file.IsCompleted ? "completed." : "not completed.")); } catch (Exception ex) { NetworkComms.LogError(ex, "IncomingPartialFileDataInfo"); } }
Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>> receivedFilesDict = new Dictionary<ConnectionInfo, Dictionary<string, ReceivedFile>>(); //临时存放收到的文件字节数据 Dictionary<ConnectionInfo, Dictionary<long, byte[]>> incomingDataCache = new Dictionary<ConnectionInfo, Dictionary<long, byte[]>>(); //临时存放收到的文件信息数据 Dictionary<ConnectionInfo, Dictionary<long, SendInfo>> incomingDataInfoCache = new Dictionary<ConnectionInfo, Dictionary<long, SendInfo>>(); //收发参数 SendReceiveOptions customOptions = new SendReceiveOptions<ProtobufSerializer>(); object syncRoot = new object();
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NetworkCommsDotNet; using System.ComponentModel; using System.IO; namespace AppServer { /// <summary> ///接收文件 /// </summary> public class ReceivedFile { /// <summary> /// 文件名称 /// </summary> public string Filename { get; private set; } /// <summary> /// 连接信息列 /// </summary> public ConnectionInfo SourceInfo { get; private set; } /// <summary> /// 文件大小 /// </summary> public long SizeBytes { get; private set; } /// <summary> /// 目前为止 已收到的文件代销 /// </summary> public long ReceivedBytes { get; private set; } /// <summary> /// 完成的百分比 /// </summary> public double CompletedPercent { get { return (double)ReceivedBytes / SizeBytes; } set { throw new Exception("An attempt to modify readonly value."); } } /// <summary> /// 源信息 /// </summary> public string SourceInfoStr { get { return "[" + SourceInfo.RemoteEndPoint.Address + ":" + SourceInfo.RemoteEndPoint.Port + "]"; } } /// <summary> /// 已经完成 /// </summary> public bool IsCompleted { get { return ReceivedBytes == SizeBytes; } } /// <summary> /// 同步锁 用来保证线程安全 /// </summary> object SyncRoot = new object(); /// <summary> /// 内存流 用来创建文件 /// </summary> Stream data; /// <summary> /// 创建一个接收文件类 /// </summary> /// <param name="filename">文件名称 Filename associated with this file</param> /// <param name="sourceInfo">文件信息类 ConnectionInfo corresponding with the file source</param> /// <param name="sizeBytes">文件大小 The total size in bytes of this file</param> public ReceivedFile(string filename, ConnectionInfo sourceInfo, long sizeBytes) { this.Filename = filename; this.SourceInfo = sourceInfo; this.SizeBytes = sizeBytes; //在硬盘上创建一个文件流 使得我们可以接收大文件 data = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 8 * 1024, FileOptions.None); } /// <summary> /// 把接收到的字节数据添加到当前文件流种 /// </summary> public void AddData(long dataStart, int bufferStart, int bufferLength, byte[] buffer) { lock (SyncRoot) { data.Seek(dataStart, SeekOrigin.Begin); data.Write(buffer, (int)bufferStart, (int)bufferLength); ReceivedBytes += (int)(bufferLength - bufferStart); //收到全部数据,保存文件 if (ReceivedBytes == SizeBytes) { data.Flush(); data.Close(); SaveFileToDisk(@"d:\" + Filename); } } } /// <summary> /// 保存文件 /// </summary> /// <param name="saveLocation">Location to save file</param> public void SaveFileToDisk(string saveLocation) { if (ReceivedBytes != SizeBytes) throw new Exception("Attempted to save out file before data is complete."); if (!File.Exists(Filename)) throw new Exception("The transfered file should have been created within the local application directory. Where has it gone?"); File.Delete(saveLocation); File.Move(Filename, saveLocation); } /// <summary> ///关闭 /// </summary> public void Close() { try { data.Dispose(); } catch (Exception) { } try { data.Close(); } catch (Exception) { } } } }
3.在MessageContract类库中添加SendInfo契约类方法,此方法用于传递文件信息,客户端和服务器端都需要使用
using System; using System.Collections.Generic; using System.Linq; using System.Text; using ProtoBuf; namespace MessageContract { /// <summary> /// 文件信息类 /// </summary> [ProtoContract] public class SendInfo { /// <summary> /// 文件名称 /// </summary> [ProtoMember(1)] public string Filename { get; private set; } /// <summary> /// 文件发送-开始位置 /// </summary> [ProtoMember(2)] public long BytesStart { get; private set; } /// <summary> /// 文件大小 /// </summary> [ProtoMember(3)] public long TotalBytes { get; private set; } /// <summary> /// 顺序号 /// </summary> [ProtoMember(4)] public long PacketSequenceNumber { get; private set; } /// <summary> /// 私有构造函数 用来反序列化 /// </summary> private SendInfo() { } /// <summary> /// 创建一个新的实例 /// </summary> /// <param name="filename">文件名称 Filename corresponding to data</param> /// <param name="totalBytes">文件大小 Total bytes of the whole ReceivedFile</param> /// <param name="bytesStart">开始位置 The starting point for the associated data</param> /// <param name="packetSequenceNumber">顺序号 Packet sequence number corresponding to the associated data</param> public SendInfo(string filename, long totalBytes, long bytesStart, long packetSequenceNumber) { this.Filename = filename; this.TotalBytes = totalBytes; this.BytesStart = bytesStart; this.PacketSequenceNumber = packetSequenceNumber; } } }