項目需求:git
局域網內有兩臺電腦,電腦A(Windows系統)主要是負責接收一些文件(遠程桌面粘貼、FTP上傳、文件夾共享等方式),但願能在A接收文件後自動傳輸到電腦B(Windows系統)來作一個備份,同時電腦B上有個目錄,若是往這個目錄裏粘貼文件了,會自動傳輸給A來保存。github
因而經過百度找到了System.IO.FileSystemWatcher這個類,經過它來監聽指定的文件夾的一些消息(文件建立、文件修改、文件刪除、文件重命名)來作對應的動做,目前只需求監控文件建立,其它事件不做處理。編程
文件傳輸方面,能夠本身寫Socket的Server和Client,但要注意粘包的問題。我這裏使用了開源的NewLife.Net(https://github.com/NewLifeX/NewLife.Net),客戶端和服務器都是用它的話,內置解決粘包問題的解決方案,並且管理起來很方便,自帶日誌輸出功能強大。這裏分享一下實現的代碼以及一些問題。服務器
一、建立一個Winform的工程,運行框架爲.Netframework4.6session
Nuget上引用NewLife.Net 框架
界面結構以下:異步
本機端口,表明本機做爲服務器(server)監聽的端口,遠程服務器IP及端口就是當本機監控到文件建立時,自動發送給哪臺服務器(接收服務器一樣須要運行本軟件)。async
本地監控自動發送文件夾:凡是在指定的這個文件夾中新建(通常是粘貼)的文件都會被自動發送走。tcp
自動保存接收到的文件夾:凡是遠程發送過來的文件,都自動保存在此文件夾下面。3d
二、實現代碼
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; } }
啓動代碼(啓動本地監控,啓用本地SocketServer服務)
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($@"狀態:{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("發送完成,關閉鏈接。"); }); }
效果圖:
未實現的功能:
斷點續傳
緣由:
一、在使用過程當中,發現NewLife.Net不支持普通Socket編程那樣的能夠一直Receive來接收後續流的操做(TCP協議),每次流到達都會觸發一次事件,從而不得不每次發送的時候都帶上一些頭部信息(文件名、偏移量、大小等),無形中增大了流量。
二、目前看的效果是NewLife.Net在服務器端接收的時候,包的順序並非和客戶端Send的時候保持一致(如客戶端發送 1 2 3 4 5),服務端可能接收的是2 1 3 5 4這樣的順序,這個問題,多是跟我用異步有關係,按說TCP是能夠保證包的順序的,我已經在GitHub上提問了,目前等待做者(大石頭:https://www.cnblogs.com/nnhy/)解答。