C#使用FileSystemWatcher來監控指定文件夾,並使用TCP/IP協議通過Socket發送到另外指定文件夾


項目需求:

局域網內有兩台電腦,電腦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;
    }
}

啟動代碼(啟動本地監控,啟用本地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("發送完成,關閉連接。");
      });
}

效果圖:

未實現的功能:

斷點續傳

原因:

1、在使用過程中,發現NewLife.Net不支持普通Socket編程那樣的可以一直Receive來接收后續流的操作(TCP協議),每次流到達都會觸發一次事件,從而不得不每次發送的時候都帶上一些頭部信息(文件名、偏移量、大小等),無形中增大了流量。

2、目前看的效果是NewLife.Net在服務器端接收的時候,包的順序並不是和客戶端Send的時候保持一致(如客戶端發送 1 2 3 4 5),服務端可能接收的是2 1 3 5 4這樣的順序,這個問題,可能是跟我用異步有關系,按說TCP是可以保證包的順序的,我已經在GitHub上提問了,目前等待作者(大石頭:https://www.cnblogs.com/nnhy/)解答。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM