关于Socket,服务器端向客户端连续发数据,客户端无法完全接受的问题!

zzutligang 2008-09-15 03:42:27
我用socket写了一个网络通信的程序。其中涉及到这样的功能。
客户端向服务器发送一个请求,服务器端接受到这个请求后,会告诉客户下面即将发送的数据的大小。然后,服务器端就开始不停地向客户端发送数据。直到把该发送的数据发送完。然后发送一个标记,告诉客户端,数据已经发完了。
我用的tcp方式。
而客户端发送完请求后,就开始接受数据,第一个接收到的,就是服务器端发送过来的消息,告诉客户端下面即将发送过来的数据大小。然后客户端就在一个循环中接受数据,接收到来的数据会被存在内存中或者写到文件里。直到客户端收到服务器发送来的结束的标记,则停止接受数据。
我的问题是。在服务器端不停发数据的时候,客户端会出现无法完全接收数据的情况。
我试着修改每次发送数据的大小,也没有办法让客户端完整接收到数据。
我又试着让服务器每发送一次数据,就sleep一会儿,但好像也没有办法保证客户端一定能准确接收到所有数据。
后来,我在网上搜了一下,说加大客户端socket地接受缓存区大小,好像效果也不是也明显,尤其是在服务器端需要发送的数据达到好几兆时,则客户端几乎没有一次能完整接受的。
tcp数据传输,不是说是可靠传输吗?怎么服务器发送的数据,客户端就没有办法完全接受到呢。
...全文
945 21 打赏 收藏 转发到动态 举报
写回复
用AI写文章
21 条回复
切换为时间正序
请发表友善的回复…
发表回复
zzutligang 2008-09-17
  • 打赏
  • 举报
回复
昨晚搞了很晚,将发送接受模式改成一问一答的形式。
也就是客户端发送请求数据的请求,服务器端收到后,就发送一块数据。
这样一问一答的形式,倒是解决了我的问题。包括每次send的数据10K,客户端都能正确接受。我把程序改成一问一答的形式后做了大数据量测试,连续运行了一夜,传递的数据大约已经有上百GB了,系统还算稳定,内存使用也没有增加。
但有一点不是很爽,好像这种一问一答的方式并没有充分利用网络带宽,所以,网络传送速度并不是很理想。
有待于一步优化了。
各位有没有更好的解决方法?劳驾指导一下!
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
回10楼,我从来没有怀疑socket有问题。我只是在找问题的所在,目的是为了解决问题。
回11楼,我上午又做了大量的实验,实在无法解决问题。
我在服务器端进行了跟踪,当发送几个包后,再send确实会返回-1,也就是说发送失败了,但我用WSAGetLastError获得的错误代码竟然是0,真是郁闷。
后来,实在不行了,我使用setsockopt函数设置了一下客户端接收缓冲区的大小,我做了很多实验,发现,因为服务器端好想发送的很快,而客户端接受的确比较慢,这样一来,当客户端的接受缓冲区足够大后,发送接受数据就都没有问题了。我实验了一下,服务器发送几百MB的数据,客户端都能正常接收到。
因此,我有如下问题:
1、服务器端向客户端发送数据的时候,是不是只要客户端接受缓冲区满了,服务器端就会发送失败?
2、为什么我的客户端用recv函数接受数据这么慢呢?服务器端的数据包早就发完了,客户端还在慢慢接受数据。这是不是正常现象呢?
3、是不是正是上面说的第一个问题,才导致我现在发现的问题呢。
4、客户端在执行recv的时候,确实会将接收缓冲区里的数据清除掉所接受到的大小吗?
5、如果我在1,2,3这三个问题上的理解是正确的话,该如何处理,能很好地解决呢。
谢谢各位了,要是有好的解决办法或则思路,我会追加分数。
目前,暂时就只能用增加客户端接受缓冲区大小的方法解决我的问题了。我为了方面,直接把客户端接受缓冲区设置为1024*1024*124,这对系统会有什么影响吗?我在跟踪的时候,发现客户端程序并没有因为我设置这么大的缓冲区而占用系统太多内存。
fantiyu 2008-09-16
  • 打赏
  • 举报
