源碼下載下傳位址:https://download.csdn.net/download/5653325/11236916
項目需求:
區域網路内有兩台電腦,電腦A(Windows系統)主要是負責接收一些檔案(遠端桌面粘貼、FTP上傳、檔案夾共享等方式),希望能在A接收檔案後自動傳輸到電腦B(Windows系統)來做一個備份,同時電腦B上有個目錄,如果往這個目錄裡粘貼檔案了,會自動傳輸給A來儲存。
于是通過百度找到了System.IO.FileSystemWatcher這個類,通過它來監聽指定的檔案夾的一些消息(檔案建立、檔案修改、檔案删除、檔案重命名)來做對應的動作,目前隻需求監控檔案建立,其它事件不作處理。
檔案傳輸方面,可以自己寫Socket的Server和Client,但要注意粘包的問題。我這裡使用了開源的NewLife.Net(https://github.com/NewLifeX/NewLife.Net),用戶端和伺服器都是用它的話,内置解決粘包問題的解決方案,而且管理起來很友善,自帶日志輸出功能強大。這裡分享一下實作的代碼以及一些問題。
1、建立一個Winform的工程,運作架構為.Netframework4.6
Nuget上引用NewLife.Net
界面結構如下:

本機端口,代表本機作為伺服器(server)監聽的端口,遠端伺服器IP及端口就是當本機監控到檔案建立時,自動發送給哪台伺服器(接收伺服器同樣需要運作本軟體)。
本地監控自動發送檔案夾:凡是在指定的這個檔案夾中建立(一般是粘貼)的檔案都會被自動發送走。
自動儲存接收到的檔案夾:凡是遠端發送過來的檔案,都自動儲存在此檔案夾下面。
2、實作代碼
Program.cs中定義兩個全局的變量,用來儲存檔案夾資訊
/// <summary>
/// 要監控的接收儲存檔案夾
/// </summary>
public static string SaveDir = "";
/// <summary>
/// 要監控的發送檔案夾
/// </summary>
public static string SendDir = "";
using的一些類
using System;
using System.Data;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using DirectoryWatch.Class;
using NewLife.Data;
using NewLife.Log;
using NewLife.Net;
using NewLife.Net.Handlers;
在窗體加載時,定義一些共用變量以及指定窗體下方TextBox為日志輸出載體
private static int remotePort = 0;//遠端端口
private static int localPort = 0;//本地端口
private static string remoteIP = "";//遠端IP
private FileSystemWatcher watcher;//監控檔案夾
private NetServer server;//本地服務
private void MainFrm_Load(object sender, EventArgs e)
{
textBox1.UseWinFormControl();
}
本地監控檔案夾選擇按鈕代碼
private void Btn_dirbd_Click(object sender, EventArgs e)
{
using (var folderBrowser = new FolderBrowserDialog())
{
if (folderBrowser.ShowDialog() != DialogResult.OK) return;
Program.SendDir = folderBrowser.SelectedPath;
if (!Directory.Exists(Program.SendDir))
{
MessageBox.Show(@"所選路徑不存在或無權通路", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (string.Equals(Program.SaveDir.ToLower(), Program.SendDir.ToLower()))
{
MessageBox.Show(@"自動接收檔案夾和自動發送檔案夾不能是同一個檔案夾", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
txt_localPath.Text = folderBrowser.SelectedPath;
Program.SendDir = folderBrowser.SelectedPath;
}
}
本地自動儲存檔案夾選擇按鈕代碼
private void Btn_saveDic_Click(object sender, EventArgs e)
{
using (var folderBrowser = new FolderBrowserDialog())
{
if (folderBrowser.ShowDialog() != DialogResult.OK) return;
Program.SaveDir = folderBrowser.SelectedPath;
if (!Directory.Exists(Program.SendDir))
{
MessageBox.Show(@"所選路徑不存在或無權通路", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (string.Equals(Program.SaveDir.ToLower(), Program.SendDir.ToLower()))
{
MessageBox.Show(@"自動接收檔案夾和自動發送檔案夾不能是同一個檔案夾", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
txt_remoteDir.Text = folderBrowser.SelectedPath;
Program.SaveDir = folderBrowser.SelectedPath;
}
}
啟動代碼(啟動本地監控,啟用本地server服務)
private void Btn_Start_Click(object sender, EventArgs e)
{
int.TryParse(txt_remotePort.Text, out remotePort);
int.TryParse(txt_localPort.Text, out localPort);
if (string.IsNullOrEmpty(txt_remoteIP.Text.Trim()))
{
MessageBox.Show(@"請填寫遠端伺服器IP", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
remoteIP = txt_remoteIP.Text.Trim();
if (remotePort == 0)
{
MessageBox.Show(@"請填寫遠端伺服器的端口", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (localPort == 0)
{
MessageBox.Show(@"請填寫本地伺服器要打開的端口", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (string.IsNullOrEmpty(Program.SendDir))
{
MessageBox.Show(@"請選擇本地自動發送檔案夾路徑", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (string.IsNullOrEmpty(Program.SaveDir))
{
MessageBox.Show(@"請選擇本地自動接收發送過來的檔案夾路徑", @"錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (Btn_Start.Text.Equals("停止"))
{
watcher.EnableRaisingEvents = false;
server.Stop("手動停止");
Btn_Start.Text = @"啟動";
foreach (Control control in Controls)
{
if (!(control is Button) && !(control is TextBox)) continue;
if (control.Name != "Btn_Start")
{
control.Enabled = true;
}
}
return;
}
watcher = new FileSystemWatcher
{
Path = Program.SendDir,
Filter = "*.*"//監控所有檔案
};
watcher.Created += OnProcess;//隻監控新增檔案
//watcher.Changed += OnProcess;
//watcher.Deleted += new FileSystemEventHandler(OnProcess);
//watcher.Renamed += new RenamedEventHandler(OnRenamed);
watcher.EnableRaisingEvents = true;//是否讓監控事件生效
//watcher.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess| NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
watcher.NotifyFilter = NotifyFilters.FileName;//這是一些通知屬性,目前不用
watcher.IncludeSubdirectories = true;//包含子檔案夾
server = new NetServer
{
Log = XTrace.Log,
SessionLog = XTrace.Log,
SocketLog = XTrace.Log,
Port = localPort,
ProtocolType = NetType.Tcp
};//使用NewLife.Net建立一個Server服務,隻使用TCP協定
server.Received += async (x, y) =>
{
//接收檔案
var session = x as NetSession;
if (!(y.Message is Packet pk)) return;
int.TryParse(Encoding.UTF8.GetString(pk.ReadBytes(0, 1)), out var fileState);//檔案狀态1位元組
int.TryParse(Encoding.UTF8.GetString(pk.ReadBytes(1, 10)), out var headinfo);//檔案總長度10位元組
int.TryParse(Encoding.UTF8.GetString(pk.ReadBytes(11, 8)), out var fileNameLength);//檔案名長度8位元組
var fileName = Encoding.UTF8.GetString(pk.ReadBytes(19, fileNameLength));//檔案名
int.TryParse(Encoding.UTF8.GetString(pk.ReadBytes(19 + fileNameLength, 10)), out var offset);//位置偏移量10位元組
var data = pk.ReadBytes(29 + fileNameLength, pk.Count - (29 + fileNameLength));//資料内容
if (data.Length == 0) return;
await Task.Run(async () =>
{
var writeData = data;
using (var filestream = new FileStream($"{Program.SaveDir}\\{fileName}", FileMode.OpenOrCreate,
FileAccess.Write, FileShare.ReadWrite))
{
filestream.Seek(offset, SeekOrigin.Begin);
await filestream.WriteAsync(writeData, 0, writeData.Length);
await filestream.FlushAsync();
}//資料寫入檔案
});
XTrace.WriteLine([email protected]"狀态:{fileState},編号:{session.ID},檔案總長度:{headinfo},檔案名長度:{fileNameLength},檔案名:{fileName},偏移量:{offset},内容長度:{data.Length}");
//XTrace.Log.Debug(Encoding.UTF8.GetString(pk.Data));//輸出日志
};
server.Add<StandardCodec>();//解決粘包,引入StandardCodec
server.Start();
Btn_Start.Text = string.Equals("啟動", Btn_Start.Text) ? "停止" : "啟動";
foreach (Control control in Controls)
{
if (!(control is Button) && !(control is TextBox)) continue;
if (control.Name != "Btn_Start")
{
control.Enabled = false;
}
}
}
監控事件觸發時執行的代碼
private static void OnProcess(object source, FileSystemEventArgs e)
{
if (e.ChangeType == WatcherChangeTypes.Created)
{
OnCreated(source, e);
}
//else if (e.ChangeType == WatcherChangeTypes.Changed)
//{
// OnChanged(source, e);
//}
//else if (e.ChangeType == WatcherChangeTypes.Deleted)
//{
// OnDeleted(source, e);
//}
}
監控到建立檔案時執行代碼
/// <summary>
/// 監測檔案建立事件,延時10秒後進行寫入檔案發送隊列,防止檔案尚未建立完成就執行發送(10秒内複制不完的 同樣有問題)
/// 第1位 0代表新檔案 1代表續傳 2代表最後一次
/// 2--11位 代表檔案總長度
/// 12--18 位代表檔案名長度
/// 19--N 位 代表檔案名資訊
/// 19--(N+1)--offset位,代表此次發送檔案的偏移量位置
/// 29+(N+1)--結束 代表此次發送的檔案内容
/// </summary>
/// <param name="source"></param>
/// <param name="e"></param>
private static void OnCreated(object source, FileSystemEventArgs e)
{
Task.Run(async () =>
{
await Task.Delay(10000);
var TcpClient = new NetUri($"tcp://{remoteIP}:{remotePort}");//需要發送給的遠端伺服器
var Netclient = TcpClient.CreateRemote();
Netclient.Log = XTrace.Log;
Netclient.LogSend = true;
Netclient.LogReceive = true;
Netclient.Add<StandardCodec>();
Netclient.Received += (s, ee) =>
{
if (!(ee.Message is Packet pk1)) return;
XTrace.WriteLine("收到伺服器:{0}", pk1.ToStr());
};
if (!File.Exists(e.FullPath)) return;
byte[] data;
using (var streamReader = new FileStream(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
if (streamReader.CanRead)
{
data = new byte[streamReader.Length];
await streamReader.ReadAsync(data, 0, (int)streamReader.Length);
}
else
{
XTrace.Log.Error($"{e.FullPath}不可通路");
return;
}
}
var fileState = Encoding.UTF8.GetBytes("0");//新檔案發送
var headinfo = new byte[10];//總長度
headinfo = Encoding.UTF8.GetBytes(data.Length.ToString());
var fileNameLength = new byte[8];//檔案名長度
fileNameLength = Encoding.UTF8.GetBytes(Encoding.UTF8.GetBytes(e.Name).Length.ToString());
var fileNameByte = new byte[e.Name.Length];//檔案名
fileNameByte = Encoding.UTF8.GetBytes(e.Name);
var offset = 0;//偏移量
var sendLength = 409600;//單次發送大小
Netclient.Open();
while (data.Length > offset)
{
if (offset > 0)
{
fileState = Encoding.UTF8.GetBytes("1");//追加檔案
}
if (sendLength > data.Length - offset)
{
sendLength = data.Length - offset;
fileState = Encoding.UTF8.GetBytes("2");//最後一次發送
}
var offsetByte = new byte[10];//偏移位置byte
offsetByte = Encoding.UTF8.GetBytes(offset.ToString());
//一次發送總byte
var sendData = new byte[1 + 10 + 8 + fileNameByte.Length + 10 + sendLength];
//檔案狀态0第一次 1追加檔案 2最後一次發送
Array.Copy(fileState, 0, sendData, 0, fileState.Length);
//檔案總長度
Array.Copy(headinfo, 0, sendData, 1, headinfo.Length);
//檔案名長度
Array.Copy(fileNameLength, 0, sendData, 11, fileNameLength.Length);
//檔案名資訊
Array.Copy(fileNameByte, 0, sendData, 19, fileNameByte.Length);
//此次内容偏移量
Array.Copy(offsetByte, 0, sendData, 19 + fileNameByte.Length, offsetByte.Length);
//一次發送的内容 offsetByte為10byte
Array.Copy(data, offset, sendData, 29 + fileNameByte.Length, sendLength);
offset += sendLength;
var pk = new Packet(sendData);
Netclient.SendMessage(pk);
}
//Netclient.Close("發送完成,關閉連接配接。");
});
}
效果圖:
未實作的功能:
斷點續傳
原因:
1、在使用過程中,發現NewLife.Net不支援普通Socket程式設計那樣的可以一直Receive來接收後續流的操作(TCP協定),每次流到達都會觸發一次事件,進而不得不每次發送的時候都帶上一些頭部資訊(檔案名、偏移量、大小等),無形中增大了流量。
2、目前看的效果是NewLife.Net在伺服器端接收的時候,包的順序并不是和用戶端Send的時候保持一緻(如用戶端發送 1 2 3 4 5),服務端可能接收的是2 1 3 5 4這樣的順序,這個問題,可能是跟我用異步有關系(我試了幾次不用async 好像不一定每次都是和發送端的順序一緻),按說TCP是可以保證包的順序的,我已經在GitHub上提問了,目前等待作者回複。