欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C#使用FileSystemWatcher来监控指定文件夹,并使用TCP/IP协议通过Socket发送到另外指定文件夹

程序员文章站 2022-03-06 22:39:27
项目需求: 局域网内有两台电脑,电脑A(Windows系统)主要是负责接收一些文件(远程桌面粘贴、FTP上传、文件夹共享等方式),希望能在A接收文件后自动传输到电脑B(Windows系统)来做一个备份,同时电脑B上有个目录,如果往这个目录里粘贴文件了,会自动传输给A来保存。 于是通过百度找到了Sys ......

项目需求:

局域网内有两台电脑,电脑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 

界面结构如下:

C#使用FileSystemWatcher来监控指定文件夹,并使用TCP/IP协议通过Socket发送到另外指定文件夹

 

本机端口,代表本机作为服务器(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("发送完成,关闭连接。");
      });
}

效果图:

C#使用FileSystemWatcher来监控指定文件夹,并使用TCP/IP协议通过Socket发送到另外指定文件夹

未实现的功能:

断点续传

原因:

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/)解答。