zoukankan      html  css  js  c++  java
  • FtpDataStream中的隐藏问题

    最近在使用FtpWebResponse.GetResponseStream方法时遇上个问题——Stream在未关闭之前就报出了ObjectDisposedException。刚开始十分困惑,因为一直用的是类似的写法,从逻辑上不应该会出现异常。之后通过ILSpy工具查看源代码以及网上找寻相关资料才找到了原因,故在此作文,以作分享。

    我最初的代码是这样写的:

    FtpWebRequest request = (FtpWebRequest)WebRequest.Create(param.URL);
    request.Credentials = new NetworkCredential(param.User, param.PassWord);
    request.Method = WebRequestMethods.Ftp.DownloadFile;
    
    using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
    {
        using (StreamReader r = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("gb2312")))
        {
            List<string> data = new List<string>();
            string line = r.ReadLine();
            while (line != null)
            {
                data.Add(line);
                line = r.ReadLine();
            }
        }
    }
    

    想法是按行读取下载的文件并保存在一个List中,以作他用。

    但这段代码在读取某些文件的时候出错了, line = r.ReadLine(); 执行至此行代码时会抛出ObjectDisposedException异常。

    这十分奇怪,因为从代码逻辑上看Stream在此时显然仍未被关闭。

    但在查看源代码后,豁然发现我错了,FtpDataStream在读取时是会自己关闭Stream的。

    FtpDataStream的Read方法源码:

    public override int Read(byte[] buffer, int offset, int size)
    {
        this.CheckError();
        int num;
        try
        {
            num = this.m_NetworkStream.Read(buffer, offset, size);
        }
        catch
        {
            this.CheckError();
            throw;
        }
        if (num == 0)
        {
            this.m_IsFullyRead = true;
            this.Close();
        }
        return num;
    }
    

    当其内部NetworkStream读取的结果为0时,其本身会自动Close。*这是FtpDataStream特有的操作方式,其它Stream不会这样做。

    但如果仅是这样的话,还是无法解释异常的出现,因为若是NetworkStream读取不到字节的情况下,循环中的判断 while (line != null) 也应该能保证不再执行ReadLine操作。

    那么再看Stream的ReadLine方法源码:

    public override string ReadLine()
    {
    	if (this.stream == null)
    	{
    		__Error.ReaderClosed();
    	}
    	this.CheckAsyncTaskInProgress();
    	if (this.charPos == this.charLen && this.ReadBuffer() == 0)
    	{
    		return null;
    	}
    	StringBuilder stringBuilder = null;
    	int num;
    	char c;
    	while (true)
    	{
    		num = this.charPos;
    		do
    		{
    			c = this.charBuffer[num];
    			if (c == '
    ' || c == '
    ')
    			{
    				goto IL_4A;
    			}
    			num++;
    		}
    		while (num < this.charLen);
    		num = this.charLen - this.charPos;
    		if (stringBuilder == null)
    		{
    			stringBuilder = new StringBuilder(num + 80);
    		}
    		stringBuilder.Append(this.charBuffer, this.charPos, num);
    		if (this.ReadBuffer() <= 0)
    		{
    			goto Block_11;
    		}
    	}
    	IL_4A:
    	string result;
    	if (stringBuilder != null)
    	{
    		stringBuilder.Append(this.charBuffer, this.charPos, num - this.charPos);
    		result = stringBuilder.ToString();
    	}
    	else
    	{
    		result = new string(this.charBuffer, this.charPos, num - this.charPos);
    	}
    	this.charPos = num + 1;
    	if (c == '
    ' && (this.charPos < this.charLen || this.ReadBuffer() > 0) && this.charBuffer[this.charPos] == '
    ')
    	{
    		this.charPos++;
    	}
    	return result;
    	Block_11:
    	return stringBuilder.ToString();
    }
    

    此方法中不会抛出异常,但若是执行到其中的ReadBuffer方法代码,就会调用内部成员stream的Read方法,即FtpDataStream的Read方法,则可能会出现问题。

    使用ReadBuffer方法的地方共有两处,第一处在完成读取文件时一定会被调用,ReadLine在此处返回null,以说明所有读取操作已经完成,不要再继续读文件。此处被执行一次且只被执行一次。(在加了类似"line != null"判断的前提下)

    第二处就不一样了,如果行末是' '或 '字符结尾的话,则第二处ReadBuffer方法不会被执行。

    若仅执行一次ReadBuffer,即只调用一次FtpDataStream的Read方法,那么即使发生最糟的结果,Stream关闭,也不会抛出异常。而如果被执行了两次,并且第一次时已读不到字节,那么由于Stream会被关闭,所以必然会出现异常。

    仔细看一下 while (true) 循环中的代码,在行末不是' '或 '字符结尾的场合,只有ReadBuffer的返回结果是小于等于零的前提下才能退出这个循环,即FtpDataStream必定被Close。于是当最后一次读取文件时,因为必定要执行第一处的ReadBuffer方法,那么抛出异常便是无法避免的。

    所以到了这里,可以大胆判断只有文件末尾没有EOL标识的才会出现因提前关闭Stream而产生的异常。(EOL指 , 这样的行结束标识符)

    对比了下运行成功的文件与失败的文件内容,果然失败的文件末是没有 的。

    当找到原因后解决问题就简单多了,网上即有现成的方法,创建继承Stream的子类,然后重写ReadLine方法:

    public override String ReadLine()
    {
        try
        {
            return base.ReadLine();
        }
        catch (ObjectDisposedException ode)
        {
            return null; 
        }
    }
    

    或者更简单的改法,直接在原来的代码上加try/catch:

    string line = r.ReadLine();
    while (line != null)
    {
        data.Add(line);
        try
        {
            line = r.ReadLine();
        }
        catch(ObjectDisposedException ode)
        {
            line = null;
        }
    }
    

    如果不喜欢这样的解决方法,也可以弃用ReadLine方法,改为使用ReadToEnd方法。在读完文件后通过Split方法再保存为List。

    string allData = r.ReadToEnd();
    List<string> data = allData.Split(new string[] { "
    " }, StringSplitOptions.RemoveEmptyEntries).ToList();
    

    最后需要指出的是,这个问题确实是个bug,而且早在2010年就已被用户发现并提交给微软,但最终的结果是微软不会修复这个bug——

    "Thank you for your feedback. You are correct, Read does call Close when it will return 0. We have examined this issue and determined that fixing it would have a negative impact on existing applications."

    https://connect.microsoft.com/VisualStudio/feedback/details/594446/ftpdatastream-read-closes-stream

  • 相关阅读:
    hdu 5902 Seam Carving
    hdu 5091 Beam Cannon
    hdu 1542 Atlantis
    hdu 2196 Computer
    第一个爬虫和测试
    排球比赛规则
    第十周博客作业
    科学计算可视化
    用matplotlib绘制图像
    面对对象学习
  • 原文地址:https://www.cnblogs.com/sjyforg/p/3898908.html
Copyright © 2011-2022 走看看