回复
楼主只认为服务器端有问题而忽视了客户端
这个问题显然是客户端代码有问题导致接收不完全
taitanyt 2008-09-16
  • 打赏
  • 举报
回复
感觉是你程序处理的问题
不要怀疑socket,别人都用的好好的,整理一下程序处理吧,我用socket一次传10M文件都没有问题
JonathanS666 2008-09-16
  • 打赏
  • 举报
回复
滑动窗口
yyunffu 2008-09-16
  • 打赏
  • 举报
回复
帮楼主顶!
kangaroo1012 2008-09-16
  • 打赏
  • 举报
回复
数据量大的时候是很麻烦啊,关注中……
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
另外RecvPacket和SendPacket使自己封装的发送和接收函数,没有什么特别的,就是调用了recv和send函数。
因为收到的数据是保存在一个结构里,结构的最后一个成员是一个BYTE*,这个指针是动态分配的,所以,要调用FreePacket释放,这个FreePacket也使自己封装的。
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
再看看客户端的DealDownLoadFile函数

void CNetMYS_clndlgDlg::DealDownLoadFile(PACKET *data)
{
DownLoadFileInfo *pdf = NULL;
if(data->head.cPC==0x0000) //表示开始发送数据
{
pdf = (DownLoadFileInfo *)data->pszInfo;
// 这个结构里,保存了要下载文件的大小,crc32等文件信息
if(!pdf->bExist)
{
m_listBoxMsg.AddString("下载失败,服务器端无法打开文件!");
}
else
{
CString s;
s.Format("开始下载文件,文件长度=%d", pdf->nFileLength);
m_listBoxMsg.AddString(s);
DeleteFile("C:\\ext-2.0.zip"); //如果文件存在就删除
}
}
else //这才是表示正式发送的数据
{
CFile f;
f.Open("C:\\ext-2.0.zip", CFile::modeWrite|CFile::modeCreate|CFile::modeNoTruncate, NULL);
f.SeekToEnd();
f.WriteHuge(data->pszInfo, data->head.nlen);
f.Close();
if(data->head.cPC==0x0002)
{
m_listBoxMsg.AddString("文件下载完成");
}
}
}


其实,这个函数,我是为了模拟解码,显示的过程,为了测试,我就把获得数据写到文件里了。
事实上,一旦客户端请求发送数据后,服务器要发送的数据可能会很多,如果要写到文件里的话,可能会上GB甚至更多。客户端其实不需要保存到文件里,客户端接受到数据后,进行解码,然后显示出来,就算完成任务了。
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
我的代码逻辑是这样的:
客户端先发送一个请求,这里测试的,就是发送一个数据头,然后发送一个文件名,数据头表示告诉服务器,我要下载文件,文件名字符串就告诉服务器端要下载的文件名称。
但实际上,我的需求不是下载文件,而是从服务器上不停地接受到数据,然后客户端进行解码并显示。
DealDownLoadFile(CHostSocket *socket, char* strFileName)
这个函数,就是服务器接受到客户端的下载请求后,调用的函数,第一个参数,就是客户端的socket,第二个参数就是要下载的文件名。
客户端在发送了下载请求后,就在线程函数里,不停地接受数据。每次接受到的数据,都有一个包头,因为这个接受线程负责接受的,不仅仅是下载文件,还有其他的数据。也就是说data变量,是一个PACKET类型结构,那里定义了很多成员,其中有一个成员,就是标示接收到的数据因为什么发送过来的。
pDlg->DealDownLoadFile(&data);
这句代码,就是先判断data的头结构成员,头结构成员里有开始,结束,等等这些标示。
zzutligang 2008-09-16
  • 打赏
  • 举报
回复

