C#使用FileSystemWatcher来监控指定文件夹,并使用TCP/IP协议通过Socket发送到另外指定文件夹
感谢大哥 https://www.cnblogs.com/wdw984/p/11008385.html
项目需求:
局域网内有两台电脑,电脑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中定义两个全局的变量,用来保存文件夹信息
1
2
3
4
5
6
7
8
9
|
/// <summary> /// 要监控的接收保存文件夹 /// </summary> public static string SaveDir = "" ; /// <summary> /// 要监控的发送文件夹 /// </summary> public static string SendDir = "" ; |
using的一些类
1
2
3
4
5
6
7
8
9
10
11
|
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为日志输出载体
1
2
3
4
5
6
7
8
9
|
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(); } |
本地监控文件夹选择按钮代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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; } } |
本地自动保存文件夹选择按钮代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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服务)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
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 ; } } } |
监控事件触发时执行的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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); //} } |
监控到创建文件时执行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
/// <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/)解答。