IOCP分段发送的数据中间夹杂错误数据的问题

Gary@Tokyo 2010-11-04 05:20:14
服务器端是完成端口+多线程(4个线程)处理客户端:

客户端请求某个文件夹的目录结构
然后发送
数据包格式:1字节长度表示序号+1字节表示包长度+1字节指令+具体消息内容(文件夹信息)。
由于文件夹的信息大于我设定64K,所以分三段发,结果发现客户端收到的信息,中间有一段数据读出来是错误的,位置比较固定。




typedef struct _FileInfo
{
TCHAR szFileName[MAX_PATH];
DWORD dwFileID;
TCHAR cFileFlag;
TCHAR cFileType;
FILETIME ftLastWriteTime;
DWORD dwFileSizeHigh;
DWORD dwFileSizeLow;
DWORD dwFileCrc;
}FileInfo,*PFileInfo;



DWORD WINAPI CIOCPServer::_WorkerThreadProc(LPVOID lpParam)
{
CIOCPServer *pThis = (CIOCPServer*)lpParam;
PIOCP_BUF pBuffer;
PIOCP_CONTEXT pContext;
DWORD dwTrans = 0;
LPOVERLAPPED lpol;
BOOL bOK;

while (TRUE)
{
// ……………………

bOK = GetQueuedCompletionStatus(pThis->m_hCompletion,&dwTrans,(LPDWORD)&pContext,&lpol,WSA_INFINITE);
// 错误处理

pThis->HandleIO( pContext, pBuffer, dwTrans, nError );
}
return 0;
}

void CIOCPServer::HandleIO(PIOCP_CONTEXT pConTextKey, PIOCP_BUF pBuffer, DWORD dwTrans, int nError)
{
PIOCP_CONTEXT pContext;
pContext=pConTextKey;

if(pContext != NULL && (unsigned int )pContext != 0xfeeefeee )
{
if(pBuffer->IoOper == IoRecv)
::InterlockedDecrement(&pContext->nOutstandingRecv);
else if(pBuffer->IoOper == IoWrite)
::InterlockedDecrement(&pContext->nOutstandingSend);
}

if ( pBuffer != NULL && (unsigned int)pBuffer != 0xfeeefeee )
{
switch(pBuffer->IoOper)
{
// 其他无关代码省略…………
case IoRecv:
pContext=pConTextKey;
if (pContext == NULL || pBuffer == NULL)
break;
EnterCriticalSection(&pContext->Lock);
SendFileStructEx(pContext,pBuffer,pContext->sCurOption, _T("0102"));
LeaveCriticalSection(&pContext->Lock);
break;

case IoWrite:
pContext=pConTextKey;
EnterCriticalSection(&pContext->Lock);
pContext->sIsPostSend=0;//发送包完成
pContext->nSendCount+=dwTrans;

if (pContext->nSendCount = pBuffer->nBufLen)
{
pContext->nSendCount=0;
}

if( _tcsnccmp(pBuffer->pBuf+4, _T("0102"),4)==0 )
{
INT64 i64t=pContext->i64TotalByte - pContext->i64HavedByte;
if (i64t != 0 )
{
if( i64t < pBuffer->nBufLen - 12)
{
pBuffer->nBufLen = (int)(i64t + 12);
}

memcpy(pBuffer->pBuf+12,pContext->pPoint+pContext->i64HavedByte,pBuffer->nBufLen-12);
pContext->i64HavedByte+=pBuffer->nBufLen-12;
*(int*)pBuffer->pBuf=pBuffer->nBufLen-4;

if (!PostSend(pContext,pBuffer))
{
LeaveCriticalSection(&pContext->Lock);
return;
}
}
else
{
if (pContext->pPoint != NULL && pContext->bFree == FALSE)
{
free(pContext->pPoint);
pContext->pPoint=NULL;
}
}
}

LeaveCriticalSection(&pContext->Lock);
break;
default :
;
}
return ;
}
}