//服务器端发送数据的代码
void DealDownLoadFile(CHostSocket *socket, char* strFileName)
{
PACKET data;
DownLoadFileInfo df;
memset(&df, 0, sizeof(DownLoadFileInfo));
memset(&data, 0, sizeof(data));
data.head.cFC = 0x0401; //下载文件
CFile f;
int nRt = 0;
BYTE * buff = NULL;
if(f.Open(strFileName, CFile::modeRead|CFile::typeBinary|CFile::shareDenyRead, NULL))
{
//先发送文件是否存在,以及文件的长度
df.nFileLength = f.GetLength();
df.bExist = TRUE;
data.head.nlen = sizeof(df);
data.pszInfo = (BYTE*)&df;
nRt = SendPacket(socket, &data); //回送结果

//
DWORD nCount = 0;
buff = new BYTE[MAX_REQPACK_LEN];
memset(buff, 0, MAX_REQPACK_LEN);
do
{
nCount = f.ReadHuge(buff, MAX_REQPACK_LEN);
printf("read %d bytes\n", nCount);
if(nCount>0)
{
if(nCount == MAX_REQPACK_LEN)
data.head.cPC = 0x0001; //下载发送数据
else
data.head.cPC = 0x0002; //下载完成
data.head.nlen = nCount;
data.head.nSEQ ++;
data.pszInfo = buff;
nRt = SendPacket(socket, &data); //发送数据
if(nRt == SOCKET_ERROR)
{
int err = WSAGetLastError();
printf("SendPacket return %d, WSAGetLastError() = %d \n", nRt, err);
}
else
{
printf("SendPacket return %d \n", nRt);
}
//Sleep(100);
}

}while(nCount==MAX_REQPACK_LEN);
f.Close();
//清除内存
printf("complete!\n");
delete []buff;
}
else
{
//如果打开文件失败
df.nFileLength = 0;
df.bExist = FALSE;
data.head.nlen = sizeof(df);
data.pszInfo = (BYTE*)&df;
SendPacket(socket, &data); //回送结果
}
}

//客户端接受数据的代码
DWORD CALLBACK CNetMYS_clndlgDlg::ReceiveProc(LPVOID lpParm)
{
TRACE("线程函数已经开始");
CNetMYS_clndlgDlg *pDlg = (CNetMYS_clndlgDlg *)lpParm;
CHostSocket *pSocketPtr = &pDlg->m_sClient;
if(NULL == pSocketPtr) return 0;
//
while((pSocketPtr->IsConnected())&&(INVALID_SOCKET != pSocketPtr->GetSocket())){
PACKET data;
int nRet = 0;
memset(&data, 0, sizeof(data));
nRet = RecvPacket(pSocketPtr, &data); //接收数据
if(0 < nRet){
switch(data.head.cFC){
case 0x0001:
//登录
pDlg->DealUserLogin(data.head.cPC);
break;
case 0x0400:
//返回的获得目录列表
pDlg->DealGetDirectory((PMyFileInfo)data.pszInfo);
break;
case 0x0401:
//下载文件
//这个函数没有别的用处,无非就是将数据写到文件里罢了
pDlg->DealDownLoadFile(&data);
break;
case 0x0000:
goto e;
//m_pClientDlg->AcceptProc(&data);
break;
default:
break;
}
//最后要对接收的数据进行释放
FreePacket(&data);
}
//Sleep(1);
}
e:
TRACE("线程函数已经结束");
return 0;
}
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
//服务器端发送数据的代码
void DealDownLoadFile(CHostSocket *socket, char* strFileName)
{
PACKET data;
DownLoadFileInfo df;
memset(&df, 0, sizeof(DownLoadFileInfo));
memset(&data, 0, sizeof(data));
data.head.cFC = 0x0401; //下载文件
CFile f;
int nRt = 0;
BYTE * buff = NULL;
if(f.Open(strFileName, CFile::modeRead|CFile::typeBinary|CFile::shareDenyRead, NULL))
{
//先发送文件是否存在,以及文件的长度
df.nFileLength = f.GetLength();
df.bExist = TRUE;
data.head.nlen = sizeof(df);
data.pszInfo = (BYTE*)&df;
nRt = SendPacket(socket, &data); //回送结果

//
DWORD nCount = 0;
buff = new BYTE[MAX_REQPACK_LEN];
memset(buff, 0, MAX_REQPACK_LEN);
do
{
nCount = f.ReadHuge(buff, MAX_REQPACK_LEN);
printf("read %d bytes\n", nCount);
if(nCount>0)
{
if(nCount == MAX_REQPACK_LEN)
data.head.cPC = 0x0001; //下载发送数据
else
data.head.cPC = 0x0002; //下载完成
data.head.nlen = nCount;
data.head.nSEQ ++;
data.pszInfo = buff;
nRt = SendPacket(socket, &data); //发送数据
if(nRt == SOCKET_ERROR)
{
int err = WSAGetLastError();
printf("SendPacket return %d, WSAGetLastError() = %d \n", nRt, err);
}
else
{
printf("SendPacket return %d \n", nRt);
}
//Sleep(100);
}

}while(nCount==MAX_REQPACK_LEN);
f.Close();
//清除内存
printf("complete!\n");
delete []buff;
}
else
{
//如果打开文件失败
df.nFileLength = 0;
df.bExist = FALSE;
data.head.nlen = sizeof(df);
data.pszInfo = (BYTE*)&df;
SendPacket(socket, &data); //回送结果
}
}

