问题现象:
基于TCP的网络通讯一段时间后出现断网。时间不定,有时四五个小时,有时二三天才出现。当问题出现时,和外界PING不通,PING本机能通,其他计算机也PING不通本机。有时还会造成和本机建立TCP连接的计算机也出现这一现象。
解决的过程:
在解决过程中,我们用PE(ProcessExplorer.exe)工具发现打开的句柄数不断飚升,检查代码后,看到在连接服务器不成功的情况下,新建的SOCKET未关闭。于是,修正这个BUG。再试,句柄数不再一路飚升了,可是,运行一段时间后断网问题还是没有解决。
这下子胸闷了,没方向了......
这时,距此问题被找出的时间点已经过去了一个多月,不时有人来问什么时候出正式版本。心中那个急啊,但急不是办法,只能暗自对自己说,这个问题搞不定就不用混下去了,以此来鼓励自己。
有人说,这个断网问题会不会和网卡有关?于是,我们换了张网卡,经三天的连续运行测试,还是断网了,问题依旧,证明了这和网卡无关。这时,我们又回到了原来的问题上来:是外部环境的问题还是新开发软件的问题?显然,这个问题还是出在了新开发的软件上!
结合测试工程师发现的一个现象——这个断网问题常出现在网络通讯的服务端,回头重新审查所有和网络通讯有关的代码,终于发现几处可疑的代码:
可疑代码之一:
BOOL CBlockingSocket::Accept(CBlockingSocket& sConnect, LPSOCKADDR psa)
{
ASSERT(m_hSocket != INVALID_SOCKET);
ASSERT(sConnect.m_hSocket == INVALID_SOCKET);
int nLengthAddr = sizeof(SOCKADDR);
sConnect.m_hSocket = accept(m_hSocket, psa, &nLengthAddr);
if(sConnect == INVALID_SOCKET)
return FALSE;
m_RemoteSockAddr = *psa; //保存远程信息(UDP时无法得到远程信息)
return TRUE;
}
通读代码后,psa相关的代码本来的意思是想保存客户端的连接信息,方便以后使用。但事实上,这里的代码只在打开TCP侦听,并授受TCP连接时才会用到,和UDP无关。当在TCP建立连接后想得到客户端的连接信息时,也可以通过相关的函数来临时取得连接信息,用不着保存下来的m_RemoteSockAddr中的客户端连接信息。所以我们把代码改为:
BOOL CBlockingSocket::Accept(CBlockingSocket &sConnect)
{
ASSERT(m_hSocket != INVALID_SOCKET);
ASSERT(sConnect.m_hSocket == INVALID_SOCKET);
sConnect.m_hSocket = accept(m_hSocket, NULL, NULL); //这里有修改,去掉了第2、3个参数
if(sConnect.m_hSocket == INVALID_SOCKET)
{
int iLastError = WSAGetLastError();
ERRORLOG_FORMAT(("CBlockingSocket::Accept() accept() return INVALID_SOCKET, WSAGetLastError() return %d", iLastError));
return FALSE;
}
return TRUE;
}
可疑代码之二:
同样还是授受TCP连接的代码。
CSocketAgentPtr pNewSocket = pSocketServer->NewSocketAgent(pSocketServer);
if (pSocketServer->Accept(pNewSocket))
{
pNewSocket->Init(pSocketServer->m_iSendBufLen, pSocketServer->m_iReceiveBufLen);
//调用OnSocketConnect()之前,必须要将新建的SOCKET加进队列,否则会找不到
SocketAgentQueue.UpdateSocket(pNewSocket);
pSocketServer->OnSocketConnect(pNewSocket);
if( pNewSocket.Get() && !pNewSocket.Get()->IsClosed() )
{
tagReadingSocket *pReadingSocket = new tagReadingSocket(pNewSocket);
AfxBeginThread(fnReadingSocket, (LPVOID)pReadingSocket);
}
}
分析可能的原因是:当Accept函数调用出错时,新建的SOCKET没有关闭,也没有启动相应的线程为此SOCKET服务,于是这一新建的SOCKET一直僵死着,既不能传输数据,也无法关闭,最终导致资源耗尽而断网。根据这一思路,修改上述代码为
CSocketAgentPtr pNewSocket = pSocketServer->NewSocketAgent(pSocketServer);
if (!pSocketServer->Accept(pNewSocket)) //这里新增
pNewSocket->Close(TRUE); //这里新增
else
{
pNewSocket->Init(pSocketServer->m_iSendBufLen, pSocketServer->m_iReceiveBufLen);
//调用OnSocketConnect()之前,必须要将新建的SOCKET加进队列,否则会找不到
if( pNewSocket.Get()==NULL || pNewSocket.Get()->IsClosed() ) //这里新增
pNewSocket->Close(TRUE); //这里新增
else
{
SocketAgentQueue.UpdateSocket(pNewSocket);
pSocketServer->OnSocketConnect(pNewSocket);
tagReadingSocket *pReadingSocket = new tagReadingSocket(pNewSocket);
AfxBeginThread(fnReadingSocket, (LPVOID)pReadingSocket);
}
}
编译新版本,经测试工程师连续一周的运行测试,已经突破了原来四天内必然断网的记录。这是个好消息!
尽管问题得到解决,但心中的郁闷没有解决。因为同一套代码用VC6+SP6编译后已在上千台计算机上运行了数年,没有发现这一问题。现在用VC2008编译后的软件有此问题,实在想不通。如哪位同行能解释一二,将感激不尽。
(成海2010-11-22撰于上海)