此项目主要实现:在服务器端上传大文件
使用的技术点:
文件流读写、进度条、多线程、WCF服务接口的实现和回调
文件服务接口
3个功能:
1.文件准备上传
2.文件传输(上传按钮)
3.文件传输终止
1.搭建客户端和服务器端环境:
本项目使用VS2017开发,废话不说直接干。
首先创建服务器端CRMServer:
然后我们把生成的服务文件删除,以及App.Config中server信息删除。
2.添加文件模型类FileModel:封装上传文件的具体信息
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
namespace CRMServer
{
/// <summary>
/// 文件模型类:用来封装上传文件的信息
/// 文件传输的特点:
//不是一下全部传输完毕,而是按指定的大小(比如100kb/秒)上传多次
/// </summary>
[Serializable]//序列化处理
[DataContract]//数据契约
public class FileModel
{
/// <summary>
/// 文件编号(GUID)--W唯一标识
/// </summary>
[DataMember]
public string FieldId { get; set; }
/// <summary>
/// 文件名称
/// </summary>
[DataMember]
public string FileName { get; set; }
/// <summary>
/// 文件全路径
/// </summary>
[DataMember]
public string FileFullPath { get; set; }
/// <summary>
/// 文件总大小(以字节为单位)
/// </summary>
[DataMember]
public long FileSize { get; set; }
/// <summary>
/// 字符数据(每次传输的大小):
/// 可能会添加一下属性数据(添加的数据),真正上传的数据+人为添加的数据
/// </summary>
[DataMember]
public byte[] FileBytes { get; set; }
/// <summary>
///每次传输的长度
/// </summary>
[DataMember]
public int FileLength { get; set; }
/// <summary>
/// 文件后缀名称
/// </summary>
[DataMember]
public string FileSuffixName { get; set; }
}
}
自定义文件服务接口并添加方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace CRMServer
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的接口名“IFileService”。
/// <summary>
/// 文件服务接口
/// </summary>
[ServiceContract(CallbackContract =typeof(ICallback))]//回调契约
public interface IFileService
{
/// <summary>
/// 文件上传(准备)
/// </summary>
[OperationContract(IsOneWay =true)]
void FileUpload(FileModel fileModel);
/// <summary>
/// 文件传输
/// </summary>
[OperationContract(IsOneWay =true)]
void FileTransfer(FileModel fileModel);
/// <summary>
/// 文件终止(取消上传 )
/// </summary>
[OperationContract(IsOneWay =true)]
void FileStop(FileModel fileModel);
}
public interface ICallback {//回调接口
/// <summary>
/// 已上传文件的大小
/// </summary>
/// <param name="fileSize"></param>
[OperationContract(IsOneWay =true)]
void ToFileSize(long fileSize);
}
}
实现服务类FileService:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace CRMServer
{
// 注意: 使用“重构”菜单上的“重命名”命令,可以同时更改代码和配置文件中的类名“FileService”。
public class FileService : IFileService
{
//保存上传的文件列表
Dictionary<string, FileStream> dict = new Dictionary<string, FileStream>();
/// <summary>
/// 文件上传
/// </summary>
/// <param name="fileModel"></param>
public void FileUpload(FileModel fileModel)
{
try {
string path = "F:/FileUpload/" + fileModel.FileName;
FileStream fileStream = new FileStream(path, FileMode.Create);
dict.Add(fileModel.FieldId, fileStream);
}
catch (Exception ex) {
Console.WriteLine(ex.Message+"--->"+ex.StackTrace);
}
}
/// <summary>
/// 文件传输:多次按指定的大小进行传输:
/// 在客户端循环处理,实现多次传输数据到服务端-->客户端可随时停止上传
/// </summary>
/// <param name="fileModel"></param>
public void FileTransfer(FileModel fileModel)
{
try {
string fileId = fileModel.FieldId;
FileStream fileStream = dict[fileId];
fileStream.Write(fileModel.FileBytes, 0, fileModel.FileLength);//实现:写入到服务器端的磁盘文件中
//回调客户端,通知写入成功
ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>();
callback.ToFileSize(fileModel.FileBytes.Length);//调用回调方法,通知客户端
//fileStream.Length:每传输一次,Length递增一次
if (fileStream.Length >= fileModel.FileSize)
{//校验是否已全部传输完毕
//自动关闭文件
if (fileStream!=null) {
fileStream.Close();//关闭流资源
}
dict.Remove(fileModel.FieldId);//从服务器端移除文件
}
}
catch (Exception ex) {
Console.WriteLine(ex.Message+"-->"+ex.StackTrace);
}
}
/// <summary>
/// 文件终止(取消上传):手动关闭
/// </summary>
/// <param name="fileModel"></param>
public void FileStop(FileModel fileModel)
{
try {
string fileId = fileModel.FieldId;
FileStream fs = dict[fileId];
if (fs != null)
{
fs.Close();
}
dict.Remove(fileId);
}
catch (Exception ex) {
Console.WriteLine(ex.Message+"--->"+ex.StackTrace);
}
}
}
}
3.创建客户端CRMClient WinForm项目:
添加窗体MainForm作为主页面:
两个Button:浏览和上传按钮,以及一个TextBox文本框:获取上传文件的路径。
对应的底层代码:
using System;
using System.Windows.Forms;
using CRMClient.FileServer;
namespace CRMClient
{
public partial class MainForm : Form
{
string fileName;//文件路径
public MainForm()
{
InitializeComponent();
this.ShowIcon = false;
}
/// <summary>
/// 浏览按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnBrowse_Click(object sender, EventArgs e)
{
OpenFile();
}
/// <summary>
/// 文本框点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void txtUploadFile_Click(object sender, EventArgs e)
{
OpenFile();
}
/// <summary>
/// 上传按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnUploadFile_Click(object sender, EventArgs e)
{
FileUploadForm fileUploadForm = new FileUploadForm(fileName);
fileUploadForm.CloseFormAction += FileUploadForm_CloseFormAction;
fileUploadForm.TopMost = true;
fileUploadForm.Show();
}
/// <summary>
/// 关闭上传窗体时的回调函数
/// </summary>
/// <param name="obj"></param>
private void FileUploadForm_CloseFormAction(FileModel fileModel)
{
if (fileModel!=null) {
MessageBox.Show($"{fileModel.FileName}文件上传成功!");
this.Close();
}
}
/// <summary>
/// 文件上传对话框:
/// </summary>
private void OpenFile() {
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "JPEG文件|*.jpeg*|JPG文件|*.jpg*|PNG文件|*.png*|GIF文件|*.gif*|BMP文件|*.bmp*|文本文件|*.txt*|文档文件|*.doc*|所有类型文件|*.*";//限制上传的文件类型
DialogResult result= ofd.ShowDialog();
if (result==DialogResult.OK) {//确认选中文件
fileName= ofd.FileName;//全路径
this.txtUploadFile.Text = fileName;
//string name=Path.GetFileNameWithoutExtension(fileName);
//string extensionName = Path.GetExtension(fileName);
//string uploadedFileName = name + extensionName;
}
}
}
}
主页面实现的功能比较简单,就是选择上传的文件,然后打开文件上传FileUploadForm页面,等文件上传成功,得到回馈信息。
添加文件上传FileUploadForm窗体:显示文件上传的进度
页面上的控件:4个label,Progressbar进度条以及取消上传按钮。
底层代码:
using CRMClient.FileServer;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.ServiceModel;
namespace CRMClient
{
public partial class FileUploadForm : Form
{
string filePath;
FileModel fileModel;
FileServiceClient fsc;
BackgroundWorker bw;//线程对象
public FileUploadForm()
{
InitializeComponent();
}
public FileUploadForm(string filePath)
{
InitializeComponent();
this.filePath = filePath;
bw = new BackgroundWorker();//创建线程对象
//上传文件
UploadFile();
}
string fileSizeValStr;
private void FileUploadForm_Load(object sender, EventArgs e)
{
//b ->MB:默认为long类型(长整形),如果文件比较小-->0MB
//实际上并不为0(不是空文件)-->带小数位的数据类型(double)
if (fileModel != null)
{
double fileSizeDouble = Convert.ToDouble(fileModel.FileSize);
double fileSize = fileSizeDouble / 1024 / 1024;//MB
double fileSizeVal = Math.Round(fileSize, 2);
fileSizeValStr = fileSizeVal.ToString() + " MB";
this.lblFileSize.Text = fileSizeValStr;
this.pbFileUpload.Maximum = 100;
this.pbFileUpload.Minimum = 0;
}
//使用单独的线程去处理进度条:
//如果直接在当前页面所在线程(主线程)直接处理,整个页面会卡死,无法对当前页面进行任何其他操作
this.bw.WorkerReportsProgress = true;
this.bw.WorkerSupportsCancellation = true;
//多线程控件(3个方法)
this.bw.DoWork += Bw_DoWork;//开启线程触发
this.bw.ProgressChanged += Bw_ProgressChanged;//辅助线程任务执行时触发
this.bw.RunWorkerCompleted += Bw_RunWorkerCompleted;//任务结束时触发
this.bw.RunWorkerAsync();//开启任务
}
/// <summary>
/// 开启线程任务(文件准备上传)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
using (FileStream fs = new FileStream(fileModel.FileFullPath, FileMode.Open))
{
while (fs.Position < fs.Length)
{//未传输完毕
if (this.bw.CancellationPending)//判断是否取消上传
{
this.bw.ReportProgress(0, null);//回滚进度条
e.Cancel = true;//取消事件
return;//退出当前方法
}
//循环上传(比如:每次上传10kb)
byte[] bufferArray = new byte[10240];//每次读取的缓存数组
int count = fs.Read(bufferArray, 0, bufferArray.Length);//每次写入到缓冲区的字节数
fileModel.FileBytes = bufferArray;
fileModel.FileLength = count;
fsc.FileTransfer(fileModel);//长连接循环发送
}
}
}
/// <summary>
/// 线程更改时触发
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//int progressPercentage= e.ProgressPercentage;
//int maxXiMum = this.pbFileUpload.Maximum;
//更新进度条:有可能 100(最大值) progressPercentage有可能为101
//this.pbFileUpload.Value = progressPercentage>maxXiMum?maxXiMum:progressPercentage;
this.pbFileUpload.Value = e.ProgressPercentage;
if (e.UserState != null)
{
this.lblUploadedFileSize.Text = e.UserState.ToString() + " MB";//实时文件大小:MB
}
}
/// <summary>
/// 线程结束时触发:当没有动作触发
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public event Action<FileModel> CloseFormAction;//定义关闭当前页面的委托事件
private void Bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (this.pbFileUpload.Value == this.pbFileUpload.Maximum)
{//传输完毕
if (CloseFormAction != null)
{
CloseFormAction(this.fileModel);//调用委托事件
}
this.Close();//关闭当前窗体
}
}
private void UploadFile()
{
//使用FileInfo包装上传的文件
FileInfo fileInfo = new FileInfo(filePath);
//封装文件模型对象
fileModel = new FileModel();
fileModel.FieldId = Guid.NewGuid().ToString();
fileModel.FileName = fileInfo.Name;
fileModel.FileSize = fileInfo.Length;
fileModel.FileSuffixName = fileInfo.Extension;
fileModel.FileFullPath = fileInfo.FullName;
FileServerCallback fileServerCallback = new FileServerCallback();
fileServerCallback.ToFileSizeCallback += FileServerCallback_ToFileSizeCallback;//注册委托事件
//创建服务器端代理对象
fsc = new FileServiceClient(new InstanceContext(fileServerCallback));
fsc.FileUpload(fileModel);//和服务器端进行对接
}
/// <summary>
/// 获取已上传文件大小的委托事件的回调函数
/// 修改进度条
/// </summary>
/// <param name="obj"></param>
private void FileServerCallback_ToFileSizeCallback(long fileSize)
{
//步长:每步走多少字节数
int stepSize = (int)(this.fileModel.FileSize / this.pbFileUpload.Maximum);
if (fileSize > stepSize * this.pbFileUpload.Value)
{//判断时候已走到头
int result = this.pbFileUpload.Value + 1;//进度条加1
//long sizeMB = fileSize / 1024 / 1024;
double fileSizeDouble = Convert.ToDouble(fileSize);
double sizeDoubleMB = fileSizeDouble / 1024 / 1024;//MB
double fileSizeVal = Math.Round(sizeDoubleMB, 2);
if (this.bw.IsBusy)
{//校验此线程是否还在运行
this.bw.ReportProgress(result, fileSizeVal);//手动触发bw线程的ProgressChanged事件
}
}
}
/// <summary>
/// 手动取消上传
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnCancell_Click(object sender, EventArgs e)
{
this.bw.CancelAsync();//取消操作:关闭线程
this.fsc.FileStop(this.fileModel);//通知服务器端关闭和移除文件流
this.Close();
}
}
}
这里我们对整体的客户端与服务端交互处理,做个概述:
客户端(前台):在主页面用户点击’浏览’按钮或者文本框,选择需要上传的文件,
点击’上传’按钮跳转到"文件上传页面"。
实例化FileUploadForm窗体对象时:
添加方法:UploadFile,根据传入的文件路径参数,使用文件包装类FileInfo处理,获取具体的文件信息,然后封装成FileModel模型对象,使用 FileServiceClient服务器端代理对象,调用服务器端FileUpload方法上传文件,此方法在服务器磁盘中仅仅创建了一个空文件.
string path = "F:/FileUpload/" + fileModel.FileName;
FileStream fileStream = new FileStream(path, FileMode.Create);
我们在页面加载时,通过一个单独的线程对象BackgroundWorker去处理文件的具体传输。
private void FileUploadForm_Load(object sender, EventArgs e)
{
.........................................................
.........................................................
.........................................................
.........................................................
//使用单独的线程去处理进度条:
//如果直接在当前页面所在线程(主线程)直接处理,整个页面会卡死,无法对当前页面进行任何其他操作
this.bw.WorkerReportsProgress = true;
this.bw.WorkerSupportsCancellation = true;
//多线程控件(3个方法)
this.bw.DoWork += Bw_DoWork;//开启线程触发
this.bw.ProgressChanged += Bw_ProgressChanged;//辅助线程任务执行时触发
this.bw.RunWorkerCompleted += Bw_RunWorkerCompleted;//任务结束时触发
this.bw.RunWorkerAsync();//开启任务:执行DoWork方法
}
/// <summary>
/// 开启线程任务(文件准备上传)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
.........................................................
.........................................................
.........................................................
.........................................................
}
/// <summary>
/// 线程更改时触发
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
.........................................................
.........................................................
.........................................................
.........................................................
}
/// <summary>
/// 线程结束时触发:当没有动作触发更改时,进度结束时
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bw_RunWorkerCompleted(object sender,RunWorkerCompletedEventArg e)
{
.........................................................
.........................................................
.........................................................
.........................................................
}
手动去触发BackgroundWorker的ProgressChanged:
我们这边是在服务器每传输一次,进行回调到客户端时,去手动触发此事件,更新进度条.
/// <summary>
/// 获取已上传文件大小的委托事件的回调函数
/// 修改进度条
/// </summary>
/// <param name="obj"></param>
private void FileServerCallback_ToFileSizeCallback(long fileSize)
{
.........................................................
.........................................................
.........................................................
if (this.bw.IsBusy)
{//校验此线程是否还在运行
this.bw.ReportProgress(result, fileSizeVal);//手动触发bw线程的ProgressChanged事件
}
}
this.bw.ReportProgress(result, fileSizeVal);
注意:ReportProgress方法的两个参数,将会传给触发的ProgressChanged事件的
ProgressChangedEventArgs参数对象,
#region 程序集 System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.dll
#endregion
namespace System.ComponentModel
{
//
// 摘要:
// 为 System.ComponentModel.BackgroundWorker.ProgressChanged 事件提供数据。
public class ProgressChangedEventArgs : EventArgs
{
//
// 摘要:
// 初始化 System.ComponentModel.ProgressChangedEventArgs 类的新实例。
//
// 参数:
// progressPercentage:
// 已完成的异步任务的百分比。
//
// userState:
// 唯一的用户状态。
public ProgressChangedEventArgs(int progressPercentage, object userState);
//
// 摘要:
// 获取异步任务进度百分比。
//
// 返回结果:
// 指示异步任务进度的百分比值。
[SRDescriptionAttribute("Async_ProgressChangedEventArgs_ProgressPercentage")]
public int ProgressPercentage { get; }
//
// 摘要:
// 获取唯一的用户状态。
//
// 返回结果:
// 一个唯一 System.Object ,该值指示用户状态。
[SRDescriptionAttribute("Async_ProgressChangedEventArgs_UserState")]
public object UserState { get; }
}
}
4.启动服务器端,在客户端引用服务,测试在客户端调用服务器端的功能。
启动以及添加服务引用请参考上个项目,这里就不阐述了,地址:测试调用服务器端操作
这里我们看一下如何,在客户端引用服务后,查看生成的服务器端代理类的名称以及所位于的nameSpace?
我们双击引用的服务,会打开对象浏览器对话框:
我么会发现代理类名称:服务名称+Client
回调接口的名称也改变了,具体的更改需要如此操作查看。