//客户端接受数据的代码
DWORD CALLBACK CNetMYS_clndlgDlg::ReceiveProc(LPVOID lpParm)
{
TRACE("线程函数已经开始");
CNetMYS_clndlgDlg *pDlg = (CNetMYS_clndlgDlg *)lpParm;
CHostSocket *pSocketPtr = &pDlg->m_sClient;
if(NULL == pSocketPtr) return 0;
//
while((pSocketPtr->IsConnected())&&(INVALID_SOCKET != pSocketPtr->GetSocket())){
PACKET data;
int nRet = 0;
memset(&data, 0, sizeof(data));
nRet = RecvPacket(pSocketPtr, &data); //接收数据
if(0 < nRet){
switch(data.head.cFC){
case 0x0001:
//登录
pDlg->DealUserLogin(data.head.cPC);
break;
case 0x0400:
//返回的获得目录列表
pDlg->DealGetDirectory((PMyFileInfo)data.pszInfo);
break;
case 0x0401:
//下载文件
//这个函数没有别的用处,无非就是将数据写到文件里罢了
pDlg->DealDownLoadFile(&data);
break;
case 0x0000:
goto e;
//m_pClientDlg->AcceptProc(&data);
break;
default:
break;
}
//最后要对接收的数据进行释放
FreePacket(&data);
}
//Sleep(1);
}
e:
TRACE("线程函数已经结束");
return 0;
}
fantiyu 2008-09-16
  • 打赏
  • 举报
回复
服务器和客户端发送接收的关键代码贴出来看看
zzutligang 2008-09-16
  • 打赏
  • 举报
回复
又发现了问题,我在客户端请求了一个大约1G的数据,在接受到200MB的时候,又无法接受了,服务器发送又返回-1,但WSAGetLastError还是返回0,客户端接受到大约200MB后,就接受不到数据了,但客户端进程占用内存却飞速上升。最后几乎将系统拖垮!
看来,我那个设置客户端接受缓冲区大小的方法,好像并不好用!!!!!!!!!!

再次问一下,默认情况下,比如客户端在接受到10K的数据后,会不会将这个10K从接受缓冲区里移除呢?
zzutligang 2008-09-15
  • 打赏
  • 举报