void CIOCPServer::SendFileStructEx(PIOCP_CONTEXT pContext,PIOCP_BUF pBuffer,TCHAR *sBrowserDir,TCHAR *sCmd)
{
u_long nTotalSize = 0;
int nFileCount = 0;
int nFileInfoSize = 0;
if (m_pFile == NULL)
{
return;
}

nFileInfoSize=sizeof(FileInfo);
int nFileIndex = 1;
BrowserFolderEx(sBrowserDir,&nFileCount,m_pFile,&m_pFile, nFileIndex ); //遍历目录
nFileIndex = 0;
nTotalSize=nFileCount*nFileInfoSize;
pContext->pPoint=(TCHAR*)m_pFile; // FileInfo 之前申请了一块内存
pContext->i64TotalByte=nTotalSize;

if(nTotalSize > BUFFER_SIZE -12)
nTotalSize=(BUFFER_SIZE -12)/nFileInfoSize*nFileInfoSize;
pContext->i64HavedByte=nTotalSize;

memset(&pBuffer->ol,0,sizeof(WSAOVERLAPPED));
pBuffer->ol.OffsetHigh = 0;
*(int*)(pBuffer->pBuf)=8+nTotalSize;
memcpy(pBuffer->pBuf+4,sCmd,4);
memcpy(pBuffer->pBuf+8,&nFileCount,4);
memcpy(pBuffer->pBuf+12,m_pFile,nTotalSize);
pBuffer->nBufLen=nTotalSize+12;

if (!PostSend(pContext,pBuffer))
{
LOG.LogF( _T("发送文件结构体信息失败 行号: %d 错误码: %d"), __LINE__,WSAGetLastError());
return;
}

PostRecv(pContext,pBuffer);
}


我测试时候,一个目录有485个文件,客户端接收的信息显示第370-396个文件信息是错误的,这段之前以及之后的都正确。

现在我判断还是发送错误,客户端读取是按格式读的,单线程,应当不会有错误。
...全文
140 16 打赏 收藏 转发到动态 举报
写回复
用AI写文章
16 条回复
切换为时间正序
请发表友善的回复…
发表回复
visualwind 2010-11-07
  • 打赏
  • 举报
回复
[Quote=引用 15 楼 itsgoodtobebad 的回复:]
飞狐你说的这种方法是否有实际使用过的?这个对我是一个新的思路啊,我一直在这个地方没想到好的办法。
如果是这样的话,设置一个发送队列,在主程序里只发送队列头的包。

然后在work线程里来继续发送?就是在GetQueuedCompletionStatus后来继续投递WSASend吗?
你看看是不是这样实现的:
if(PerIOData->OperationType==WSA_SEND)
{
if(BytesTransferred<PerIOData->DataBuf.len)
{
// 继续发送
}
else
{
// 发送下一个包
}
[/Quote]

对的,就是这个意思
白虹李李 2010-11-07
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 visualwind 的回复:]
引用 12 楼 sky04 的回复:
我将缓冲区从64K改成32K后好像没发生混乱

请问这种大的数据包,一次send发送不了,分多次发送,该怎么发送好点?
您所说的继续发送上次剩余的,不大明白怎么实现,麻烦明示一下,最好能有些代码,谢谢!


不一定每次都混乱,比如网络环境良好的情况下概率就很小。但是作为良好的程序来说应该去处理各种可能的情况。你可以先把要发送的数据放入一个队列,然……
[/Quote]

飞狐你说的这种方法是否有实际使用过的?这个对我是一个新的思路啊,我一直在这个地方没想到好的办法。
如果是这样的话,设置一个发送队列,在主程序里只发送队列头的包。

然后在work线程里来继续发送?就是在GetQueuedCompletionStatus后来继续投递WSASend吗?
你看看是不是这样实现的:
if(PerIOData->OperationType==WSA_SEND)
{
if(BytesTransferred<PerIOData->DataBuf.len)
{
// 继续发送
}
else
{
// 发送下一个包
}
}
白虹李李 2010-11-05
  • 打赏
  • 举报
回复
你的程序处理沾包不?
就是说你第一次收到了1.5个包,然后你按照你的协议处理了前面部分。
下一次你收到半个包,你按照协议去处理,结果发现长度很大(因为你读的位置根本不是长度)。就出问题了。
你说的这个应该是个抓包工具吧,我也没用过。
Gary@Tokyo 2010-11-05
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 visualwind 的回复:]

引用 12 楼 sky04 的回复:
我将缓冲区从64K改成32K后好像没发生混乱

请问这种大的数据包,一次send发送不了,分多次发送,该怎么发送好点?
您所说的继续发送上次剩余的,不大明白怎么实现,麻烦明示一下,最好能有些代码,谢谢!


