脑残的FtpWebRequest 2之事故现场重现
鉴于上一篇POST过于抽象以至于很多人无法理解,现在用代码来说话,我们一起来重新回顾一下事故现场的情况。
首先在本机安装FTP软件,我这里使用的Serv-U一个用得非常广泛的Ftp Server,准备好Ftp的目录,我这里使用自己放mp3的目录
其中有一个子目录 2
我们可以看到两个目录的内容截然不同以方便我们重现事故现场。
第二部我们建立一个Winform项目
之后在界面上用三个按钮来实现testCase
第三步是准备的重头戏,我们准备一个Ftp的操作类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.IO; namespace TestFtpWebRequest { class FtpHelper { public static void Upload(string Url, string LocalPath) { FileInfo f = new FileInfo(LocalPath); FtpWebRequest req = (FtpWebRequest)FtpWebRequest.Create(Url + "/" + f.Name); req.Credentials = new NetworkCredential("Test", "123456"); req.Method = WebRequestMethods.Ftp.UploadFile; req.UseBinary = true; req.ContentLength = f.Length; FtpWebResponse rep = (FtpWebResponse)req.GetResponse(); using (Stream s = req.GetRequestStream()) { using (FileStream fs = f.OpenRead()) { int readcount = 0; long totalread = 0; byte[] buffer = new byte[1024]; while (true) { readcount = fs.Read(buffer, 0, 1024); totalread += readcount; s.Write(buffer, 0, readcount); if (totalread >= f.Length) { break; } } } } } public static string[] List(string Url) { List<string> rs = new List<string>(); FtpWebRequest req = (FtpWebRequest)FtpWebRequest.Create(Url); req.Credentials = new NetworkCredential("Test", "123456"); req.Method = WebRequestMethods.Ftp.ListDirectory; FtpWebResponse rep = (FtpWebResponse)req.GetResponse(); long len = rep.ContentLength; using (Stream s = rep.GetResponseStream()) { MemoryStream ms = new MemoryStream(); int readcount = 0; long totalcount = 0; byte[] buffer = new byte[1024]; while (true) { readcount = s.Read(buffer, 0, 1024); totalcount += readcount; ms.Write(buffer, 0, readcount); if (totalcount >= len) { break; } } s.Close(); rep.Close(); string str = Encoding.Default.GetString(ms.ToArray()); StringReader sr = new StringReader(str); string line = string.Empty; while ((line = sr.ReadLine()) != null) { rs.Add(line); } } return rs.ToArray(); } } }
这里准备了两个操作方法,一个是读取文件列表,一个是上传文件
最后在按钮里写上调用的方法
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace TestFtpWebRequest { public partial class Form1 : Form { const string Path1 = @"D:\MyDocument\test.jpg"; const string Path2 = @"D:\MyDocument\yoxi.jpg"; const string FtpUrl = "ftp://192.168.212.102"; const string FtpUrl2 = "ftp://192.168.212.102/2"; public Form1() { InitializeComponent(); } private void button3_Click(object sender, EventArgs e) { textBox1.Text = string.Empty; string[] files = FtpHelper.List(FtpUrl); textBox1.Text = "file count:" + files.Length + "\r\n"; foreach (string s in files) { textBox1.Text += "ftp:" + s + "\r\n"; } } private void button1_Click(object sender, EventArgs e) { FtpHelper.Upload(FtpUrl2, Path1); MessageBox.Show("up ok!"); } private void button2_Click(object sender, EventArgs e) { FtpHelper.Upload(FtpUrl2, Path2); MessageBox.Show("up ok!"); } } }
好了,根据两个ftp的方法大家可以看到,从表面上两个方法应该是独立的,不管在什么时候,我如果按列表的按钮都应该返回Ftp根目录的文件列表出来。但是实际的执行结果,我们拭目以待。
首先我们执行程序,点击列表按钮(List with no upload),这个时候我们能够看到返回的确实是根目录的文件
现在我们继续点击 FirstUpload,按钮,猜猜结果怎么样? 报错了:
路径明明是对的,为什么报错?大家可以自己想想
接下来我们把顺序换一下。重新执行程序,这次先点击Firstupload,结果上传成功,结果如下
好,这个时候我们再点击列表按钮,本来应该显示根目录内容的,结果却显示的:
这下看明白了没?好的,接下来我们调整一下Ftp的设置,现在Ftp账号的设置如下:
我们现在把将用户锁定于主目录的勾去掉:
如此这般,好了,现在我们再次执行程序。
先点击列表,然后再点击上传,看看,没报错,成功了:
---------------------------------------------------------
一开始我怀疑是因为KeepAlive保持了链接造成的,因为这个结果非常像是每一个Request都复用了连接,所以我两个方法都加上了KeepAlive=false
然后恢复Serv-U的设置,将锁定用户于主目录的勾选上。
执行程序,点击列表,再点击上传,成功了,貌似没问题了吧。不过不要高兴得太早,这个时候再点击一次列表,看,报错了:
到现在为止,我应该对事故的由来,前因后果说得很清楚了。
这里我总结出了几点问题。
1.不管是不是要复用底层的链接,但是在语义上的一致性是需要保证的,一个Request,既然用url :Ftp://ip/创建出来的,那么就应该是操作Ftp的根而不是其他目录,如果这个都不能保证,那么谁还能信任这样的一个工具?更不用说多线程的环境下工作会如何了。
2.为了安全性,大部分ftp都会锁定用户到主目录,一个ftpclient怎么能够依赖于服务端的设置工作?无论是谁做出这种脑残设定都是不对的。
3.KeepAlive禁用后,居然同样的过程执行都会报错,不可理喻。
具体问题处在哪里,等我下次分析了。