回复
我在测试的时候,发现这样一个现象,服务器端发送数据,每次发送MAX_SENDBUFF_LEN长度的数据,总是发现服务器端很快就发完了,每次send返回值都是等于MAX_SENDBUFF_LEN,这应该说明发完。 (因为我在一台机器上测试的)。
而服务器端接收的时候,前几个包,每个包确实能接受到MAX_SENDBUFF_LEN 长的数据,并且数据也是对的。
但再往后,客户端就无法接收到数据,或则接收到的数据都是0x00。
我总觉得好像是服务器端发送的太快,客户端无法及时接收造成了。
后来,我在服务器端发送数据的时候,每发送一次,就sleep(1000),好,这回好了,可以正常接收到数据了。
可这不是解决问题的办法呀,我总不能在服务器每发送一次都sleep(1000)吧。
zzutligang 2008-09-15
  • 打赏
  • 举报
回复
回4楼,虽然包长是有限制,但既然是tcp,也就是可靠传输,为什么不让我发送10K的数据,更何况,我在调用send函数发送10K的数据时候,发现返回值确实是10K,也就是说,确实发送成功了。我是在同一台机器上测试的,可以排除网络状况不好的情况。
我又做了如下试验。
让客户端每次都发送一个请求,服务器每接收到一个请求,就发送10K的数据,这样,就算服务器端发送几百MB的数据,都不会出错。
我虽然可以把程序改成这样,但还是觉得不太合适,因为还有其他的传递数据的操作不能用这用方法。
而我的客户端接收数据是在一个线程函数里实现的。
怎么办?

回3楼,虽然可以用非阻塞模式,但程序已经写到这份上了,如果能解决这个问题,还是不想改。不过如果实在解决不了,只能改了。
zzutligang 2008-09-15
  • 打赏
  • 举报
回复
上面的
“如果按照楼上的发送代码,如果一直发送错误,那服务器端不就进入死循环了吗?”
这句说错了,楼上的代码是遇到错误,就直接退出发送。
那问题就又来了,服务器不发送了,客户端怎么办?客户端还在那里傻乎乎地等数据来呢。
因为我用的是阻塞模式。
虽然我的客户端接收数据的操作是在一个线程里,但服务器没有发送完,客户端就没有办法知道服务器端怎么了。
oo_v_oo 2008-09-15
  • 打赏
  • 举报
回复
#define MAX_SENDBUFF_LEN 1024*10

建议LZ每次发送数据的字节数控制在1k字节以内,TCP/UDP底层包长是有限制的,具体109X还是其他,忘了
ouyh12345 2008-09-15
  • 打赏
  • 举报
回复
用非阻塞socket,并循环接收
zzutligang 2008-09-15
  • 打赏
  • 举报
回复
感谢楼上的帮助。
的代码大致如下
//MAX_SENDBUFF_LEN是定义的一次传送的数据大小,比如
#define MAX_SENDBUFF_LEN 1024*10 //表示一次传10K
int nSendLen = send(m_s, buffer, MAX_SENDBUFF_LEN, 0);
我在每次发送的时候,都判断返回值了
只有返回值nSendLen==MAX_SENDBUFF_LEN的时候,才认为发送成功,我看了服务器端每次调用send,返回值都是待发送字节数。这应该表示发送成功了。
在客户端,会出现以下几种情况。
第一,客户端只受到几个MAX_SENDBUFF_LEN,然后就停止那个接收函数上了(因为我用的是阻塞模式)。这就是表示客户端没有可接收的数据包了。
第二,客户端会不停地接受数据,比如服务器总共就发了1MB,但客户端确不停地接受,并且发现接收到的数据都是0x00,我为了调试,把接收到的数据保存到文件里,发现这个文件不停地增长。但用十六进制编辑器打开这个文件,发现文件后面都是0x00。
真搞不懂!
令外,如果按照楼上的发送代码,如果一直发送错误,那服务器端不就进入死循环了吗?
那么,一般情况下,控制发送多少次失败,就认为网络出问题了,不能再发了。也就是重试次数设置为多少,比较合适呢?
加载更多回复(1)
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创作助手写篇文章吧