当前位置:  编程技术>.net/c#/asp.net

基于一个应用程序多线程误用的分析详解

    来源: 互联网  发布时间:2014-10-19

    本文导语:  一、需求和初步实现很简单的一个windows服务:客户端连接邮件服务器,下载邮件(含附件)并保存为.eml格式,保存成功后删除服务器上的邮件。实现的伪代码大致如下: 代码如下:      public void Process()        {        ...

一、需求和初步实现
很简单的一个windows服务:客户端连接邮件服务器,下载邮件(含附件)并保存为.eml格式,保存成功后删除服务器上的邮件。实现的伪代码大致如下:

代码如下:

      public void Process()
        {
            var recordCount = 1000;//每次取出邮件记录数
            while (true)
            {
                using (var client = new Pop3Client())
                {
                    //1、建立连接,并进行身份认证
                    client.Connect(server, port, useSSL);
                    client.Authenticate(userName, pwd);

                    var messageCount = client.GetMessageCount(); // 邮箱中现有邮件数
                    if (messageCount > recordCount)
                    {
                        messageCount = recordCount;
                    }
                    if (messageCount < 1)
                    {
                        break;
                    }
                    var listAllMsg = new List(messageCount); //用于临时保存取出的邮件

                    //2、取出邮件后填充至列表,每次最多recordCount封邮件
                    for (int i = 1; i IPEndPoint.MaxPort || port < IPEndPoint.MinPort)
                throw new ArgumentOutOfRangeException("port");

            if (receiveTimeout < -1)
                throw new ArgumentOutOfRangeException("receiveTimeout");

            if (sendTimeout < -1)
                throw new ArgumentOutOfRangeException("sendTimeout");

            if (State != ConnectionState.Disconnected)
                throw new InvalidUseException("You cannot ask to connect to a POP3 server, when we are already connected to one. Disconnect first.");

            TcpClient clientSocket = new TcpClient();
            clientSocket.ReceiveTimeout = receiveTimeout;
            clientSocket.SendTimeout = sendTimeout;

            try
            {
                clientSocket.Connect(hostname, port);
            }
            catch (SocketException e)
            {
                // Close the socket - we are not connected, so no need to close stream underneath
                clientSocket.Close();

                DefaultLogger.Log.LogError("Connect(): " + e.Message);
                throw new PopServerNotFoundException("Server not found", e);
            }

            Stream stream;
            if (useSsl)
            {
                // If we want to use SSL, open a new SSLStream on top of the open TCP stream.
                // We also want to close the TCP stream when the SSL stream is closed
                // If a validator was passed to us, use it.
                SslStream sslStream;
                if (certificateValidator == null)
                {
                    sslStream = new SslStream(clientSocket.GetStream(), false);
                }
                else
                {
                    sslStream = new SslStream(clientSocket.GetStream(), false, certificateValidator);
                }
                sslStream.ReadTimeout = receiveTimeout;
                sslStream.WriteTimeout = sendTimeout;

                // Authenticate the server
                sslStream.AuthenticateAsClient(hostname);

                stream = sslStream;
            }
            else
            {
                // If we do not want to use SSL, use plain TCP
                stream = clientSocket.GetStream();
            }

            // Now do the connect with the same stream being used to read and write to
            Connect(stream, stream); //In/OutputStream属性初始化
        }


一下子看到了TcpClient对象,这个不就是基于Socket,通过Socket编程实现POP3协议操作指令吗?毫无疑问需要发起TCP连接,什么三次握手呀,发送命令操作服务器呀…一下子全想起来了。

我们知道一个TCP连接就是一个会话(Session),发送命令(比如获取和删除)需要通过TCP连接和邮件服务器通信。如果是多线程在一个会话上发送命令(比如获取(TOP或者RETR)、删除(DELE))操作服务器,这些命令的操作都不是线程安全的,这样很可能出现OutputStream和InputStream数据不匹配而相互打架的情况,这个很可能就是我们看到的日志里有乱码的原因。说到线程安全,突然恍然大悟,我觉得查收邮件应该也有问题。为了验证我的想法,我又查看了下GetMessage方法的源码:

代码如下:

        public Message GetMessage(int messageNumber)
        {
            AssertDisposed();

            ValidateMessageNumber(messageNumber);

            if (State != ConnectionState.Transaction)
                throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet");

            byte[] messageContent = GetMessageAsBytes(messageNumber);

            return new Message(messageContent);
        }


内部的GetMessageAsBytes方法最终果然还是走SendCommand方法:
代码如下:

      if (askOnlyForHeaders)
            {
                // 0 is the number of lines of the message body to fetch, therefore it is set to zero to fetch only headers
                SendCommand("TOP " + messageNumber + " 0");
            }
            else
            {
                // Ask for the full message
                SendCommand("RETR " + messageNumber);
            }

根据我的跟踪,在测试中抛出异常的乱码来自于LastServerResponse(This is the last response the server sent back when a command was issued to it),在IsOKResponse方法中它不是以“+OK”开头就会抛出PopServerException异常:
代码如下:

    ///
        /// Tests a string to see if it is a "+OK" string.

        /// An "+OK" string should be returned by a compliant POP3
        /// server if the request could be served.

        ///

        /// The method does only check if it starts with "+OK".
        ///
        /// The string to examine
        /// Thrown if server did not respond with "+OK" message
        private static void IsOkResponse(string response)
        {
            if (response == null)
                throw new PopServerException("The stream used to retrieve responses from was closed");

            if (response.StartsWith("+OK", StringComparison.OrdinalIgnoreCase))
                return;

            throw new PopServerException("The server did not respond with a +OK response. The response was: "" + response + """);
        }


分析到这里,终于知道最大的陷阱是Pop3Client不是线程安全的。终于找到原因了,哈哈哈,此刻我犹如见到女神出现一样异常兴奋心花怒放,高兴的差点忘了错误的代码就是自己写的。

片刻后终于冷静下来,反省自己犯了很低级的失误,晕死,我怎么把TCP和线程安全这茬给忘了呢?啊啊啊啊啊啊,好累,感觉再也不会用类库了。

对了,保存为.eml的时候是通过Message对象的SaveToFile方法,并不需要和邮件服务器通信,所以异步保存没有出现异常(二进制数组RawMessage也不会数据不匹配),它的源码是下面这样的:

代码如下:

      ///
        /// Save this to a file.

        ///

        /// Can be loaded at a later time using the method.
        ///
        /// The File location to save the to. Existent files will be overwritten.
        /// If is
        /// Other exceptions relevant to file saving might be thrown as well
        public void SaveToFile(FileInfo file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            File.WriteAllBytes(file.FullName, RawMessage);
        }


再来总结看看这个bug是怎么产生的:对TCP和线程安全没有保持足够的敏感和警惕,看见for循环就进行性能调优,测试数据不充分,不小心触雷。归根结底,产生错误的原因是对线程安全考虑不周异步场景选择不当,这种不当的使用还有很多,比较典型的就是对数据库连接的误用。我看过一篇讲数据库连接对象误用的文章,比如这一篇《解析为何要关闭数据库连接,可不可以不关闭的问题详解》,当时我也总结过,所以很有印象。现在还是要罗嗦一下,对于using一个Pop3Client或者SqlConnection这种方式共用一个连接访问网络的情况可能不适合使用多线程,尤其是和服务器进行密集通信的时候,哪怕用对了多线程技术,性能也不见得有提升。

我们经常使用的一些Libray或者.NET客户端,比如FastDFS、Memcached、RabbitMQ、Redis、MongDB、Zookeeper等等,它们都要访问网络和服务器通信并解析协议,分析过几个客户端的源码,记得FastDFS,Memcached及Redis的客户端内部都有一个Pool的实现,印象中它们就没有线程安全风险。依个人经验,使用它们的时候必须保持敬畏之心,也许你用的语言和类库编程体验非常友好,API使用说明通俗易懂,调用起来看上去轻而易举,但是要用好用对也不是全部都那么容易,最好快速过一遍源码理解大致实现思路,否则如不熟悉内部实现原理埋头拿过来即用很可能掉入陷阱当中而不自知。当我们重构或调优使用多线程技术的时候,绝不能忽视一个深刻的问题,就是要清醒认识到适合异步处理的场景,就像知道适合使用缓存场景一样,我甚至认为明白这一点比怎么写代码更重要。还有就是重构或调优必须要谨慎,测试所依赖的数据必须准备充分,实际工作当中这一点已经被多次证明,给我的印象尤其深刻。很多业务系统数据量不大的时候都可以运行良好,但在高并发数据量较大的环境下很容易出现各种各样莫名其妙的问题,比如本文中所述,在测试多线程异步获取和删除邮件的时候,邮件服务器上只有一两封内容和附件很小的邮件,通过异步获取和删除都正常运行,没有任何异常日志,但是数据一多,出现异常日志,排查,调试,看源码,再排查......这篇文章就面世了。


    
 
 
 
本站(WWW.)旨在分享和传播互联网科技相关的资讯和技术,将尽最大努力为读者提供更好的信息聚合和浏览方式。
本站(WWW.)站内文章除注明原创外,均为转载、整理或搜集自网络。欢迎任何形式的转载,转载请注明出处。












  • 相关文章推荐
  • 深入C#任务管理器中应用程序选项隐藏程序本身的方法详解
  • Win32应用程序(SDK)设计原理详解
  • 基于Windows C++ 应用程序通用日志组件的使用详解
  • 重装服务器后IIS网站错误(应用程序中的服务器错误)
  • 如何将应用程序加到桌面或应用程序组?
  • 怎样开发在LINUX 上运行的应用程序,像WINDOWS桌面应用程序一样
  • 我要监测一台远程电脑的状态(未上线/上线但没打开每个应用程序/上线且打开应用程序),该如何作?
  • asp.net应用程序的生命周期和iis应用程序池
  • 手动执行应用程序ok,但用crontab(在正确的用户名下)运行应用程序就报-12545(tns连接错误),怎么解决?
  • 一个静态库包含多个函数,应用程序连接了库中的某个函数,应用程序目标代码中是否还包含了该静态库中的其他函数代码?
  • 终端打开应用程序,怎样使当终端退出时应用程序不退出.问了好多人,其实很简单.
  • linux 桌面应用程序和web应用程序编写常用的语言
  • 用SecureCRT或Putty 远程启动linux服务器上的一个应用程序,但是当我关掉SecureCRT的时候,应用程序也被关掉了,怎么能够做到我关闭客户
  • QT的应用程序中如何获取程序执行的路径?
  • 请问如何通过telnet的方式启动服务器(solaris)上的用.sh角本方式启动java写的应用程序,在退出telnet时服务器上的应用程序不会退出?
  • 把java源程序生成应用程序有哪些方法?
  • 菜鸟求助:Linux 应用程序后台启动后关闭窗口程序退出
  • 一个程序能否控制其他应用程序?
  • 运行什么程序都提示没有找到msvbvm5.0.dll,因此这个应用程序未能启动
  • 走虚拟网卡内核程序和走物理网卡应用程序结合问题
  • 学了linux程序设计后能不能编写出应用程序
  • 请问如何设置驱动程序和应用程序的启动顺序和优先级呢?
  • 请问能否在linux实现一个应用程序访问另外一个程序的内存数据?
  • java.exe-应用程序错误(程序一运行就报错)


  • 站内导航:


    特别声明:169IT网站部分信息来自互联网,如果侵犯您的权利,请及时告知,本站将立即删除!

    ©2012-2021,,E-mail:www_#163.com(请将#改为@)

    浙ICP备11055608号-3