#include <Winsock2.h>
#include <stdio.h>
#include <windows.h>
#include <string.h>
DWORD WINAPI RecvProc(
LPVOID lpParameter // thread data
);
BOOL g_bRun;
void main()
{
WORD wVersionRequseted;
WSADATA wsaData;
int err;
wVersionRequseted = MAKEWORD(1, 1);
err = WSAStartup(wVersionRequseted, &wsaData);
if(err!=0)
{
return;
}
if(LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) !=1)
{
WSACleanup();
return;
}
SOCKET socketClt = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrClt;
addrClt.sin_addr.S_un.S_addr = inet_addr("172.20.33.82");
addrClt.sin_family = AF_INET;
addrClt.sin_port = htons(5000);
int i= connect(socketClt, (SOCKADDR *)&addrClt, sizeof(SOCKADDR));
if(i==0)
{
printf("connect sucess!\n");
g_bRun = TRUE;
}
else
{
printf("connect error!");
return;
}
HANDLE hRecvThread;
hRecvThread=CreateThread(NULL,0,RecvProc,(LPVOID)socketClt,0,NULL);
char ch;
while(1)
{
ch = getchar();
if(ch=='q')
{
g_bRun=FALSE;
break;
}
}
}
DWORD WINAPI RecvProc(
LPVOID lpParameter // thread data
)
{
char recvBuf[512];
int len;
memset(recvBuf,0 ,512);
FILE *fp = fopen("file.txt", "w");
SOCKET sockClient =(SOCKET)lpParameter;
while(g_bRun)
{
len=recv(sockClient, recvBuf, 512,0);
if(len==SOCKET_ERROR)
{
int errCode=GetLastError();
printf("链接错误:%d\n",errCode);
break;
}else if(len==0)
{
printf("服务器断开\n");
break;
}
for(int i=0;i<len;i++)
{
fprintf(fp,"%d\t",recvBuf[i]); //save data
}
printf("%d\n",len);
}
fclose(fp);
printf("线程终止\n");
return 0;
}
在数据接收线程中,recv函数阻塞当前线程等待数据到来,返回实际copy的字节数(copy表示TCP/IP协议将数据拷贝到socketClient指定的缓冲区中),如果在copy的时候出现网络错误(比如被其它应用程序中断),那么它将返回SOCKET_ERROR(-1),这时可调用GetLastError()查找错误原因,如果服务器“优雅”的中断了链接,那么它将返回0。
如果socketClient接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待(阻塞),直到协议把数据接收完毕,当数据接收完毕,recv就把socketClient接收缓存区的数据copy到recvBuf中。
注意:
1. 协议接收到的数据可能大于recvBuf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的。
(对于winsock, AFD.SYS驱动负责缓冲区的管理,也就是说,当一个程序调用send或WSASend函数发送数据的时候,数据被复制到AFD.SYS的内部缓冲里(大小根据SO_SNDBUF设置),然后send和WSASend立刻返回。之后数据由AFD.SYS负责发送到网络上,与应用程序无关。当然,如果应用程序希望发送比SO_SNDBUF设置的缓冲区还大的数据,WSASend函数将会被堵塞,直到所有数据均被发送完毕为止。同样,当从远地客户端接受数据的时候,如果应用程序没有提交receive请求,而且线上数据没有超出SO_RCVBUF设置的缓冲大小,那么AFD.SYS就把网络上的数据复制到自己的内部缓冲保存。当应用程序调用recv或WSARecv函数的时候,数据即从AFD.SYS的缓冲复制到应用程序提供的缓冲区里。)
2. tcp是一种流式的协议,发送方在发送一个固定长度数据时,可能被拆成多个包发送,因此接收方就需要进行多次接收,直到接收完整。比如发送500字节数据,可能发送的时候就被拆成100,200,200发送,接收的时候就要读取3次。
3. WIndows默认的socket缓存区大小为8K,如果要重新设置缓冲区大小可调用以下函数,但不建议修改:
int setsockopt( SOCKET s,
int level, // SOL_SOCKET
int optname, // SO_RCVBUF or SO_SNDBUF
const char* optval,
int optlen
);
示例代码如下所示(注意须在connect函数之前设置):
int optVal = 1024*20; // 20K
int optLen = sizeof(int);
setsockopt(sockSrv, SOL_SOCKET, SO_RCVBUF, (char *)&optVal, optLen);
如果确实有大量数据发送,需注意send函数的返回值(实际发送出去的字节数),可参看以下代码(来源于网络):
char *Buf;
#define BUF_LEN 10*1024*1024//10MB
Buf = new char[BUF_LEN];//此时Buf当中都是随机数据,实际需要的话,自己填充
int iNeedSend = BUF_LEN;
char *p = Buf;
while(iNeedSend){
int iRet = send(sock,p,iNeedSend,0);
if(iRet <= 0) return;//小于0表示出错,等于0表示断开
p += iRet;
iNeedSend -= iRet;
}
delete[] Buf;
Ps:在实际应用中,服务器端以固定的速率(大概200kB/s)发送128字节数据,接收的时候发现被拆包,大部分时候是以112和16接收。最终数据保存完整,没有出现丢包现象。
如果双方传输速率比较高的情况下,参考下面解决方案(来源于网络):
作为一个套接字,它拥有两个缓冲,接收数据缓冲和发送数据缓冲(此缓冲不同与你自己定义的缓冲),当有数据到达时,首先进入的就是接收数据缓冲,然后用户从这个缓冲中将数据读出来,这就是套接字接受的过程,这个缓冲的大小可以自己用SetSocketOpt()设定,同时操作系统对它有一个默认大小,如果对方在很短时间内发送大量数据到达这个套接子时,可能它没来得及接收完,因此接收缓冲处于满的状态,再有数据来的时候就进不去了,因此对方的 SEND可能就返回错误,在对方发送的数据量很小时不会出现这种情况,当数据量很大时,情况就很明显了,很容易造成收不到的情况。同样,发送方的发送缓冲也有相对应的问题。解决这个问题的办法有几种:
1. 利用SetSocketOpt()函数将接收方套接子接收缓冲设为足够大小;
2. 发送方进行数据发送时判断发送是否成功,如果不成功重发;
3. 要求接收方收到数据后给发送方回应,发送方只在收到回应后才发送下一条数据。