不一定每次都混乱,比如网络环境良好的情况下概率就很小。但是作为良好的程序来说应该去处理各种可能的情况。你可以先把要发送的数据放入一个队列,……
[/Quote]
谢谢,我看看怎么把代码改改,现在这代码好像不好改
visualwind 2010-11-05
  • 打赏
  • 举报
回复
[Quote=引用 12 楼 sky04 的回复:]
我将缓冲区从64K改成32K后好像没发生混乱

请问这种大的数据包,一次send发送不了,分多次发送,该怎么发送好点?
您所说的继续发送上次剩余的,不大明白怎么实现,麻烦明示一下,最好能有些代码,谢谢!
[/Quote]

不一定每次都混乱,比如网络环境良好的情况下概率就很小。但是作为良好的程序来说应该去处理各种可能的情况。你可以先把要发送的数据放入一个队列,然后每次WSASend这个队列的第一个包,发送完成回调后判断是否发完了,如果发完了就把队列第一个包删掉,继续发第二个包。如果只发了一部分,就继续发第一个包剩余的部分。
Gary@Tokyo 2010-11-05
  • 打赏
  • 举报
回复
[Quote=引用 10 楼 visualwind 的回复:]

引用 5 楼 sky04 的回复:

引用 2 楼 visualwind 的回复:

两次send操作,一前一后也会造成数据混乱吗?


会混乱。因为send只是把数据放入发送队列,并不是立刻发送。如果前一个send包未发完数量就回调,系统会跟着发第二个send包,这时第一个包的一部分数据就在第二个数据包后面了
[/Quote]
我将缓冲区从64K改成32K后好像没发生混乱

请问这种大的数据包,一次send发送不了,分多次发送,该怎么发送好点?
您所说的继续发送上次剩余的,不大明白怎么实现,麻烦明示一下,最好能有些代码,谢谢!
Gary@Tokyo 2010-11-05
  • 打赏
  • 举报
回复
[Quote=引用 6 楼 hmm7e 的回复:]

代码没细看,应当没什么大问题.

1:把缓冲区改成4K,弄一个文件较少的(或者正好到临界点上).
2:把四个线程,改成一个线程,输出一下调试信息.
[/Quote]
分别改了试了试
将缓冲区改成32K就没出错,线程是4还是1没有影响
visualwind 2010-11-05
  • 打赏
  • 举报
回复
[Quote=引用 5 楼 sky04 的回复:]

引用 2 楼 visualwind 的回复:

两次send操作,一前一后也会造成数据混乱吗?
[/Quote]

会混乱。因为send只是把数据放入发送队列,并不是立刻发送。如果前一个send包未发完数量就回调,系统会跟着发第二个send包,这时第一个包的一部分数据就在第二个数据包后面了
野男孩 2010-11-05
  • 打赏
  • 举报
回复
太长了,只能帮顶了。

网络的问题,用抓包工具帮着定位吧。WireShark或者Ethereal啥的,先定位看是客户端发送错了,还是服务端接收有问题。
xili 2010-11-04
  • 打赏
  • 举报
回复
奇怪的数据包格式,
数据包格式:1字节长度表示序号+1字节表示包长度+1字节指令+具体消息内容(文件夹信息)。

从代码中没有看到这个所谓的数据包格式.
如果一个数据包表示一个FileInfo,那程序就问题. 你的FileInfo大小超出了 "1字节表示包长度"的 表达能力.
「已注销」 2010-11-04
  • 打赏
  • 举报
回复
代码没细看,应当没什么大问题.

1:把缓冲区改成4K,弄一个文件较少的(或者正好到临界点上).
2:把四个线程,改成一个线程,输出一下调试信息.

Gary@Tokyo 2010-11-04
  • 打赏
  • 举报
回复
[Quote=引用 2 楼 visualwind 的回复:]

用完成端口发送的时候,应该等一次发送完成后才能发下一包,在case IoWrite的时候要判断transferbytes是否等于请求发送的长度,如果小于的话还得继续发上次一次剩下的。
你这里case IoRecv的时候在SendFileStructEx,case IoWrite的时候也在PostSend,虽然加了临界区但是也还会造成投递了两个send操作,有可能导致发送出去的数据混乱。
[/Quote]
两次send操作,一前一后也会造成数据混乱吗?
Gary@Tokyo 2010-11-04
  • 打赏
  • 举报
