一、需求和初步实现
很简单的一个windows服务:客户端连接邮件服务器,下载邮件(含附件)并保存为.eml格式,保存成功后删除服务器上的邮件。实现的伪代码大致如下:
代码如下:
publicvoidProcess()
{
varrecordCount=1000;//每次取出邮件记录数
while(true)
{
using(varclient=newPop3Client())
{
//1、建立连接,并进行身份认证
client.Connect(server,port,useSSL);
client.Authenticate(userName,pwd);
varmessageCount=client.GetMessageCount();//邮箱中现有邮件数
if(messageCount>recordCount)
{
messageCount=recordCount;
}
if(messageCount<1)
{
break;
}
varlistAllMsg=newList<Message>(messageCount);//用于临时保存取出的邮件
//2、取出邮件后填充至列表,每次最多recordCount封邮件
for(inti=1;i<=messageCount;i++)//邮箱索引是基于1开始的,索引范围:[1,messageCount]
{
listAllMsg.Add(client.GetMessage(i));//取出邮件至列表
}
//3、遍历并保存至客户端,格式为.eml
foreach(varmessageinlistAllMsg)
{
varemlInfo=newSystem.IO.FileInfo(string.Format(“{0}.eml”,Guid.NewGuid().ToString(“n”)));
message.SaveToFile(emlInfo);//保存邮件为.eml格式文件
}
//4、遍历并删除
intmessageNumber=1;
foreach(varmessageinlistAllMsg)
{
client.DeleteMessage(messageNumber);//删除邮件(本质上,在关闭连接前只是打上DELETE标签,并没有真正删除)
messageNumber++;
}
//5、断开连接,真正完成删除
client.Disconnect();
if(messageCount<recordCount)
{
break;
}
}
}
}
开发中接收邮件的时候使用了开源组件Mail.Net(实际上这是OpenSMTP.Net和OpenPop两个项目的并集),调用接口实现很简单。代码写完后发现基本功能是满足了,本着在稳定的基础上更快更有效率的原则,最终进行性能调优。
二、性能调优及产生BUG分析
暂时不管这里的耗时操作是属于计算密集型还是IO密集型,反正有人一看到有集合要一个一个遍历顺序处理,就忍不住有多线程异步并行操作的冲动。有条件异步尽量异步,没有条件异步,创造条件也要异步,真正发挥多线程优势,充分利用服务器的强大处理能力,而且也自信中规中矩写了很多多线程程序,这个业务逻辑比较简单而且异常处理也较容易控制(就算有问题也有补偿措施,可以在后期处理中完善它),理论上每天需要查收的邮件的数量也不会太多,不会长时间成为CPU和内存杀手,这样的多线程异步服务实现应该可以接受。而且根据分析,显而易见,这是一个典型的频繁访问网络IO密集型的应用程序,当然要从IO处理上下功夫。
1、收取邮件
从Mail.Net的示例代码中看到,取邮件需要一个从1开始的索引,而且必须有序。如果异步发起多个请求,这个索引怎么传入呢?必须有序这一条开始让我有点犹豫,如果通过Lock或者Interlocked等同步构造,很显然就失去了多线程的优势,我猜可能还不如顺序同步获取速度快。
分析归分析,我们还是写点代码试试看效率如何。
快速写个异步方法传递整型参数,同时通过Interlocked控制提取邮件总数的变化,每一个异步方法获取完了之后通过Lock将Message加入到listAllMsg列表中即可。
邮件服务器测试邮件不多,测试获取一两封邮件,嗯,很好,提取邮件成功,初步调整就有收获,可喜可贺。
2、保存邮件
调优过程是这样的:遍历并保存为.eml的实现代码改为使用多线程,将message.SaveToFile保存操作并行处理,经测试,保存一到两封邮件,CPU没看出高多少,保存的效率貌似稍有提升,又有点进步。
3、删除邮件
再次调优:仿照多线程保存操作,将遍历删除邮件的代码进行修改,也通过多线程并行处理删除的操作。好,很好,非常好,这时候我心里想着什么Thread啊,ThreadPool啊,CCR啊,TPL啊,EAP啊,APM啊,把自己知道的能用的全给它用一遍,挑最好用的最优效率的一个,显得很有技术含量,哇哈哈。
然后,快速写了个异步删除方法开始测试。在邮件不多的情况下,比如三两封信,能正常工作,看起来好像蛮快的。
到这里我心里已经开始准备庆祝大功告成了。
4、产生BUG原因分析
从上面的1、2、3独立效果看,似乎每一个线程都能够独立运行而不需要相互通信或者数据共享,而且使用了异步多线程技术,取的快存的快删的也快,看上去邮件处理将进入最佳状态。但是最后提取、保存、删除集成联调测试。运行了一段时间查看日志,悲剧发生了:
在测试邮件较多的时候,比如二三十封左右,日志里看到有PopServerException异常,好像还有点乱码,而且每次乱码好像还不一样;再测试三两封信,发现有时能正常工作,有时也抛出PopServerException异常,还是有乱码,分析出错堆栈,是在删除邮件的地方。
我kao,这是要闹哪样啊,和邮件服务器关系没搞好吗,怎么总是PopServerException异常?
难道,难道是异步删除方法有问题?异步删除,索引为1的序号,嗯,索引的问题?还是不太确定。
到这里你能发现多线程处理删除操作抛出异常的原因吗?你已经知道原因了?OK,下面的内容对你就毫无意义了,可以不用往下看了。
谈谈我的排查经过。
看日志我初步怀疑是删除邮件的方法有问题,但是看了一下目测还是可靠的。接着估计是删除时邮件编码不正确,后来又想不太可能,同样的邮件同步代码查收保存删除这三个操作就没有异常抛出。不太放心,又分几次分别测试了几封邮件,有附件的没附件的,html的纯文本的,同步代码处理的很好。
百思不得其解,打开Mail.NET源码,从DeleteMessage方法跟踪查看到Mail.Net的Pop3Client类中的SendCommand方法,一下子感觉有头绪了。DeleteMessage删除邮件的源码如下:
代码如下:
publicvoidDeleteMessage(intmessageNumber)
{
AssertDisposed();
ValidateMessageNumber(messageNumber);
if(State!=ConnectionState.Transaction)
thrownewInvalidUseException(“Youcannotdeleteanymessageswithoutauthenticatingyourselftowardstheserverfirst”);
SendCommand(“DELE”+messageNumber);
}
最后一行SendCommand需要提交一个DELE命令,跟进去看看它是怎么实现的:
代码如下:
privatevoidSendCommand(stringcommand)
{
//ConvertthecommandwithCRLFafterwardsasperRFCtoabytearraywhichwecanwrite
byte[]commandBytes=Encoding.ASCII.GetBytes(command+”rn”);
//Writethecommandtotheserver
OutputStream.Write(commandBytes,0,commandBytes.Length);
OutputStream.Flush();//Flushthecontentaswenowwaitforaresponse
//Readtheresponsefromtheserver.TheresponseshouldbeinASCII
LastServerResponse=StreamUtility.ReadLineAsAscii(InputStream);
IsOkResponse(LastServerResponse);
}
注意InputStream和OutputStream属性,它们的定义如下(神奇的private修饰属性,这种写法少见哪):
代码如下:
///<summary>
///Thisisthestreamusedtoreadofftheserverresponsetoacommand
///</summary>
privateStreamInputStream{get;set;}
///<summary>
///Thisisthestreamusedtowritecommandstotheserver
///</summary>
privateStreamOutputStream{get;set;}
给它赋值的地方是调用Pop3Client类里的publicvoidConnect(StreaminputStream,StreamoutputStream)方法,而这个Connect方法最终调用的Connect方法如下:
代码如下:
///<summary>
///ConnectstoaremotePOP3server
///</summary>
///<paramname=”hostname”>The<paramrefname=”hostname”/>ofthePOP3server</param>
///<paramname=”port”>TheportofthePOP3server</param>
///<paramname=”useSsl”>TrueifSSLshouldbeused.FalseifplainTCPshouldbeused.</param>
///<paramname=”receiveTimeout”>Timeoutinmillisecondsbeforeasocketshouldtimeoutfromreading.Setto0or-1tospecifyinfinitetimeout.</param>
///<paramname=”sendTimeout”>Timeoutinmillisecondsbeforeasocketshouldtimeoutfromsending.Setto0or-1tospecifyinfinitetimeout.</param>
///<paramname=”certificateValidator”>IfyouwanttovalidatethecertificateinaSSLconnection,passareferencetoyourvalidator.Supply<seelangword=”null”/>ifdefaultshouldbeused.</param>
///<exceptioncref=”PopServerNotAvailableException”>IftheserverdidnotsendanOKmessagewhenaconnectionwasestablished</exception>
///<exceptioncref=”PopServerNotFoundException”>Ifitwasnotpossibletoconnecttotheserver</exception>
///<exceptioncref=”ArgumentNullException”>If<paramrefname=”hostname”/>is<seelangword=”null”/></exception>
///<exceptioncref=”ArgumentOutOfRangeException”>Ifportisnotintherange[<seecref=”IPEndPoint.MinPort”/>,<seecref=”IPEndPoint.MaxPort”/>orifanyofthetimeoutsislessthan-1.</exception>
publicvoidConnect(stringhostname,intport,booluseSsl,intreceiveTimeout,intsendTimeout,RemoteCertificateValidationCallbackcertificateValidator)
{
AssertDisposed();
if(hostname==null)
thrownewArgumentNullException(“hostname”);
if(hostname.Length==0)
thrownewArgumentException(“hostnamecannotbeempty”,”hostname”);
if(port>IPEndPoint.MaxPort||port<IPEndPoint.MinPort)
thrownewArgumentOutOfRangeException(“port”);
if(receiveTimeout<-1)
thrownewArgumentOutOfRangeException(“receiveTimeout”);
if(sendTimeout<-1)
thrownewArgumentOutOfRangeException(“sendTimeout”);
if(State!=ConnectionState.Disconnected)
thrownewInvalidUseException(“YoucannotasktoconnecttoaPOP3server,whenwearealreadyconnectedtoone.Disconnectfirst.”);
TcpClientclientSocket=newTcpClient();
clientSocket.ReceiveTimeout=receiveTimeout;
clientSocket.SendTimeout=sendTimeout;
try
{
clientSocket.Connect(hostname,port);
}
catch(SocketExceptione)
{
//Closethesocket-wearenotconnected,sononeedtoclosestreamunderneath
clientSocket.Close();
DefaultLogger.Log.LogError(“Connect():”+e.Message);
thrownewPopServerNotFoundException(“Servernotfound”,e);
}
Streamstream;
if(useSsl)
{
//IfwewanttouseSSL,openanewSSLStreamontopoftheopenTCPstream.
//WealsowanttoclosetheTCPstreamwhentheSSLstreamisclosed
//Ifavalidatorwaspassedtous,useit.
SslStreamsslStream;
if(certificateValidator==null)
{
sslStream=newSslStream(clientSocket.GetStream(),false);
}
else
{
sslStream=newSslStream(clientSocket.GetStream(),false,certificateValidator);
}
sslStream.ReadTimeout=receiveTimeout;
sslStream.WriteTimeout=sendTimeout;
//Authenticatetheserver
sslStream.AuthenticateAsClient(hostname);
stream=sslStream;
}
else
{
//IfwedonotwanttouseSSL,useplainTCP
stream=clientSocket.GetStream();
}
//Nowdotheconnectwiththesamestreambeingusedtoreadandwriteto
Connect(stream,stream);//In/OutputStream属性初始化
}
一下子看到了TcpClient对象,这个不就是基于Socket,通过Socket编程实现POP3协议操作指令吗?毫无疑问需要发起TCP连接,什么三次握手呀,发送命令操作服务器呀…一下子全想起来了。
我们知道一个TCP连接就是一个会话(Session),发送命令(比如获取和删除)需要通过TCP连接和邮件服务器通信。如果是多线程在一个会话上发送命令(比如获取(TOP或者RETR)、删除(DELE))操作服务器,这些命令的操作都不是线程安全的,这样很可能出现OutputStream和InputStream数据不匹配而相互打架的情况,这个很可能就是我们看到的日志里有乱码的原因。说到线程安全,突然恍然大悟,我觉得查收邮件应该也有问题。为了验证我的想法,我又查看了下GetMessage方法的源码:
代码如下:
publicMessageGetMessage(intmessageNumber)
{
AssertDisposed();
ValidateMessageNumber(messageNumber);
if(State!=ConnectionState.Transaction)
thrownewInvalidUseException(“Cannotfetchamessage,whentheuserhasnotbeenauthenticatedyet”);
byte[]messageContent=GetMessageAsBytes(messageNumber);
returnnewMessage(messageContent);
}
内部的GetMessageAsBytes方法最终果然还是走SendCommand方法:
代码如下:
if(askOnlyForHeaders)
{
//0isthenumberoflinesofthemessagebodytofetch,thereforeitissettozerotofetchonlyheaders
SendCommand(“TOP”+messageNumber+”0”);
}
else
{
//Askforthefullmessage
SendCommand(“RETR”+messageNumber);
}
根据我的跟踪,在测试中抛出异常的乱码来自于LastServerResponse(Thisisthelastresponsetheserversentbackwhenacommandwasissuedtoit),在IsOKResponse方法中它不是以“+OK”开头就会抛出PopServerException异常:
代码如下:
///<summary>
///Testsastringtoseeifitisa”+OK”string.<br/>
///An”+OK”stringshouldbereturnedbyacompliantPOP3
///serveriftherequestcouldbeserved.<br/>
///<br/>
///Themethoddoesonlycheckifitstartswith”+OK”.
///</summary>
///<paramname=”response”>Thestringtoexamine</param>
///<exceptioncref=”PopServerException”>Thrownifserverdidnotrespondwith”+OK”message</exception>
privatestaticvoidIsOkResponse(stringresponse)
{
if(response==null)
thrownewPopServerException(“Thestreamusedtoretrieveresponsesfromwasclosed”);
if(response.StartsWith(“+OK”,StringComparison.OrdinalIgnoreCase))
return;
thrownewPopServerException(“Theserverdidnotrespondwitha+OKresponse.Theresponsewas:””+response+”””);
}
分析到这里,终于知道最大的陷阱是Pop3Client不是线程安全的。终于找到原因了,哈哈哈,此刻我犹如见到女神出现一样异常兴奋心花怒放,高兴的差点忘了错误的代码就是自己写的。
片刻后终于冷静下来,反省自己犯了很低级的失误,晕死,我怎么把TCP和线程安全这茬给忘了呢?啊啊啊啊啊啊,好累,感觉再也不会用类库了。
对了,保存为.eml的时候是通过Message对象的SaveToFile方法,并不需要和邮件服务器通信,所以异步保存没有出现异常(二进制数组RawMessage也不会数据不匹配),它的源码是下面这样的:
代码如下:
///<summary>
///Savethis<seecref=”Message”/>toafile.<br/>
///<br/>
///Canbeloadedatalatertimeusingthe<seecref=”LoadFromFile”/>method.
///</summary>
///<paramname=”file”>TheFilelocationtosavethe<seecref=”Message”/>to.Existentfileswillbeoverwritten.</param>
///<exceptioncref=”ArgumentNullException”>If<paramrefname=”file”/>is<seelangword=”null”/></exception>
///<exception>Otherexceptionsrelevanttofilesavingmightbethrownaswell</exception>
publicvoidSaveToFile(FileInfofile)
{
if(file==null)
thrownewArgumentNullException(“file”);
File.WriteAllBytes(file.FullName,RawMessage);
}
再来上述就是C#学习教程:基于一个应用程序多线程误用的分析详解分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/cdevelopment/904673.html