回复
[Quote=引用 3 楼 itsgoodtobebad 的回复:]

重叠模型啊
如果只能发完一次再发下一次,就不叫重叠模型了,何必呢

其实我最开始也在这里疑惑,如果有没发完的部分怎么办。但如果发完一个再发下一个,那么重叠模型就没任何意义了啊。
我想如果出现了没发完的,估计要上层处理了。

我觉得问题在于没有处理沾包,造成你读到的长度不对。就是说你在错误的位置去读长度了。

不过你说是一个字节的长度,那么怎么会有4038啊,不是最大255吗?
[/Quote]
不知道,我不知道wireshark那个是啥意思,还不会用
白虹李李 2010-11-04
  • 打赏
  • 举报
回复
重叠模型啊
如果只能发完一次再发下一次,就不叫重叠模型了,何必呢

其实我最开始也在这里疑惑,如果有没发完的部分怎么办。但如果发完一个再发下一个,那么重叠模型就没任何意义了啊。
我想如果出现了没发完的,估计要上层处理了。

我觉得问题在于没有处理沾包,造成你读到的长度不对。就是说你在错误的位置去读长度了。

不过你说是一个字节的长度,那么怎么会有4038啊,不是最大255吗?
visualwind 2010-11-04
  • 打赏
  • 举报
回复
用完成端口发送的时候,应该等一次发送完成后才能发下一包,在case IoWrite的时候要判断transferbytes是否等于请求发送的长度,如果小于的话还得继续发上次一次剩下的。
你这里case IoRecv的时候在SendFileStructEx,case IoWrite的时候也在PostSend,虽然加了临界区但是也还会造成投递了两个send操作,有可能导致发送出去的数据混乱。
Gary@Tokyo 2010-11-04
  • 打赏
  • 举报
回复
调试了好多遍,没有发现哪里有问题,用wireshark抓了一下包,发现第二段数据有个突然变大的迹象

之前都是4038一段一段的,发了64K的样子,然后突然有段3万多长度的数据,然后又是4038
最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口(IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
1、本课程是一个干货课程,主要讲解如何封装服务器底层,使用Tcp/ip长连接,IDE使用vs2019 c++开发以及使用c++11的一些标准,跨平台windows和linux,服务器性能高效,单服务器压力测试上万无压力,服务器框架是经历过上线产品的验证,框架简单明了,不熟悉底层封装的人,半个小时就能完全掌握服务器框架上手写业务逻辑。2、本课程是一个底层服务器框架教程,主要是教会学员在windows或linux下如何封装一个高效的,避免踩坑的商业级框架,服务器底层使用初始化即开辟内存的技术,使用内存池,服务器运行期间内存不会溢出,非常稳定,同时服务器使用自定义哈希hashContainer,在处理新的连接,新的数据,新的封包,以及解包,发包,粘包的过程,哈希容器性能非常高效,增、删、查、改永远不会随着连接人数的上升而降低性能,增、删、查、改的复杂度永远都是恒定的O(1)。3、服务器底层封装没有使用任何第三方网络库以及任何第三方插件,自由度非常的高,出了任何BUG,你都有办法去修改,查找问题也非常方便,在windows下使用iocp,linux下使用epoll.4、讲解c++纯客户端,主要用于服务器之间通信,也就是说你想搭建多层结构的服务器,服务器与服务器之间使用socket通信。还可以使用c++客户端做压力测试,开辟多线程连接服务器,教程提供了压力测试,学员可以自己做压力测试服务器性能。5、赠送ue4和unity3d通信底层框架以及多人交互demo,登录,注册,玩家离开,同步主要是教会学员服务器与客户端如何交互。6、赠送c++连接mysql数据库框架demo,登录,注册,玩家离开数据持久化.7、服务器教程使用自定义通信协议,同时也支持protobuf,选择权在开发者自己手里,想用什么协议都可以,自由度高。8、服务器教程使用手动敲代码逐句讲解的方式开展教学课程。非喜勿喷,谢谢大家。9、服务器教程提供源码,大家可以在平台提供的地址下载或者联系我,服务器使用c++11部分标准,std::thread,条件变量,线程锁,智能指针等,需要学员具备一定c++知识,购买前请慎重考虑。

18,356

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 网络编程
c++c语言开发语言 技术论坛(原bbs)
社区管理员
  • 网络编程
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